302 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const env = require('../config/env');
 | |
| 
 | |
| const { createFilePath } = require('gatsby-source-filesystem');
 | |
| const uniq = require('lodash/uniq');
 | |
| 
 | |
| 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.'
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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 missing or invalid. Required for donations.'
 | |
|       );
 | |
|     }
 | |
|   }
 | |
|   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
 | |
|         result.data.allMarkdownRemark.edges.forEach(edge => {
 | |
|           const {
 | |
|             node: { frontmatter, fields }
 | |
|           } = edge;
 | |
| 
 | |
|           if (!fields) {
 | |
|             return null;
 | |
|           }
 | |
|           const { slug, nodeIdentity } = fields;
 | |
|           if (slug.includes('LICENCE')) {
 | |
|             return null;
 | |
|           }
 | |
|           try {
 | |
|             if (nodeIdentity === 'blockIntroMarkdown') {
 | |
|               if (!blocks.some(block => block === frontmatter.block)) {
 | |
|                 return null;
 | |
|               }
 | |
|             } else if (
 | |
|               !superBlocks.some(
 | |
|                 superBlock => superBlock === frontmatter.superBlock
 | |
|               )
 | |
|             ) {
 | |
|               return null;
 | |
|             }
 | |
|             const pageBuilder = createByIdentityMap[nodeIdentity](createPage);
 | |
|             return pageBuilder(edge);
 | |
|           } catch (e) {
 | |
|             console.log(`
 | |
|             ident: ${nodeIdentity} does not belong to a function
 | |
| 
 | |
|             ${frontmatter ? JSON.stringify(edge.node) : 'no frontmatter'}
 | |
| 
 | |
| 
 | |
|             `);
 | |
|           }
 | |
|           return null;
 | |
|         });
 | |
| 
 | |
|         return null;
 | |
|       })
 | |
|     );
 | |
|   });
 | |
| };
 | |
| 
 | |
| const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
 | |
| 
 | |
| exports.onCreateWebpackConfig = ({ stage, plugins, actions }) => {
 | |
|   const newPlugins = [
 | |
|     plugins.define({
 | |
|       HOME_PATH: JSON.stringify(
 | |
|         process.env.HOME_PATH || 'http://localhost:3000'
 | |
|       ),
 | |
|       STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || '')
 | |
|     })
 | |
|   ];
 | |
|   // 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());
 | |
|   }
 | |
|   actions.setWebpackConfig({
 | |
|     node: {
 | |
|       fs: 'empty'
 | |
|     },
 | |
|     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
 | |
|       },
 | |
|       lodash: {
 | |
|         transform: 'lodash/${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
 | |
|     }
 | |
|     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);
 | |
| // };
 |