diff --git a/client/gatsby-node.js b/client/gatsby-node.js index d1adc1805e..173c7e2a73 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -244,17 +244,11 @@ exports.createSchemaCustomization = ({ actions }) => { const { createTypes } = actions; const typeDefs = ` type ChallengeNode implements Node { - files: ChallengeFile + challengeFiles: [FileContents] url: String } - type ChallengeFile { - indexcss: FileContents - indexhtml: FileContents - indexjs: FileContents - indexjsx: FileContents - } type FileContents { - key: String + fileKey: String ext: String name: String contents: String diff --git a/client/src/client-only-routes/show-project-links.tsx b/client/src/client-only-routes/show-project-links.tsx index 29ca8ffdef..28a370ffb4 100644 --- a/client/src/client-only-routes/show-project-links.tsx +++ b/client/src/client-only-routes/show-project-links.tsx @@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next'; import ProjectModal from '../components/SolutionViewer/ProjectModal'; import { Spacer, Link } from '../components/helpers'; import { - ChallengeFileType, + ChallengeFiles, CompletedChallenge, UserType } from '../redux/prop-types'; @@ -24,14 +24,14 @@ interface IShowProjectLinksProps { type SolutionStateType = { projectTitle: string; - files?: ChallengeFileType[] | null; + challengeFiles: ChallengeFiles; solution: CompletedChallenge['solution']; isOpen: boolean; }; const initSolutionState: SolutionStateType = { projectTitle: '', - files: null, + challengeFiles: null, solution: null, isOpen: false }; @@ -56,16 +56,16 @@ const ShowProjectLinks = (props: IShowProjectLinksProps): JSX.Element => { return null; } - const { solution, githubLink, files } = completedProject; + const { solution, githubLink, challengeFiles } = completedProject; const onClickHandler = () => setSolutionState({ projectTitle, - files, + challengeFiles, solution, isOpen: true }); - if (files?.length) { + if (challengeFiles?.length) { return ( diff --git a/client/src/components/SolutionViewer/SolutionViewer.js b/client/src/components/SolutionViewer/SolutionViewer.js index c380cde3c6..34ab2db6af 100644 --- a/client/src/components/SolutionViewer/SolutionViewer.js +++ b/client/src/components/SolutionViewer/SolutionViewer.js @@ -11,21 +11,25 @@ const prismLang = { }; const SolutionViewer = ({ - files, + challengeFiles, solution = '// The solution is not available for this project' }) => - files && Array.isArray(files) && files.length ? ( - files.map(file => ( - - {file.ext.toUpperCase()} + challengeFiles?.length ? ( + challengeFiles.map(challengeFile => ( + + {challengeFile.ext.toUpperCase()}
             
@@ -59,7 +63,7 @@ const SolutionViewer = ({
 
 SolutionViewer.displayName = 'SolutionViewer';
 SolutionViewer.propTypes = {
-  files: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)),
+  challengeFiles: PropTypes.array,
   solution: PropTypes.string
 };
 
diff --git a/client/src/components/profile/components/TimeLine.test.tsx b/client/src/components/profile/components/TimeLine.test.tsx
index 21c9a1782a..833f5bc735 100644
--- a/client/src/components/profile/components/TimeLine.test.tsx
+++ b/client/src/components/profile/components/TimeLine.test.tsx
@@ -80,7 +80,7 @@ describe('', () => {
 
 const contents = 'This is not JS';
 const ext = 'js';
-const key = 'indexjs';
+const fileKey = 'indexjs';
 const name = 'index';
 const path = 'index.js';
 
@@ -100,15 +100,7 @@ const propsForOnlySolution = {
     {
       id: '5e46f7f8ac417301a38fb92a',
       completedDate: 1604043678032,
-      files: [
-        {
-          contents,
-          ext,
-          key,
-          name,
-          path
-        }
-      ]
+      challengeFiles: [{ contents, ext, fileKey, name, path }]
     }
   ],
   username: 'developmentuser'
diff --git a/client/src/components/profile/components/TimeLine.tsx b/client/src/components/profile/components/TimeLine.tsx
index 0920781569..64f4364b0f 100644
--- a/client/src/components/profile/components/TimeLine.tsx
+++ b/client/src/components/profile/components/TimeLine.tsx
@@ -1,8 +1,3 @@
-/* eslint-disable @typescript-eslint/ban-ts-comment */
-/* eslint-disable @typescript-eslint/no-unsafe-call */
-/* eslint-disable @typescript-eslint/no-unsafe-member-access */
-/* eslint-disable @typescript-eslint/no-unsafe-assignment */
-/* eslint-disable @typescript-eslint/restrict-template-expressions */
 /* eslint-disable @typescript-eslint/unbound-method */
 import {
   Button,
@@ -25,6 +20,7 @@ import {
   getTitleFromId
 } from '../../../../../utils';
 import CertificationIcon from '../../../assets/icons/certification-icon';
+import { ChallengeFiles } from '../../../redux/prop-types';
 import { maybeUrlRE } from '../../../utils';
 import { FullWidthRow, Link } from '../../helpers';
 import TimelinePagination from './TimelinePagination';
@@ -32,70 +28,54 @@ import TimelinePagination from './TimelinePagination';
 import './timeline.css';
 
 const SolutionViewer = Loadable(
-  () =>
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    import('../../SolutionViewer/SolutionViewer')
+  () => import('../../SolutionViewer/SolutionViewer')
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-const { clientLocale } = envData;
-
-// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/ban-ts-comment
-// @ts-ignore
-// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
+const { clientLocale } = envData as { clientLocale: keyof typeof langCodes };
 const localeCode = langCodes[clientLocale];
 
 // Items per page in timeline.
 const ITEMS_PER_PAGE = 15;
 
-interface ICompletedMap {
+interface CompletedMap {
   id: string;
   completedDate: number;
   challengeType: number;
   solution: string;
-  files: IFile[];
+  challengeFiles: ChallengeFiles;
   githubLink: string;
 }
 
-interface ITimelineProps {
-  completedMap: ICompletedMap[];
+interface TimelineProps {
+  completedMap: CompletedMap[];
   t: TFunction;
   username: string;
 }
 
-interface IFile {
-  ext: string;
-  contents: string;
-}
-
-interface ISortedTimeline {
+interface SortedTimeline {
   id: string;
   completedDate: number;
-  files: IFile[];
+  challengeFiles: ChallengeFiles;
   githubLink: string;
   solution: string;
 }
 
-interface ITimelineInnerProps extends ITimelineProps {
+interface TimelineInnerProps extends TimelineProps {
   idToNameMap: Map;
-  sortedTimeline: ISortedTimeline[];
+  sortedTimeline: SortedTimeline[];
   totalPages: number;
 }
 
-interface ITimeLineInnerState {
+interface TimeLineInnerState {
   solutionToView: string | null;
   solutionOpen: boolean;
   pageNo: number;
   solution: string | null;
-  files: IFile[] | null;
+  challengeFiles: ChallengeFiles;
 }
 
-class TimelineInner extends Component<
-  ITimelineInnerProps,
-  ITimeLineInnerState
-> {
-  constructor(props: ITimelineInnerProps) {
+class TimelineInner extends Component {
+  constructor(props: TimelineInnerProps) {
     super(props);
 
     this.state = {
@@ -103,7 +83,7 @@ class TimelineInner extends Component<
       solutionOpen: false,
       pageNo: 1,
       solution: null,
-      files: null
+      challengeFiles: null
     };
 
     this.closeSolution = this.closeSolution.bind(this);
@@ -118,19 +98,19 @@ class TimelineInner extends Component<
 
   renderViewButton(
     id: string,
-    files: IFile[],
+    challengeFiles: ChallengeFiles,
     githubLink: string,
     solution: string
   ): React.ReactNode {
     const { t } = this.props;
-    if (files && files.length) {
+    if (challengeFiles?.length) {
       return (
         
@@ -183,12 +163,11 @@ class TimelineInner extends Component<
     }
   }
 
-  renderCompletion(completed: ISortedTimeline): JSX.Element {
+  renderCompletion(completed: SortedTimeline): JSX.Element {
     const { idToNameMap, username } = this.props;
-    const { id, files, githubLink, solution } = completed;
+    const { id, challengeFiles, githubLink, solution } = completed;
     const completedDate = new Date(completed.completedDate);
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
+    // @ts-expect-error idToNameMap is not a  Map...
     const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
     return (
       
@@ -196,16 +175,18 @@ class TimelineInner extends Component<
           {certPath ? (
             
               {challengeTitle}
               
             
           ) : (
-            {challengeTitle}
+            {challengeTitle}
           )}
         
-        {this.renderViewButton(id, files, githubLink, solution)}
+        
+          {this.renderViewButton(id, challengeFiles, githubLink, solution)}
+        
         
           
", }, "solutions": Array [ - Object {}, + Array [], ], "tests": Array [], } @@ -52,38 +52,33 @@ Object { exports[`challenge parser should import md from other files 1`] = ` Object { - "description": "
-

Paragraph 1

-
code example
-
-
", - "files": Object { - "indexcss": Object { - "contents": "body { - background: green; -}", - "editableRegionBoundaries": Array [], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + "challengeFiles": Array [ + Object { "contents": " ", "editableRegionBoundaries": Array [], "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: green; +}", + "editableRegionBoundaries": Array [], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y'; for (let index = 0; index < array.length; index++) { const element = array[index]; @@ -91,20 +86,25 @@ for (let index = 0; index < array.length; index++) { }", "editableRegionBoundaries": Array [], "ext": "js", + "fileKey": "indexjs", "head": "", "id": "custom-name", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], + "description": "
+

