* fix: remove circular dependency redux depended on templates/Challenges/redux and vice versa. This meant that import order mattered and confusing bugs could arise. (cherry picked from commit 7d67a4e70922bbb3051f2f9982dcc69e240d43dc) * feat: require imports to be in alphabetical order Import order generally does not matter, but there are edge cases (circular imports and css imports, for example) where changing order changes behaviour (cherry picked from commit b8d1393a91ec6e068caf8e8498a5c95df68c2b2c) * chore: order imports * fix: lift up challenge description + title comps This brings the classic Show closer to the others as they now all create the description and title components * fix: remove donation-saga/index circular import (cherry picked from commit 51a44ca668a700786d2744feffeae4fdba5fd207) * refactor: extract action-types from settings (cherry picked from commit 25e26124d691c84a0d0827d41dafb761c686fadd) * fix: lint errors * feat: prevent useless renames
299 lines
8.3 KiB
JavaScript
299 lines
8.3 KiB
JavaScript
const { createFilePath } = require('gatsby-source-filesystem');
|
|
// TODO: ideally we'd remove lodash and just use lodash-es, but we can't require
|
|
// es modules here.
|
|
const uniq = require('lodash/uniq');
|
|
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
|
const webpack = require('webpack');
|
|
const env = require('../config/env.json');
|
|
|
|
const { blockNameify } = require('../utils/block-nameify');
|
|
const {
|
|
createChallengePages,
|
|
createBlockIntroPages,
|
|
createSuperBlockIntroPages
|
|
} = require('./utils/gatsby');
|
|
|
|
const createByIdentityMap = {
|
|
blockIntroMarkdown: createBlockIntroPages,
|
|
superBlockIntroMarkdown: createSuperBlockIntroPages
|
|
};
|
|
|
|
exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
|
|
const { createNodeField } = actions;
|
|
if (node.internal.type === 'ChallengeNode') {
|
|
const { tests = [], block, dashedName, superBlock } = node;
|
|
const slug = `/learn/${superBlock}/${block}/${dashedName}`;
|
|
createNodeField({ node, name: 'slug', value: slug });
|
|
createNodeField({ node, name: 'blockName', value: blockNameify(block) });
|
|
createNodeField({ node, name: 'tests', value: tests });
|
|
}
|
|
|
|
if (node.internal.type === 'MarkdownRemark') {
|
|
const slug = createFilePath({ node, getNode });
|
|
if (!slug.includes('LICENSE')) {
|
|
const {
|
|
frontmatter: { component = '' }
|
|
} = node;
|
|
createNodeField({ node, name: 'slug', value: slug });
|
|
createNodeField({ node, name: 'component', value: component });
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.createPages = function createPages({ graphql, actions, reporter }) {
|
|
if (!env.algoliaAPIKey || !env.algoliaAppId) {
|
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
|
throw new Error(
|
|
'Algolia App id and API key are required to start the client!'
|
|
);
|
|
} else {
|
|
reporter.info(
|
|
'Algolia keys missing or invalid. Required for search to yield results.'
|
|
);
|
|
}
|
|
}
|
|
|
|
const { createPage } = actions;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// Query for all markdown 'nodes' and for the slug we previously created.
|
|
resolve(
|
|
graphql(`
|
|
{
|
|
allChallengeNode(
|
|
sort: { fields: [superOrder, order, challengeOrder] }
|
|
) {
|
|
edges {
|
|
node {
|
|
block
|
|
challengeType
|
|
fields {
|
|
slug
|
|
}
|
|
id
|
|
order
|
|
required {
|
|
link
|
|
src
|
|
}
|
|
challengeOrder
|
|
superBlock
|
|
superOrder
|
|
template
|
|
}
|
|
}
|
|
}
|
|
allMarkdownRemark {
|
|
edges {
|
|
node {
|
|
fields {
|
|
slug
|
|
nodeIdentity
|
|
component
|
|
}
|
|
frontmatter {
|
|
block
|
|
superBlock
|
|
title
|
|
}
|
|
htmlAst
|
|
id
|
|
excerpt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`).then(result => {
|
|
if (result.errors) {
|
|
console.log(result.errors);
|
|
return reject(result.errors);
|
|
}
|
|
|
|
// Create challenge pages.
|
|
result.data.allChallengeNode.edges.forEach(
|
|
createChallengePages(createPage)
|
|
);
|
|
|
|
const blocks = uniq(
|
|
result.data.allChallengeNode.edges.map(({ node: { block } }) => block)
|
|
).map(block => blockNameify(block));
|
|
|
|
const superBlocks = uniq(
|
|
result.data.allChallengeNode.edges.map(
|
|
({ node: { superBlock } }) => superBlock
|
|
)
|
|
);
|
|
|
|
// Create intro pages
|
|
// TODO: Remove allMarkdownRemark (populate from elsewhere)
|
|
result.data.allMarkdownRemark.edges.forEach(edge => {
|
|
const {
|
|
node: { frontmatter, fields }
|
|
} = edge;
|
|
|
|
if (!fields) {
|
|
return;
|
|
}
|
|
const { slug, nodeIdentity } = fields;
|
|
if (slug.includes('LICENCE')) {
|
|
return;
|
|
}
|
|
try {
|
|
if (nodeIdentity === 'blockIntroMarkdown') {
|
|
if (!blocks.includes(frontmatter.block)) {
|
|
return;
|
|
}
|
|
} else if (!superBlocks.includes(frontmatter.superBlock)) {
|
|
return;
|
|
}
|
|
const pageBuilder = createByIdentityMap[nodeIdentity](createPage);
|
|
pageBuilder(edge);
|
|
} catch (e) {
|
|
console.log(`
|
|
ident: ${nodeIdentity} does not belong to a function
|
|
|
|
${frontmatter ? JSON.stringify(edge.node) : 'no frontmatter'}
|
|
|
|
|
|
`);
|
|
}
|
|
});
|
|
|
|
return null;
|
|
})
|
|
);
|
|
});
|
|
};
|
|
|
|
exports.onCreateWebpackConfig = ({ stage, actions }) => {
|
|
const newPlugins = [
|
|
// We add the shims of the node globals to the global scope
|
|
new webpack.ProvidePlugin({
|
|
Buffer: ['buffer', 'Buffer']
|
|
}),
|
|
new webpack.ProvidePlugin({
|
|
process: 'process/browser'
|
|
})
|
|
];
|
|
// The monaco editor relies on some browser only globals so should not be
|
|
// involved in SSR. Also, if the plugin is used during the 'build-html' stage
|
|
// it overwrites the minfied files with ordinary ones.
|
|
if (stage !== 'build-html') {
|
|
newPlugins.push(
|
|
new MonacoWebpackPlugin({ filename: '[name].worker-[contenthash].js' })
|
|
);
|
|
}
|
|
actions.setWebpackConfig({
|
|
resolve: {
|
|
fallback: {
|
|
fs: false,
|
|
path: require.resolve('path-browserify'),
|
|
assert: require.resolve('assert'),
|
|
crypto: require.resolve('crypto-browserify'),
|
|
util: false,
|
|
buffer: require.resolve('buffer'),
|
|
stream: require.resolve('stream-browserify'),
|
|
process: require.resolve('process/browser')
|
|
}
|
|
},
|
|
plugins: newPlugins
|
|
});
|
|
};
|
|
|
|
exports.onCreateBabelConfig = ({ actions }) => {
|
|
actions.setBabelPlugin({
|
|
name: '@babel/plugin-proposal-function-bind'
|
|
});
|
|
actions.setBabelPlugin({
|
|
name: '@babel/plugin-proposal-export-default-from'
|
|
});
|
|
actions.setBabelPlugin({
|
|
name: 'babel-plugin-transform-imports',
|
|
options: {
|
|
'@freecodecamp/react-bootstrap': {
|
|
transform: '@freecodecamp/react-bootstrap/lib/${member}',
|
|
preventFullImport: true
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
exports.onCreatePage = async ({ page, actions }) => {
|
|
const { createPage } = actions;
|
|
// Only update the `/challenges` page.
|
|
if (page.path.match(/^\/challenges/)) {
|
|
// page.matchPath is a special key that's used for matching pages
|
|
// with corresponding routes only on the client.
|
|
page.matchPath = '/challenges/*';
|
|
// Update the page.
|
|
createPage(page);
|
|
}
|
|
};
|
|
|
|
exports.createSchemaCustomization = ({ actions }) => {
|
|
const { createTypes } = actions;
|
|
const typeDefs = `
|
|
type ChallengeNode implements Node {
|
|
files: ChallengeFile
|
|
url: String
|
|
}
|
|
type ChallengeFile {
|
|
indexcss: FileContents
|
|
indexhtml: FileContents
|
|
indexjs: FileContents
|
|
indexjsx: FileContents
|
|
}
|
|
type FileContents {
|
|
key: String
|
|
ext: String
|
|
name: String
|
|
contents: String
|
|
head: String
|
|
tail: String
|
|
editableRegionBoundaries: [Int]
|
|
}
|
|
`;
|
|
createTypes(typeDefs);
|
|
};
|
|
|
|
// TODO: this broke the React challenges, not sure why, but I'll investigate
|
|
// further and reimplement if it's possible and necessary (Oliver)
|
|
// I'm still not sure why, but the above schema seems to work.
|
|
// Typically the schema can be inferred, but not when some challenges are
|
|
// skipped (at time of writing the Chinese only has responsive web design), so
|
|
// this makes the missing fields explicit.
|
|
// exports.createSchemaCustomization = ({ actions }) => {
|
|
// const { createTypes } = actions;
|
|
// const typeDefs = `
|
|
// type ChallengeNode implements Node {
|
|
// question: Question
|
|
// videoId: String
|
|
// required: ExternalFile
|
|
// files: ChallengeFile
|
|
// }
|
|
// type Question {
|
|
// text: String
|
|
// answers: [String]
|
|
// solution: Int
|
|
// }
|
|
// type ChallengeFile {
|
|
// indexhtml: FileContents
|
|
// indexjs: FileContents
|
|
// indexjsx: FileContents
|
|
// }
|
|
// type ExternalFile {
|
|
// link: String
|
|
// src: String
|
|
// }
|
|
// type FileContents {
|
|
// key: String
|
|
// ext: String
|
|
// name: String
|
|
// contents: String
|
|
// head: String
|
|
// tail: String
|
|
// }
|
|
// `;
|
|
// createTypes(typeDefs);
|
|
// };
|