feat(client): ts-migrate client/utils/** (#42823)

* rename js files

* update imports and references

* migrate build-challenges

* migrate challenge-types

* migrate utils/index

* migrate state-management

* install @types/psl for tags

* migrate tags

* migrate tags.test

* migrate challenge-page-creator

* migrate utils/gatsby/index

* migrate layout-selector

* migrate layout-selector.test

* revert challenge-types

Curriculum can't handle TS or modules

* convert arrow functions

* revert build-challenges

* revert utils/gatsby/index

* revert challenge-page-creator

* revert challenge-types reference

* Delete state-management

Deleted in #42960

* Disable render-result-naming-convention (for now)

* update layout-selector.test comment

* reorder imports in build-challenges

* change ts-ignore to ts-expect-error
This commit is contained in:
awu43
2021-08-09 01:30:31 -07:00
committed by GitHub
parent fcadd534e7
commit dd5d2919be
23 changed files with 116 additions and 83 deletions

View File

@ -4,7 +4,7 @@ const {
buildChallenges, buildChallenges,
replaceChallengeNode, replaceChallengeNode,
localeChallengesRootDir localeChallengesRootDir
} = require('./utils/buildChallenges'); } = require('./utils/build-challenges');
const { clientLocale, curriculumLocale, homeLocation } = envData; const { clientLocale, curriculumLocale, homeLocation } = envData;

View File

@ -4461,6 +4461,12 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
}, },
"@types/psl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz",
"integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==",
"dev": true
},
"@types/reach__router": { "@types/reach__router": {
"version": "1.3.9", "version": "1.3.9",
"resolved": "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.9.tgz", "resolved": "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.9.tgz",

View File

@ -138,6 +138,7 @@
"@types/loadable__component": "5.13.4", "@types/loadable__component": "5.13.4",
"@types/lodash-es": "4.17.4", "@types/lodash-es": "4.17.4",
"@types/prismjs": "1.16.6", "@types/prismjs": "1.16.6",
"@types/psl": "^1.1.0",
"@types/reach__router": "1.3.9", "@types/reach__router": "1.3.9",
"@types/react-dom": "17.0.9", "@types/react-dom": "17.0.9",
"@types/react-helmet": "6.1.2", "@types/react-helmet": "6.1.2",

View File

@ -11,7 +11,7 @@ import {
import store from 'store'; import store from 'store';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { backEndProject } from '../../utils/challengeTypes'; import { backEndProject } from '../../utils/challenge-types';
import { isGoodXHRStatus } from '../templates/Challenges/utils'; import { isGoodXHRStatus } from '../templates/Challenges/utils';
import postUpdate$ from '../templates/Challenges/utils/postUpdate$'; import postUpdate$ from '../templates/Challenges/utils/postUpdate$';
import { actionTypes } from './action-types'; import { actionTypes } from './action-types';

View File

@ -13,7 +13,7 @@ import { createStructuredSelector } from 'reselect';
// Local Utilities // Local Utilities
import store from 'store'; import store from 'store';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challenge-types';
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
import { import {
ChallengeNodeType, ChallengeNodeType,

View File

@ -7,7 +7,7 @@ import {
backEndProject, backEndProject,
frontEndProject, frontEndProject,
pythonProject pythonProject
} from '../../../../utils/challengeTypes'; } from '../../../../utils/challenge-types';
import { Form } from '../../../components/formHelpers'; import { Form } from '../../../components/formHelpers';
interface SubmitProps { interface SubmitProps {

View File

@ -10,7 +10,7 @@ import {
finalize finalize
} from 'rxjs/operators'; } from 'rxjs/operators';
import { challengeTypes, submitTypes } from '../../../../utils/challengeTypes'; import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
import { import {
userSelector, userSelector,
isSignedInSelector, isSignedInSelector,

View File

@ -3,7 +3,7 @@ import { createAction, handleActions } from 'redux-actions';
import { getLines } from '../../../../../utils/get-lines'; import { getLines } from '../../../../../utils/get-lines';
import { createPoly } from '../../../../../utils/polyvinyl'; import { createPoly } from '../../../../../utils/polyvinyl';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challenge-types';
import { completedChallengesSelector } from '../../../redux'; import { completedChallengesSelector } from '../../../redux';
import { getTargetEditor } from '../utils/getTargetEditor'; import { getTargetEditor } from '../utils/getTargetEditor';
import { actionTypes, ns } from './action-types'; import { actionTypes, ns } from './action-types';

View File

@ -3,7 +3,7 @@
import frameRunnerData from '../../../../../config/client/frame-runner.json'; import frameRunnerData from '../../../../../config/client/frame-runner.json';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import testEvaluatorData from '../../../../../config/client/test-evaluator.json'; import testEvaluatorData from '../../../../../config/client/test-evaluator.json';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challenge-types';
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
import { getTransformers } from '../rechallenge/transformers'; import { getTransformers } from '../rechallenge/transformers';
import { import {

View File

@ -1,4 +1,5 @@
const path = require('path'); const path = require('path');
const _ = require('lodash'); const _ = require('lodash');
const envData = require('../../config/env.json'); const envData = require('../../config/env.json');

View File

@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
const { dasherize } = require('../../../utils/slugs'); const { dasherize } = require('../../../utils/slugs');
const { viewTypes } = require('../challengeTypes'); const { viewTypes } = require('../challenge-types');
const backend = path.resolve( const backend = path.resolve(
__dirname, __dirname,
@ -42,21 +42,22 @@ const views = {
// quiz: Quiz // quiz: Quiz
}; };
const getNextChallengePath = (node, index, nodeArray) => { function getNextChallengePath(_node, index, nodeArray) {
const next = nodeArray[index + 1]; const next = nodeArray[index + 1];
return next ? next.node.fields.slug : '/learn'; return next ? next.node.fields.slug : '/learn';
}; }
const getPrevChallengePath = (node, index, nodeArray) => { function getPrevChallengePath(_node, index, nodeArray) {
const prev = nodeArray[index - 1]; const prev = nodeArray[index - 1];
return prev ? prev.node.fields.slug : '/learn'; return prev ? prev.node.fields.slug : '/learn';
}; }
const getTemplateComponent = challengeType => views[viewTypes[challengeType]]; function getTemplateComponent(challengeType) {
return views[viewTypes[challengeType]];
}
exports.createChallengePages = exports.createChallengePages = function (createPage) {
createPage => return function ({ node }, index, thisArray) {
({ node }, index, thisArray) => {
const { const {
superBlock, superBlock,
block, block,
@ -69,7 +70,7 @@ exports.createChallengePages =
// TODO: challengeType === 7 and isPrivate are the same, right? If so, we // TODO: challengeType === 7 and isPrivate are the same, right? If so, we
// should remove one of them. // should remove one of them.
return createPage({ createPage({
path: slug, path: slug,
component: getTemplateComponent(challengeType), component: getTemplateComponent(challengeType),
context: { context: {
@ -86,14 +87,16 @@ exports.createChallengePages =
} }
}); });
}; };
};
exports.createBlockIntroPages = createPage => edge => { exports.createBlockIntroPages = function (createPage) {
return function (edge) {
const { const {
fields: { slug }, fields: { slug },
frontmatter: { block } frontmatter: { block }
} = edge.node; } = edge.node;
return createPage({ createPage({
path: slug, path: slug,
component: intro, component: intro,
context: { context: {
@ -102,14 +105,16 @@ exports.createBlockIntroPages = createPage => edge => {
} }
}); });
}; };
};
exports.createSuperBlockIntroPages = createPage => edge => { exports.createSuperBlockIntroPages = function (createPage) {
return function (edge) {
const { const {
fields: { slug }, fields: { slug },
frontmatter: { superBlock } frontmatter: { superBlock }
} = edge.node; } = edge.node;
return createPage({ createPage({
path: slug, path: slug,
component: superBlockIntro, component: superBlockIntro,
context: { context: {
@ -118,3 +123,4 @@ exports.createSuperBlockIntroPages = createPage => edge => {
} }
}); });
}; };
};

View File

@ -1,4 +1,4 @@
const challengePageCreators = require('./challengePageCreator'); const challengePageCreators = require('./challenge-page-creator');
module.exports = { module.exports = {
...challengePageCreators ...challengePageCreators

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import ShallowRenderer from 'react-test-renderer/shallow'; import ShallowRenderer from 'react-test-renderer/shallow';
@ -11,10 +12,19 @@ import layoutSelector from './layout-selector';
jest.mock('../../src/analytics'); jest.mock('../../src/analytics');
const store = createStore(); const store = createStore();
function getComponentNameAndProps(elementType, pathname) {
const shallow = new ShallowRenderer(); interface NameAndProps {
props: Record<string, unknown>;
name: string;
}
function getComponentNameAndProps(
elementType: React.JSXElementConstructor<never>,
pathname: string
): NameAndProps {
// eslint-disable-next-line testing-library/render-result-naming-convention
const shallow = ShallowRenderer.createRenderer();
const LayoutReactComponent = layoutSelector({ const LayoutReactComponent = layoutSelector({
element: { type: elementType }, element: { type: elementType, props: {}, key: '' },
props: { props: {
location: { location: {
pathname pathname
@ -24,8 +34,13 @@ function getComponentNameAndProps(elementType, pathname) {
shallow.render(<Provider store={store}>{LayoutReactComponent}</Provider>); shallow.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
const view = shallow.getRenderOutput(); const view = shallow.getRenderOutput();
return { return {
props: view.props, props: view.props as Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
name: view.type.WrappedComponent.displayName name: view.type.WrappedComponent.displayName
// TODO: Revisit this when react-test-renderer is replaced with
// react-testing-library
}; };
} }

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { import {
CertificationLayout, CertificationLayout,
DefaultLayout DefaultLayout
@ -6,21 +7,14 @@ import {
import FourOhFourPage from '../../src/pages/404'; import FourOhFourPage from '../../src/pages/404';
import { isChallenge } from '../../src/utils/path-parsers'; import { isChallenge } from '../../src/utils/path-parsers';
interface Location {
pathname: string;
}
interface LayoutSelectorProps { interface LayoutSelectorProps {
props: { element: JSX.Element;
location: Location; props: { location: { pathname: string } };
};
element: React.ReactElement;
} }
export default function layoutSelector({ export default function layoutSelector({
element, element,
props props
}: LayoutSelectorProps): React.ReactElement { }: LayoutSelectorProps): JSX.Element {
const { const {
location: { pathname } location: { pathname }
} = props; } = props;

View File

@ -1,3 +0,0 @@
exports.isBrowser = function isBrowser() {
return typeof window !== 'undefined';
};

3
client/utils/index.ts Normal file
View File

@ -0,0 +1,3 @@
export function isBrowser(): boolean {
return typeof window !== 'undefined';
}

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { injectConditionalTags } from './tags'; import { injectConditionalTags } from './tags';
describe('Tags', () => { describe('Tags', () => {
it('injectConditionalTags should inject gap dev homelocation', () => { it('injectConditionalTags should inject gap dev homelocation', () => {
let injectedTags = injectConditionalTags( const injectedTags = injectConditionalTags(
[], [],
'https://www.freecodecamp.dev' 'https://www.freecodecamp.dev'
); );
@ -10,7 +11,7 @@ describe('Tags', () => {
expect(injectedTags[0].props.id === 'gap-dev').toBeTruthy(); expect(injectedTags[0].props.id === 'gap-dev').toBeTruthy();
}); });
it('injectConditionalTags should inject gap for english homeLocation', () => { it('injectConditionalTags should inject gap for english homeLocation', () => {
let injectedTags = injectConditionalTags( const injectedTags = injectConditionalTags(
[], [],
'https://www.freecodecamp.org' 'https://www.freecodecamp.org'
); );
@ -18,7 +19,7 @@ describe('Tags', () => {
expect(injectedTags[0].props.id === 'gap-org').toBeTruthy(); expect(injectedTags[0].props.id === 'gap-org').toBeTruthy();
}); });
it('injectConditionalTags should inject gap for espanol homeLocation', () => { it('injectConditionalTags should inject gap for espanol homeLocation', () => {
let injectedTags = injectConditionalTags( const injectedTags = injectConditionalTags(
[], [],
'https://www.freecodecamp.org/espanol' 'https://www.freecodecamp.org/espanol'
); );
@ -26,7 +27,7 @@ describe('Tags', () => {
expect(injectedTags[0].props.id === 'gap-org').toBeTruthy(); expect(injectedTags[0].props.id === 'gap-org').toBeTruthy();
}); });
it('injectConditionalTags should inject cap and chinese gap for chinese homeLocation', () => { it('injectConditionalTags should inject cap and chinese gap for chinese homeLocation', () => {
let injectedTags = injectConditionalTags( const injectedTags = injectConditionalTags(
[], [],
'https://chinese.freecodecamp.org' 'https://chinese.freecodecamp.org'
); );
@ -35,7 +36,7 @@ describe('Tags', () => {
expect(injectedTags[1].props.id === 'gap-org-chinese').toBeTruthy(); expect(injectedTags[1].props.id === 'gap-org-chinese').toBeTruthy();
}); });
it('injectConditionalTags should not inject tags for localhost homeLocation', () => { it('injectConditionalTags should not inject tags for localhost homeLocation', () => {
let injectedTags = injectConditionalTags([], 'http://localhost:8000/'); const injectedTags = injectConditionalTags([], 'http://localhost:8000/');
expect(injectedTags.length === 0).toBeTruthy(); expect(injectedTags.length === 0).toBeTruthy();
}); });
}); });

View File

@ -6,13 +6,13 @@ import env from '../../config/env.json';
const { homeLocation } = env; const { homeLocation } = env;
export const getheadTagComponents = () => { export function getheadTagComponents(): JSX.Element[] {
const socialImage = const socialImage =
'https://cdn.freecodecamp.org/platform/universal/fcc_meta_1920X1080-indigo.png'; 'https://cdn.freecodecamp.org/platform/universal/fcc_meta_1920X1080-indigo.png';
const pathToBootstrap = withPrefix('/css/bootstrap.min.css'); const pathToBootstrap = withPrefix('/css/bootstrap.min.css');
let headTags = [ const headTags = [
<link <link
as='style' as='style'
href={pathToBootstrap} href={pathToBootstrap}
@ -54,19 +54,25 @@ export const getheadTagComponents = () => {
/> />
]; ];
return injectConditionalTags(headTags, homeLocation); return injectConditionalTags(headTags, homeLocation);
}; }
// strips subpath and protocol // strips subpath and protocol
export const injectConditionalTags = (tagsArray, homeLocation) => { export function injectConditionalTags(
tagsArray: JSX.Element[],
homeLocation: string
): JSX.Element[] {
if (homeLocation.includes('localhost')) return tagsArray; if (homeLocation.includes('localhost')) return tagsArray;
const parsedHomeUrl = psl.parse(new URL(homeLocation).host); const parsedHomeUrl = psl.parse(
new URL(homeLocation).host
) as psl.ParsedDomain;
// inject gap all production languages except Chinese // inject gap all production languages except Chinese
if (parsedHomeUrl.subdomain === 'www' && parsedHomeUrl.tld === 'org') { if (parsedHomeUrl.subdomain === 'www' && parsedHomeUrl.tld === 'org') {
tagsArray.push( tagsArray.push(
<script <script
// @ts-expect-error TODO: check use of href/rel on <script>
href={withPrefix('/misc/gap-org.js')} href={withPrefix('/misc/gap-org.js')}
id='gap-org' id='gap-org'
key='gap-org' key='gap-org'
@ -79,6 +85,7 @@ export const injectConditionalTags = (tagsArray, homeLocation) => {
if (parsedHomeUrl.subdomain === 'www' && parsedHomeUrl.tld === 'dev') { if (parsedHomeUrl.subdomain === 'www' && parsedHomeUrl.tld === 'dev') {
tagsArray.push( tagsArray.push(
<script <script
// @ts-expect-error See above
href={withPrefix('/misc/gap-dev.js')} href={withPrefix('/misc/gap-dev.js')}
id='gap-dev' id='gap-dev'
key='gap-dev' key='gap-dev'
@ -90,13 +97,15 @@ export const injectConditionalTags = (tagsArray, homeLocation) => {
// inject cap and Chinese gap for production Chinese // inject cap and Chinese gap for production Chinese
if (parsedHomeUrl.subdomain === 'chinese' && parsedHomeUrl.tld === 'org') { if (parsedHomeUrl.subdomain === 'chinese' && parsedHomeUrl.tld === 'org') {
tagsArray.push( tagsArray.push(
<scripts <script
// @ts-expect-error See above
href={withPrefix('/misc/cap.js')} href={withPrefix('/misc/cap.js')}
id='cap' id='cap'
key='cap' key='cap'
rel='stylesheet' rel='stylesheet'
/>, />,
<script <script
// @ts-expect-error See above
href={withPrefix('/misc/gap-org-chinese.js')} href={withPrefix('/misc/gap-org-chinese.js')}
id='gap-org-chinese' id='gap-org-chinese'
key='gap-org-chinese' key='gap-org-chinese'
@ -105,10 +114,10 @@ export const injectConditionalTags = (tagsArray, homeLocation) => {
); );
} }
return tagsArray; return tagsArray;
}; }
export const getPostBodyComponents = pathname => { export function getPostBodyComponents(pathname: string): JSX.Element[] {
let scripts = []; const scripts = [];
const mathJaxScriptElement = ( const mathJaxScriptElement = (
<script <script
async={false} async={false}
@ -127,4 +136,4 @@ export const getPostBodyComponents = pathname => {
} }
return scripts.filter(Boolean); return scripts.filter(Boolean);
}; }

View File

@ -4,7 +4,7 @@ const util = require('util');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { findIndex, reduce, toString } = require('lodash'); const { findIndex, reduce, toString } = require('lodash');
const readDirP = require('readdirp'); const readDirP = require('readdirp');
const { helpCategoryMap } = require('../client/utils/challengeTypes'); const { helpCategoryMap } = require('../client/utils/challenge-types');
const { showUpcomingChanges } = require('../config/env.json'); const { showUpcomingChanges } = require('../config/env.json');
const { curriculum: curriculumLangs } = const { curriculum: curriculumLangs } =
require('../config/i18n/all-langs').availableLangs; require('../config/i18n/all-langs').availableLangs;

View File

@ -1,7 +1,7 @@
const Joi = require('joi'); const Joi = require('joi');
Joi.objectId = require('joi-objectid')(Joi); Joi.objectId = require('joi-objectid')(Joi);
const { challengeTypes } = require('../../client/utils/challengeTypes'); const { challengeTypes } = require('../../client/utils/challenge-types');
const slugRE = new RegExp('^[a-z0-9-]+$'); const slugRE = new RegExp('^[a-z0-9-]+$');

View File

@ -33,7 +33,7 @@ const {
const { const {
default: createWorker default: createWorker
} = require('../../client/src/templates/Challenges/utils/worker-executor'); } = require('../../client/src/templates/Challenges/utils/worker-executor');
const { challengeTypes } = require('../../client/utils/challengeTypes'); const { challengeTypes } = require('../../client/utils/challenge-types');
// the config files are created during the build, but not before linting // the config files are created during the build, but not before linting
/* eslint-disable import/no-unresolved */ /* eslint-disable import/no-unresolved */
const testEvaluator = const testEvaluator =