Paragraph 1

+
code example
+
+
", "instructions": "

Paragraph 0

code example 0
 
", "solutions": Array [ - Object {}, + Array [], ], "tests": Array [ Object { @@ -121,6 +121,43 @@ for (let index = 0; index < array.length; index++) { exports[`challenge parser should not mix other YAML with the frontmatter 1`] = ` Object { + "challengeFiles": Array [ + Object { + "contents": " + + +", + "editableRegionBoundaries": Array [], + "ext": "html", + "fileKey": "indexhtml", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { + "contents": "body { + background: green; +}", + "editableRegionBoundaries": Array [], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { + "contents": "var x = 'y';", + "editableRegionBoundaries": Array [], + "ext": "js", + "fileKey": "indexjs", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + ], "description": "

Paragraph 1

code example
@@ -130,50 +167,13 @@ Object {
     anothersubkey: another value
 
", - "files": Object { - "indexcss": Object { - "contents": "body { - background: green; -}", - "editableRegionBoundaries": Array [], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { - "contents": " - - -", - "editableRegionBoundaries": Array [], - "ext": "html", - "head": "", - "id": "", - "key": "indexhtml", - "name": "index", - "tail": "", - }, - "indexjs": Object { - "contents": "var x = 'y';", - "editableRegionBoundaries": Array [], - "ext": "js", - "head": "", - "id": "", - "key": "indexjs", - "name": "index", - "tail": "", - }, - }, "instructions": "

Paragraph 0

code example 0
 
", "solutions": Array [ - Object {}, + Array [], ], "tests": Array [ Object { @@ -190,42 +190,8 @@ Object { exports[`challenge parser should parse a more realistic md file 1`] = ` Object { - "description": "
-

When you add a lower rank heading element to the page, it's implied that you're starting a new subsection.

-

After the last h2 element of the second section element, add an h3 element with the text Things cats love:.

-
-

Some text in a blockquote

-

- Some text in a blockquote, with code -

-
-
", - "files": Object { - "indexcss": Object { - "contents": "body { - background: white; -} - -h1 { - font-size: 20px; -} - - -a { - color: green; -}", - "editableRegionBoundaries": Array [ - 7, - 9, - ], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + "challengeFiles": Array [ + Object { "contents": "

CatPhotoApp

@@ -256,32 +222,14 @@ a { 23, ], "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "html-key", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { - "contents": "var x = 'y';", - "editableRegionBoundaries": Array [], - "ext": "js", - "head": " // this runs before the user's code is evaluated.", - "id": "final-key", - "key": "indexjs", - "name": "index", - "tail": "", - }, - }, - "instructions": "
-

Do something with the code.

-

To test that adjacent tags are handled correctly:

-

a bit of code with more after a space and another pair of elements with a space

-
", - "solutions": Array [ Object { - "indexcss": Object { - "contents": "body { + "contents": "body { background: white; } @@ -293,14 +241,46 @@ h1 { a { color: green; }", - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + "editableRegionBoundaries": Array [ + 7, + 9, + ], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { + "contents": "var x = 'y';", + "editableRegionBoundaries": Array [], + "ext": "js", + "fileKey": "indexjs", + "head": " // this runs before the user's code is evaluated.", + "id": "final-key", + "name": "index", + "tail": "", + }, + ], + "description": "
+

When you add a lower rank heading element to the page, it's implied that you're starting a new subsection.

+

After the last h2 element of the second section element, add an h3 element with the text Things cats love:.

+
+

Some text in a blockquote

+

+ Some text in a blockquote, with code +

+
+
", + "instructions": "
+

Do something with the code.

+

To test that adjacent tags are handled correctly:

+

a bit of code with more after a space and another pair of elements with a space

+
", + "solutions": Array [ + Array [ + Object { "contents": "

CatPhotoApp

@@ -327,22 +307,42 @@ a { ", "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "html-key", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: white; +} + +h1 { + font-size: 20px; +} + + +a { + color: green; +}", + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y';", "ext": "js", + "fileKey": "indexjs", "head": "", "id": "final-key", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], ], "tests": Array [ Object { @@ -385,89 +385,89 @@ assert( exports[`challenge parser should parse a simple md file 1`] = ` Object { - "description": "
-

Paragraph 1

-
code example
-
-
", - "files": Object { - "indexcss": Object { - "contents": "body { - background: green; -}", - "editableRegionBoundaries": Array [], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + "challengeFiles": Array [ + Object { "contents": " ", "editableRegionBoundaries": Array [], "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: green; +}", + "editableRegionBoundaries": Array [], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y';", "editableRegionBoundaries": Array [], "ext": "js", + "fileKey": "indexjs", "head": "", "id": "", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], + "description": "
+

Paragraph 1

+
code example
+
+
", "instructions": "

Paragraph 0

code example 0
 
", "solutions": Array [ - Object { - "indexcss": Object { - "contents": "body { - background: white; -}", - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + Array [ + Object { "contents": " ", "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "html-key", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: white; +}", + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y'; \`\`", "ext": "js", + "fileKey": "indexjs", "head": "", "id": "", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], ], "tests": Array [ Object { @@ -491,54 +491,54 @@ if(let x of xs) { exports[`challenge parser should parse frontmatter 1`] = ` Object { - "challengeType": 0, - "description": "
-

Paragraph 1

-
code example
-
-
", - "files": Object { - "indexcss": Object { - "contents": "body { - background: green; -}", - "editableRegionBoundaries": Array [], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + "challengeFiles": Array [ + Object { "contents": " ", "editableRegionBoundaries": Array [], "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: green; +}", + "editableRegionBoundaries": Array [], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y';", "editableRegionBoundaries": Array [], "ext": "js", + "fileKey": "indexjs", "head": "", "id": "", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], + "challengeType": 0, + "description": "
+

Paragraph 1

+
code example
+
+
", "forumTopicId": 18276, "id": "bd7123c8c441eddfaeb5bdef", "isHidden": false, "solutions": Array [ - Object {}, + Array [], ], "tests": Array [ Object { @@ -557,6 +557,43 @@ Object { exports[`challenge parser should parse gfm strikethrough and frontmatter 1`] = ` Object { + "challengeFiles": Array [ + Object { + "contents": " + + +", + "editableRegionBoundaries": Array [], + "ext": "html", + "fileKey": "indexhtml", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { + "contents": "body { + background: green; +}", + "editableRegionBoundaries": Array [], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { + "contents": "var x = 'y';", + "editableRegionBoundaries": Array [], + "ext": "js", + "fileKey": "indexjs", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + ], "description": "

Paragraph 1 Strikethrough text. https://should.not.be.autolinked

code example
@@ -576,84 +613,47 @@ Object {
 
 
 
", - "files": Object { - "indexcss": Object { - "contents": "body { - background: green; -}", - "editableRegionBoundaries": Array [], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { - "contents": " - - -", - "editableRegionBoundaries": Array [], - "ext": "html", - "head": "", - "id": "", - "key": "indexhtml", - "name": "index", - "tail": "", - }, - "indexjs": Object { - "contents": "var x = 'y';", - "editableRegionBoundaries": Array [], - "ext": "js", - "head": "", - "id": "", - "key": "indexjs", - "name": "index", - "tail": "", - }, - }, "instructions": "

Paragraph 0

code example 0
 
", "solutions": Array [ - Object { - "indexcss": Object { - "contents": "body { - background: white; -}", - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + Array [ + Object { "contents": " ", "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "html-key", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: white; +}", + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y'; \`\`", "ext": "js", + "fileKey": "indexjs", "head": "", "id": "", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], ], "tests": Array [ Object { diff --git a/tools/challenge-parser/parser/plugins/__snapshots__/add-seed.test.js.snap b/tools/challenge-parser/parser/plugins/__snapshots__/add-seed.test.js.snap index 9ee0817a0b..c62776dd8e 100644 --- a/tools/challenge-parser/parser/plugins/__snapshots__/add-seed.test.js.snap +++ b/tools/challenge-parser/parser/plugins/__snapshots__/add-seed.test.js.snap @@ -2,42 +2,42 @@ exports[`add-seed plugin should have an output to match the snapshot 1`] = ` Object { - "files": Object { - "indexcss": Object { - "contents": "body { - background: green; -}", - "editableRegionBoundaries": Array [], - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + "challengeFiles": Array [ + Object { "contents": " ", "editableRegionBoundaries": Array [], "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: green; +}", + "editableRegionBoundaries": Array [], + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y';", "editableRegionBoundaries": Array [], "ext": "js", + "fileKey": "indexjs", "head": "", "id": "", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], } `; diff --git a/tools/challenge-parser/parser/plugins/__snapshots__/add-solution.test.js.snap b/tools/challenge-parser/parser/plugins/__snapshots__/add-solution.test.js.snap index cc2a4f00ff..7df32888d3 100644 --- a/tools/challenge-parser/parser/plugins/__snapshots__/add-solution.test.js.snap +++ b/tools/challenge-parser/parser/plugins/__snapshots__/add-solution.test.js.snap @@ -3,41 +3,41 @@ exports[`add solution plugin should have an output to match the snapshot 1`] = ` Object { "solutions": Array [ - Object { - "indexcss": Object { - "contents": "body { - background: white; -}", - "ext": "css", - "head": "", - "id": "", - "key": "indexcss", - "name": "index", - "tail": "", - }, - "indexhtml": Object { + Array [ + Object { "contents": " ", "ext": "html", + "fileKey": "indexhtml", "head": "", "id": "html-key", - "key": "indexhtml", "name": "index", "tail": "", }, - "indexjs": Object { + Object { + "contents": "body { + background: white; +}", + "ext": "css", + "fileKey": "indexcss", + "head": "", + "id": "", + "name": "index", + "tail": "", + }, + Object { "contents": "var x = 'y'; \`\`", "ext": "js", + "fileKey": "indexjs", "head": "", "id": "", - "key": "indexjs", "name": "index", "tail": "", }, - }, + ], ], } `; diff --git a/tools/challenge-parser/parser/plugins/add-seed.js b/tools/challenge-parser/parser/plugins/add-seed.js index e4f8900083..da016291d9 100644 --- a/tools/challenge-parser/parser/plugins/add-seed.js +++ b/tools/challenge-parser/parser/plugins/add-seed.js @@ -7,8 +7,8 @@ const { getFileVisitor } = require('./utils/get-file-visitor'); const editableRegionMarker = '--fcc-editable-region--'; -function findRegionMarkers(file) { - const lines = file.contents.split('\n'); +function findRegionMarkers(challengeFile) { + const lines = challengeFile.contents.split('\n'); const editableLines = lines .map((line, id) => (line.trim() === editableRegionMarker ? id : -1)) .filter(id => id >= 0); @@ -55,26 +55,33 @@ function addSeeds() { visitForContents(contentsTree); visitForHead(headTree); visitForTail(tailTree); + const seedVals = Object.values(seeds); file.data = { ...file.data, - files: seeds + challengeFiles: seedVals }; // process region markers - remove them from content and add them to data - Object.keys(seeds).forEach(key => { - const fileData = seeds[key]; - const editRegionMarkers = findRegionMarkers(fileData); + const challengeFiles = Object.values(seeds).map(data => { + const seed = { ...data }; + const editRegionMarkers = findRegionMarkers(seed); if (editRegionMarkers) { - fileData.contents = removeLines(fileData.contents, editRegionMarkers); + seed.contents = removeLines(seed.contents, editRegionMarkers); if (editRegionMarkers[1] <= editRegionMarkers[0]) { throw Error('Editable region must be non zero'); } - fileData.editableRegionBoundaries = editRegionMarkers; + seed.editableRegionBoundaries = editRegionMarkers; } else { - fileData.editableRegionBoundaries = []; + seed.editableRegionBoundaries = []; } + return seed; }); + + file.data = { + ...file.data, + challengeFiles + }; } return transformer; diff --git a/tools/challenge-parser/parser/plugins/add-seed.test.js b/tools/challenge-parser/parser/plugins/add-seed.test.js index a448ef8679..7c91a69479 100644 --- a/tools/challenge-parser/parser/plugins/add-seed.test.js +++ b/tools/challenge-parser/parser/plugins/add-seed.test.js @@ -1,4 +1,3 @@ -const { isObject } = require('lodash'); const isArray = require('lodash/isArray'); const adjacentKeysAST = require('../__fixtures__/ast-adjacent-keys.json'); @@ -32,26 +31,26 @@ describe('add-seed plugin', () => { expect(typeof plugin).toEqual('function'); }); - it('adds a `files` property to `file.data`', () => { + it('adds a `challengeFiles` property to `file.data`', () => { plugin(simpleAST, file); - expect('files' in file.data).toBe(true); + expect('challengeFiles' in file.data).toBe(true); }); - it('ensures that the `files` property is an object', () => { + it('ensures that the `challengeFiles` property is an array', () => { plugin(simpleAST, file); - expect(isObject(file.data.files)).toBe(true); + expect(isArray(file.data.challengeFiles)).toBe(true); }); - it('adds test objects to the files array following a schema', () => { + it('adds test objects to the challengeFiles array following a schema', () => { expect.assertions(17); plugin(simpleAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const testObject = files.indexjs; + const testObject = challengeFiles.find(x => x.fileKey === 'indexjs'); expect(Object.keys(testObject).length).toEqual(8); - expect(testObject).toHaveProperty('key'); - expect(typeof testObject['key']).toBe('string'); + expect(testObject).toHaveProperty('fileKey'); + expect(typeof testObject['fileKey']).toBe('string'); expect(testObject).toHaveProperty('ext'); expect(typeof testObject['ext']).toBe('string'); expect(testObject).toHaveProperty('name'); @@ -69,33 +68,32 @@ describe('add-seed plugin', () => { }); it('parses seeds without ids', () => { - expect.assertions(6); + expect.assertions(3); plugin(simpleAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexjs, indexhtml, indexcss } = files; + const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs'); + const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml'); + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexjs.contents).toBe(`var x = 'y';`); - expect(indexjs.key).toBe(`indexjs`); expect(indexhtml.contents).toBe(` `); - expect(indexhtml.key).toBe(`indexhtml`); expect(indexcss.contents).toBe(`body { background: green; }`); - expect(indexcss.key).toBe(`indexcss`); }); it('removes region markers from contents', () => { expect.assertions(2); plugin(withEditableAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexcss } = files; + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexcss.contents).not.toMatch('--fcc-editable-region--'); expect(indexcss.editableRegionBoundaries).toEqual([1, 4]); @@ -107,9 +105,11 @@ describe('add-seed plugin', () => { expect.assertions(3); plugin(withSeedKeysAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexhtml, indexcss, indexjs } = files; + const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs'); + const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml'); + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexhtml.id).toBe(''); expect(indexcss.id).toBe('key-for-css'); @@ -138,9 +138,11 @@ describe('add-seed plugin', () => { expect.assertions(3); plugin(withBeforeAfterAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexjs, indexhtml, indexcss } = files; + const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs'); + const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml'); + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexjs.head).toBe(''); expect(indexhtml.head).toBe(``); @@ -153,9 +155,11 @@ describe('add-seed plugin', () => { expect.assertions(3); plugin(withBeforeAfterAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexjs, indexhtml, indexcss } = files; + const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs'); + const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml'); + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexjs.tail).toBe(`function teardown(params) { // after @@ -188,9 +192,11 @@ describe('add-seed plugin', () => { expect.assertions(6); plugin(emptyBeforeAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexjs, indexhtml, indexcss } = files; + const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs'); + const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml'); + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexjs.head).toBe(''); expect(indexjs.tail).toBe('function teardown(params) {\n // after\n}'); @@ -204,9 +210,11 @@ describe('add-seed plugin', () => { expect.assertions(6); plugin(emptyAfterAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexjs, indexhtml, indexcss } = files; + const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs'); + const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml'); + const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss'); expect(indexjs.head).toBe(''); expect(indexjs.tail).toBe(''); @@ -234,9 +242,9 @@ describe('add-seed plugin', () => { expect.assertions(4); plugin(jsxSeedAST, file); const { - data: { files } + data: { challengeFiles } } = file; - const { indexjsx } = files; + const indexjsx = challengeFiles.find(x => x.fileKey === 'indexjsx'); expect(indexjsx.head).toBe(`function setup() {}`); expect(indexjsx.tail).toBe(`function teardown(params) { @@ -248,7 +256,7 @@ describe('add-seed plugin', () => { const Button = () => { return ; };`); - expect(indexjsx.key).toBe(`indexjsx`); + expect(indexjsx.fileKey).toBe(`indexjsx`); }); it('combines all the code of a specific language into a single file', () => { diff --git a/tools/challenge-parser/parser/plugins/add-solution.js b/tools/challenge-parser/parser/plugins/add-solution.js index 6a373f3a67..af08df35e0 100644 --- a/tools/challenge-parser/parser/plugins/add-solution.js +++ b/tools/challenge-parser/parser/plugins/add-solution.js @@ -30,7 +30,7 @@ function createPlugin() { ); visitForContents(solutionTree); - solutions.push(solution); + solutions.push(Object.values(solution)); }); file.data = { diff --git a/tools/challenge-parser/parser/plugins/add-solution.test.js b/tools/challenge-parser/parser/plugins/add-solution.test.js index 281b313991..522deb2386 100644 --- a/tools/challenge-parser/parser/plugins/add-solution.test.js +++ b/tools/challenge-parser/parser/plugins/add-solution.test.js @@ -33,16 +33,18 @@ describe('add solution plugin', () => { expect(file.data.solutions.every(el => isObject(el))).toBe(true); }); - it('adds solution objects to the files array following a schema', () => { + it('adds solution objects to the challengeFiles array following a schema', () => { expect.assertions(15); plugin(mockAST, file); const { data: { solutions } } = file; - const testObject = solutions[0].indexjs; + const testObject = solutions[0].find( + solution => solution.fileKey === 'indexjs' + ); expect(Object.keys(testObject).length).toEqual(7); - expect(testObject).toHaveProperty('key'); - expect(typeof testObject['key']).toBe('string'); + expect(testObject).toHaveProperty('fileKey'); + expect(typeof testObject['fileKey']).toBe('string'); expect(testObject).toHaveProperty('ext'); expect(typeof testObject['ext']).toBe('string'); expect(testObject).toHaveProperty('name'); @@ -64,16 +66,24 @@ describe('add solution plugin', () => { data: { solutions } } = file; expect(solutions.length).toBe(3); - expect(solutions[0].indexjs.contents).toBe("var x = 'y';"); - expect(solutions[1].indexhtml.contents).toBe(` + expect( + solutions[0].find(solution => solution.fileKey === 'indexjs').contents + ).toBe("var x = 'y';"); + expect( + solutions[1].find(solution => solution.fileKey === 'indexhtml').contents + ).toBe(` solution number two `); - expect(solutions[1].indexcss.contents).toBe(`body { + expect( + solutions[1].find(solution => solution.fileKey === 'indexcss').contents + ).toBe(`body { background: white; }`); - expect(solutions[2].indexjs.contents).toBe("var x = 'y3';"); + expect( + solutions[2].find(solution => solution.fileKey === 'indexjs').contents + ).toBe("var x = 'y3';"); }); it('should reject solutions with editable region markers', () => { diff --git a/tools/challenge-parser/parser/plugins/utils/get-file-visitor.js b/tools/challenge-parser/parser/plugins/utils/get-file-visitor.js index 4f263a55e2..920d41d15c 100644 --- a/tools/challenge-parser/parser/plugins/utils/get-file-visitor.js +++ b/tools/challenge-parser/parser/plugins/utils/get-file-visitor.js @@ -12,7 +12,7 @@ const supportedLanguages = ['js', 'css', 'html', 'jsx', 'py']; function defaultFile(lang, id) { return { - key: `index${lang}`, + fileKey: `index${lang}`, ext: lang, name: 'index', contents: '', @@ -43,21 +43,21 @@ function codeToData(node, seeds, seedKey, validate) { Please use one of js, css, html, jsx or py `); - const key = `index${lang}`; - const id = seeds[key] ? seeds[key].id : ''; + const fileKey = `index${lang}`; + const id = seeds[fileKey] ? seeds[fileKey].id : ''; // the contents will be missing if there is an id preceding this code // block. - if (!seeds[key]) { - seeds[key] = defaultFile(lang, id); + if (!seeds[fileKey]) { + seeds[fileKey] = defaultFile(lang, id); } if (isEmpty(node.value) && seedKey !== 'contents') { const section = keyToSection[seedKey]; throw Error(`Empty code block in --${section}-- section`); } - seeds[key][seedKey] = isEmpty(seeds[key][seedKey]) + seeds[fileKey][seedKey] = isEmpty(seeds[fileKey][seedKey]) ? node.value - : seeds[key][seedKey] + '\n' + node.value; + : seeds[fileKey][seedKey] + '\n' + node.value; } function idToData(node, index, parent, seeds) { @@ -73,9 +73,9 @@ function idToData(node, index, parent, seeds) { } const codeNode = parent.children[index + 1]; if (codeNode && is(codeNode, 'code')) { - const key = `index${codeNode.lang}`; - if (seeds[key]) throw Error('::id{#id}s must come before code blocks'); - seeds[key] = defaultFile(codeNode.lang, id); + const fileKey = `index${codeNode.lang}`; + if (seeds[fileKey]) throw Error('::id{#id}s must come before code blocks'); + seeds[fileKey] = defaultFile(codeNode.lang, id); } else { throw Error('::id{#id}s must come before code blocks'); } diff --git a/tools/challenge-parser/translation-parser/__fixtures__/challenge-objects.js b/tools/challenge-parser/translation-parser/__fixtures__/challenge-objects.js index a17e7b4f49..81fecf6fab 100644 --- a/tools/challenge-parser/translation-parser/__fixtures__/challenge-objects.js +++ b/tools/challenge-parser/translation-parser/__fixtures__/challenge-objects.js @@ -17,7 +17,7 @@ const ENGLISH_CHALLENGE_NO_FILES = { solutions: ['solution html string'], description: 'description html string', instructions: 'instructions html string', - files: [] + challengeFiles: [] }; exports.ENGLISH_CHALLENGE_NO_FILES = ENGLISH_CHALLENGE_NO_FILES; diff --git a/tools/challenge-parser/translation-parser/index.js b/tools/challenge-parser/translation-parser/index.js index 4b7c3ee459..4283cfd205 100644 --- a/tools/challenge-parser/translation-parser/index.js +++ b/tools/challenge-parser/translation-parser/index.js @@ -17,19 +17,19 @@ exports.translateComments = (text, lang, dict, codeLang) => { exports.translateCommentsInChallenge = (challenge, lang, dict) => { const challClone = cloneDeep(challenge); - if (!challClone.files) { + if (!challClone.challengeFiles) { console.warn(`Challenge ${challClone.title} has no seed to translate`); } else { - Object.keys(challClone.files).forEach(key => { - if (challClone.files[key].contents) { + challClone.challengeFiles.forEach(challengeFile => { + if (challengeFile.contents) { let { text, commentCounts } = this.translateComments( - challenge.files[key].contents, + challengeFile.contents, lang, dict, - challClone.files[key].ext + challengeFile.ext ); challClone.__commentCounts = commentCounts; - challClone.files[key].contents = text; + challengeFile.contents = text; } }); } diff --git a/utils/__fixtures__/challenges.js b/utils/__fixtures__/challenges.js index df9d0b54b6..e4dab4e37a 100644 --- a/utils/__fixtures__/challenges.js +++ b/utils/__fixtures__/challenges.js @@ -1,50 +1,50 @@ -exports.challengeFiles = { - indexcss: { +exports.challengeFiles = [ + { contents: 'some css', error: null, ext: 'css', head: '', history: ['index.css'], - key: 'indexcss', + fileKey: 'indexcss', name: 'index', path: 'index.css', seed: 'some css', tail: '' }, - indexhtml: { + { contents: 'some html', error: null, ext: 'html', head: '', history: ['index.html'], - key: 'indexhtml', + fileKey: 'indexhtml', name: 'index', path: 'index.html', seed: 'some html', tail: '' }, - indexjs: { + { contents: 'some js', error: null, ext: 'js', head: '', history: ['index.js'], - key: 'indexjs', + fileKey: 'indexjs', name: 'index', path: 'index.js', seed: 'some js', tail: '' }, - indexjsx: { + { contents: 'some jsx', error: null, ext: 'jsx', head: '', history: ['index.jsx'], - key: 'indexjsx', + fileKey: 'indexjsx', name: 'index', path: 'index.jsx', seed: 'some jsx', tail: '' } -}; +]; diff --git a/utils/polyvinyl.js b/utils/polyvinyl.js index 53f973056d..b22f921ee9 100644 --- a/utils/polyvinyl.js +++ b/utils/polyvinyl.js @@ -37,7 +37,7 @@ function createPoly({ name, ext, contents, history, ...rest } = {}) { name, ext, path: name + '.' + ext, - key: name + ext, + fileKey: name + ext, contents, error: null }; @@ -81,7 +81,7 @@ function setExt(ext, poly) { ...poly, ext, path: poly.name + '.' + ext, - key: poly.name + ext + fileKey: poly.name + ext }; newPoly.history = [...poly.history, newPoly.path]; return newPoly; diff --git a/utils/sort-files.js b/utils/sort-files.js index 443cab4481..433a1a8e4d 100644 --- a/utils/sort-files.js +++ b/utils/sort-files.js @@ -1,5 +1,5 @@ exports.toSortedArray = function toSortedArray(challengeFiles) { - const xs = Object.values(challengeFiles); + const xs = challengeFiles; // TODO: refactor this to use an ext array ['html', 'js', 'css'] and loop over // that. xs.sort((a, b) => { diff --git a/utils/sort-files.test.js b/utils/sort-files.test.js index 234aff6979..bb17949c7f 100644 --- a/utils/sort-files.test.js +++ b/utils/sort-files.test.js @@ -9,14 +9,14 @@ describe('sort-files', () => { }); it('should not modify the challenges', () => { const sorted = toSortedArray(challengeFiles); - const expected = Object.values(challengeFiles); + const expected = challengeFiles; expect(sorted).toEqual(expect.arrayContaining(expected)); expect(sorted.length).toEqual(expected.length); }); it('should sort the objects into html, js, css order', () => { - const sorted = toSortedArray(challengeFiles); - const sortedKeys = sorted.map(({ key }) => key); + const sorted = challengeFiles; + const sortedKeys = sorted.map(({ fileKey }) => fileKey); const expected = ['indexhtml', 'indexjsx', 'indexjs', 'indexcss']; expect(sortedKeys).toStrictEqual(expected); });