Files
freeCodeCamp/client/gatsby-node.js
Oliver Eyton-Williams e118dda13a fix: order imports and remove circular dependencies (#41824)
* 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
2021-08-02 08:39:40 -05:00

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);
// };