2018-10-23 13:18:46 +00:00
|
|
|
const { createFilePath } = require('gatsby-source-filesystem');
|
2021-04-28 21:11:20 +00:00
|
|
|
// TODO: ideally we'd remove lodash and just use lodash-es, but we can't require
|
|
|
|
// es modules here.
|
2020-09-10 10:40:41 +00:00
|
|
|
const uniq = require('lodash/uniq');
|
2021-08-02 13:39:40 +00:00
|
|
|
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
|
|
|
const webpack = require('webpack');
|
|
|
|
const env = require('../config/env.json');
|
2018-10-04 13:47:55 +00:00
|
|
|
|
2020-06-13 09:27:15 +00:00
|
|
|
const { blockNameify } = require('../utils/block-nameify');
|
2018-10-04 13:47:55 +00:00
|
|
|
const {
|
|
|
|
createChallengePages,
|
|
|
|
createBlockIntroPages,
|
2019-07-25 08:23:42 +00:00
|
|
|
createSuperBlockIntroPages
|
2018-10-04 13:47:55 +00:00
|
|
|
} = require('./utils/gatsby');
|
|
|
|
|
|
|
|
const createByIdentityMap = {
|
|
|
|
blockIntroMarkdown: createBlockIntroPages,
|
|
|
|
superBlockIntroMarkdown: createSuperBlockIntroPages
|
|
|
|
};
|
2018-09-30 10:37:19 +00:00
|
|
|
|
2018-10-04 13:47:55 +00:00
|
|
|
exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
|
2018-09-30 10:37:19 +00:00
|
|
|
const { createNodeField } = actions;
|
|
|
|
if (node.internal.type === 'ChallengeNode') {
|
2020-10-01 13:07:03 +00:00
|
|
|
const { tests = [], block, dashedName, superBlock } = node;
|
2021-02-23 04:22:48 +00:00
|
|
|
const slug = `/learn/${superBlock}/${block}/${dashedName}`;
|
2018-09-30 10:37:19 +00:00
|
|
|
createNodeField({ node, name: 'slug', value: slug });
|
|
|
|
createNodeField({ node, name: 'blockName', value: blockNameify(block) });
|
|
|
|
createNodeField({ node, name: 'tests', value: tests });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.internal.type === 'MarkdownRemark') {
|
2019-02-04 00:46:27 +00:00
|
|
|
const slug = createFilePath({ node, getNode });
|
2018-10-04 13:47:55 +00:00
|
|
|
if (!slug.includes('LICENSE')) {
|
2019-02-04 00:46:27 +00:00
|
|
|
const {
|
|
|
|
frontmatter: { component = '' }
|
|
|
|
} = node;
|
2018-10-04 13:47:55 +00:00
|
|
|
createNodeField({ node, name: 'slug', value: slug });
|
2019-02-04 00:46:27 +00:00
|
|
|
createNodeField({ node, name: 'component', value: component });
|
2018-09-30 10:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-10-08 08:19:43 +00:00
|
|
|
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.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2019-11-29 20:11:31 +00:00
|
|
|
|
2021-08-08 20:22:25 +00:00
|
|
|
if (!env.stripePublicKey) {
|
|
|
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
|
|
|
throw new Error('Stripe public key is required to start the client!');
|
|
|
|
} else {
|
|
|
|
reporter.info(
|
|
|
|
'Stripe public key is missing or invalid. Required for Stripe integration.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-30 10:37:19 +00:00
|
|
|
const { createPage } = actions;
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
// Query for all markdown 'nodes' and for the slug we previously created.
|
|
|
|
resolve(
|
|
|
|
graphql(`
|
|
|
|
{
|
2018-10-04 13:47:55 +00:00
|
|
|
allChallengeNode(
|
|
|
|
sort: { fields: [superOrder, order, challengeOrder] }
|
|
|
|
) {
|
2018-09-30 10:37:19 +00:00
|
|
|
edges {
|
|
|
|
node {
|
|
|
|
block
|
|
|
|
challengeType
|
|
|
|
fields {
|
|
|
|
slug
|
|
|
|
}
|
|
|
|
id
|
|
|
|
order
|
|
|
|
required {
|
|
|
|
link
|
|
|
|
src
|
|
|
|
}
|
2018-10-04 13:47:55 +00:00
|
|
|
challengeOrder
|
2018-09-30 10:37:19 +00:00
|
|
|
superBlock
|
|
|
|
superOrder
|
|
|
|
template
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
allMarkdownRemark {
|
|
|
|
edges {
|
|
|
|
node {
|
|
|
|
fields {
|
|
|
|
slug
|
2018-10-23 13:18:46 +00:00
|
|
|
nodeIdentity
|
2019-02-04 00:46:27 +00:00
|
|
|
component
|
2018-09-30 10:37:19 +00:00
|
|
|
}
|
|
|
|
frontmatter {
|
|
|
|
block
|
|
|
|
superBlock
|
|
|
|
title
|
|
|
|
}
|
2018-10-04 13:47:55 +00:00
|
|
|
htmlAst
|
|
|
|
id
|
|
|
|
excerpt
|
2018-09-30 10:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`).then(result => {
|
|
|
|
if (result.errors) {
|
|
|
|
console.log(result.errors);
|
2018-10-23 13:18:46 +00:00
|
|
|
return reject(result.errors);
|
2018-09-30 10:37:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create challenge pages.
|
2020-05-28 15:26:19 +00:00
|
|
|
result.data.allChallengeNode.edges.forEach(
|
|
|
|
createChallengePages(createPage)
|
2018-09-30 10:37:19 +00:00
|
|
|
);
|
|
|
|
|
2020-09-10 10:40:41 +00:00
|
|
|
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
|
|
|
|
)
|
2021-02-13 05:06:04 +00:00
|
|
|
);
|
2020-09-10 10:40:41 +00:00
|
|
|
|
2018-09-30 10:37:19 +00:00
|
|
|
// Create intro pages
|
2021-04-23 19:29:17 +00:00
|
|
|
// TODO: Remove allMarkdownRemark (populate from elsewhere)
|
2018-10-04 13:47:55 +00:00
|
|
|
result.data.allMarkdownRemark.edges.forEach(edge => {
|
|
|
|
const {
|
2018-10-23 13:18:46 +00:00
|
|
|
node: { frontmatter, fields }
|
2018-10-04 13:47:55 +00:00
|
|
|
} = edge;
|
2018-10-23 13:18:46 +00:00
|
|
|
|
2018-10-04 13:47:55 +00:00
|
|
|
if (!fields) {
|
2021-07-19 12:18:17 +00:00
|
|
|
return;
|
2018-10-04 13:47:55 +00:00
|
|
|
}
|
2018-10-23 13:18:46 +00:00
|
|
|
const { slug, nodeIdentity } = fields;
|
2018-10-04 13:47:55 +00:00
|
|
|
if (slug.includes('LICENCE')) {
|
2021-07-19 12:18:17 +00:00
|
|
|
return;
|
2018-10-04 13:47:55 +00:00
|
|
|
}
|
|
|
|
try {
|
2020-09-10 10:40:41 +00:00
|
|
|
if (nodeIdentity === 'blockIntroMarkdown') {
|
2021-07-19 12:18:17 +00:00
|
|
|
if (!blocks.includes(frontmatter.block)) {
|
|
|
|
return;
|
2020-09-10 10:40:41 +00:00
|
|
|
}
|
2021-07-19 12:18:17 +00:00
|
|
|
} else if (!superBlocks.includes(frontmatter.superBlock)) {
|
|
|
|
return;
|
2020-09-10 10:40:41 +00:00
|
|
|
}
|
2018-10-23 13:18:46 +00:00
|
|
|
const pageBuilder = createByIdentityMap[nodeIdentity](createPage);
|
2021-07-19 12:18:17 +00:00
|
|
|
pageBuilder(edge);
|
2018-10-04 13:47:55 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.log(`
|
2018-10-23 13:18:46 +00:00
|
|
|
ident: ${nodeIdentity} does not belong to a function
|
2018-10-04 13:47:55 +00:00
|
|
|
|
|
|
|
${frontmatter ? JSON.stringify(edge.node) : 'no frontmatter'}
|
|
|
|
|
|
|
|
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
});
|
2018-09-30 10:37:19 +00:00
|
|
|
|
2018-10-23 13:18:46 +00:00
|
|
|
return null;
|
2018-09-30 10:37:19 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-07-27 15:35:14 +00:00
|
|
|
exports.onCreateWebpackConfig = ({ stage, actions }) => {
|
2020-02-06 11:24:00 +00:00
|
|
|
const newPlugins = [
|
2021-03-25 19:13:43 +00:00
|
|
|
// We add the shims of the node globals to the global scope
|
|
|
|
new webpack.ProvidePlugin({
|
|
|
|
Buffer: ['buffer', 'Buffer']
|
|
|
|
}),
|
|
|
|
new webpack.ProvidePlugin({
|
|
|
|
process: 'process/browser'
|
2020-02-06 11:24:00 +00:00
|
|
|
})
|
|
|
|
];
|
|
|
|
// 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') {
|
2021-05-11 12:56:06 +00:00
|
|
|
newPlugins.push(
|
|
|
|
new MonacoWebpackPlugin({ filename: '[name].worker-[contenthash].js' })
|
|
|
|
);
|
2020-02-06 11:24:00 +00:00
|
|
|
}
|
2018-09-30 10:37:19 +00:00
|
|
|
actions.setWebpackConfig({
|
2021-03-25 19:13:43 +00:00
|
|
|
resolve: {
|
|
|
|
fallback: {
|
|
|
|
fs: false,
|
2021-03-27 13:49:30 +00:00
|
|
|
path: require.resolve('path-browserify'),
|
2021-03-25 19:13:43 +00:00
|
|
|
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')
|
|
|
|
}
|
2018-09-30 10:37:19 +00:00
|
|
|
},
|
2020-02-06 11:24:00 +00:00
|
|
|
plugins: newPlugins
|
2018-09-30 10:37:19 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2020-03-04 14:37:06 +00:00
|
|
|
|
2020-08-21 16:50:05 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-09-09 17:22:20 +00:00
|
|
|
exports.createSchemaCustomization = ({ actions }) => {
|
|
|
|
const { createTypes } = actions;
|
|
|
|
const typeDefs = `
|
|
|
|
type ChallengeNode implements Node {
|
|
|
|
files: ChallengeFile
|
2021-06-15 15:37:13 +00:00
|
|
|
url: String
|
2020-09-09 17:22:20 +00:00
|
|
|
}
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2020-08-18 01:09:19 +00:00
|
|
|
// TODO: this broke the React challenges, not sure why, but I'll investigate
|
|
|
|
// further and reimplement if it's possible and necessary (Oliver)
|
2020-09-09 17:22:20 +00:00
|
|
|
// I'm still not sure why, but the above schema seems to work.
|
2020-03-04 14:37:06 +00:00
|
|
|
// 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.
|
2020-08-18 01:09:19 +00:00
|
|
|
// 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);
|
|
|
|
// };
|