View File

@ -39,7 +39,7 @@ The practice projects have some additional tooling to help create new projects a
--- ---
id: Unique identifier (alphanumerical, MongoDB_id) id: Unique identifier (alphanumerical, MongoDB_id)
title: 'Challenge Title' title: 'Challenge Title'
challengeType: Integer, defined in `client/utils/challengeTypes.js` challengeType: Integer, defined in `client/utils/challenge-types.js`
videoUrl: 'url of video explanation' videoUrl: 'url of video explanation'
forumTopicId: 12345 forumTopicId: 12345
--- ---
@ -488,6 +488,6 @@ Once you have verified that each challenge you've worked on passes the tests, [p
Creating and Editing Challenges: Creating and Editing Challenges:
1. [Challenge types](https://github.com/freeCodeCamp/freeCodeCamp/blob/main/client/utils/challengeTypes.js#L1-L13) - what the numeric challenge type values mean (enum). 1. [Challenge types](https://github.com/freeCodeCamp/freeCodeCamp/blob/main/client/utils/challenge-types.js#L1-L13) - what the numeric challenge type values mean (enum).
2. [Contributing to FreeCodeCamp - Writing ES6 Challenge Tests](https://www.youtube.com/watch?v=iOdD84OSfAE#t=2h49m55s) - a video following [Ethan Arrowood](https://twitter.com/ArrowoodTech) as he contributes to the old version of the curriculum. 2. [Contributing to FreeCodeCamp - Writing ES6 Challenge Tests](https://www.youtube.com/watch?v=iOdD84OSfAE#t=2h49m55s) - a video following [Ethan Arrowood](https://twitter.com/ArrowoodTech) as he contributes to the old version of the curriculum.