feat(client): use typescript in challenges (#56253)

Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
Oliver Eyton-Williams
2024-10-28 21:40:11 +01:00
committed by GitHub
parent e40a60bc60
commit e9a4e92955
16 changed files with 483 additions and 237 deletions

View File

@@ -18,6 +18,7 @@
},
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
"dependencies": {
"@types/invariant": "^2.2.37",
"invariant": "2.2.4"
}
}

View File

@@ -1,168 +0,0 @@
// originally based off of https://github.com/gulpjs/vinyl
const invariant = require('invariant');
// interface PolyVinyl {
// source: String,
// contents: String,
// name: String,
// ext: String,
// path: String,
// key: String,
// head: String,
// tail: String,
// history: [...String],
// error: Null|Object|Error
// }
// createPoly({
// name: String,
// ext: String,
// contents: String,
// history?: [...String],
// }) => PolyVinyl, throws
function createPoly({ name, ext, contents, history, ...rest }) {
invariant(typeof name === 'string', 'name must be a string but got %s', name);
invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext);
invariant(
typeof contents === 'string',
'contents must be a string but got %s',
contents
);
return {
...rest,
history: Array.isArray(history) ? history : [name + '.' + ext],
name,
ext,
path: name + '.' + ext,
fileKey: name + ext,
contents,
error: null
};
}
// isPoly(poly: Any) => Boolean
function isPoly(poly) {
return (
poly &&
typeof poly.contents === 'string' &&
typeof poly.name === 'string' &&
typeof poly.ext === 'string' &&
Array.isArray(poly.history)
);
}
// checkPoly(poly: Any) => Void, throws
function checkPoly(poly) {
invariant(
isPoly(poly),
'function should receive a PolyVinyl, but got %s',
poly
);
}
// setContent(contents: String, poly: PolyVinyl) => PolyVinyl
// setContent will loose source if set
function setContent(contents, poly) {
checkPoly(poly);
return {
...poly,
contents,
source: null
};
}
// setExt(ext: String, poly: PolyVinyl) => PolyVinyl
function setExt(ext, poly) {
checkPoly(poly);
const newPoly = {
...poly,
ext,
path: poly.name + '.' + ext,
fileKey: poly.name + ext
};
newPoly.history = [...poly.history, newPoly.path];
return newPoly;
}
// This is currently only used to add back properties that are not stored in the
// database.
function regeneratePathAndHistory(poly) {
const newPath = poly.name + '.' + poly.ext;
const newPoly = {
...poly,
path: newPath,
history: [newPath]
};
checkPoly(newPoly);
return newPoly;
}
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
function clearHeadTail(poly) {
checkPoly(poly);
return {
...poly,
head: '',
tail: ''
};
}
// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl
function compileHeadTail(padding = '', poly) {
return clearHeadTail(
transformContents(
() => [poly.head, poly.contents, poly.tail].join(padding),
poly
)
);
}
// transformContents(
// wrap: (contents: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
// transformContents will keep a copy of the original
// code in the `source` property. If the original polyvinyl
// already contains a source, this version will continue as
// the source property
function transformContents(wrap, poly) {
const newPoly = setContent(wrap(poly.contents), poly);
// if no source exist, set the original contents as source
newPoly.source = poly.source || poly.contents;
return newPoly;
}
// transformHeadTailAndContents(
// wrap: (source: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
function transformHeadTailAndContents(wrap, poly) {
return {
...transformContents(wrap, poly),
head: wrap(poly.head),
tail: wrap(poly.tail)
};
}
// createSource(poly: PolyVinyl) => PolyVinyl
function createSource(poly) {
return {
...poly,
source: poly.source || poly.contents
};
}
module.exports = {
createPoly,
isPoly,
setContent,
setExt,
createSource,
compileHeadTail,
regeneratePathAndHistory,
transformContents,
transformHeadTailAndContents
};

View File

@@ -1,4 +1,4 @@
import polyvinyl from './polyvinyl';
import { createPoly, createSource } from './polyvinyl';
const polyData = {
name: 'test',
@@ -9,21 +9,21 @@ const polyData = {
describe('createSource', () => {
it('should return a vinyl object with a source matching the contents', () => {
const original = polyvinyl.createPoly(polyData);
const original = createPoly(polyData);
const updated = polyvinyl.createSource(original);
const updated = createSource(original);
expect(original).not.toHaveProperty('source');
expect(updated).toHaveProperty('source', 'var hello = world;');
expect(updated).toMatchObject(original);
});
it('should not update the source if it already exists', () => {
const original = polyvinyl.createPoly({
const original = createPoly({
...polyData,
source: 'const hello = world;'
});
const updated = polyvinyl.createSource(original);
const updated = createSource(original);
expect(updated).toStrictEqual(original);
});
});

185
shared/utils/polyvinyl.ts Normal file
View File

@@ -0,0 +1,185 @@
// originally based off of https://github.com/gulpjs/vinyl
import invariant from 'invariant';
export type Ext = 'js' | 'html' | 'css' | 'jsx' | 'ts';
export type ChallengeFile = {
fileKey: string;
ext: Ext;
name: string;
editableRegionBoundaries?: number[];
editableContents?: string;
usesMultifileEditor?: boolean;
error?: unknown;
head: string;
tail: string;
seed: string;
contents: string;
source?: string | null;
id: string;
history: string[];
};
type PolyProps = {
name: string;
ext: string;
contents: string;
history?: string[];
};
// The types are a little awkward, but should suffice until we move the
// curriculum to TypeScript.
type AddedProperties = {
path: string;
fileKey: string;
error: null;
};
export function createPoly<Rest>({
name,
ext,
contents,
history,
...rest
}: PolyProps & Rest): PolyProps & AddedProperties & Rest {
invariant(typeof name === 'string', 'name must be a string but got %s', name);
invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext);
invariant(
typeof contents === 'string',
'contents must be a string but got %s',
contents
);
return {
...rest,
history: Array.isArray(history) ? history : [name + '.' + ext],
name,
ext,
path: name + '.' + ext,
fileKey: name + ext,
contents,
error: null
} as PolyProps & AddedProperties & Rest;
}
export function isPoly(poly: unknown): poly is ChallengeFile {
return (
!!poly &&
typeof poly === 'object' &&
'contents' in poly &&
typeof poly.contents === 'string' &&
'name' in poly &&
typeof poly.name === 'string' &&
'ext' in poly &&
typeof poly.ext === 'string' &&
'history' in poly &&
Array.isArray(poly.history)
);
}
function checkPoly(poly: ChallengeFile) {
invariant(
isPoly(poly),
'function should receive a PolyVinyl, but got %s',
poly
);
}
// setContent will lose source if set
export function setContent(
contents: string,
poly: ChallengeFile
): ChallengeFile {
checkPoly(poly);
return {
...poly,
contents,
source: null
};
}
export async function setExt(ext: string, polyP: Promise<ChallengeFile>) {
const poly = await polyP;
checkPoly(poly);
const newPoly = {
...poly,
ext,
path: poly.name + '.' + ext,
fileKey: poly.name + ext
};
newPoly.history = [...poly.history, newPoly.path];
return newPoly;
}
// This is currently only used to add back properties that are not stored in the
// database.
export function regeneratePathAndHistory(poly: ChallengeFile) {
const newPath = poly.name + '.' + poly.ext;
const newPoly = {
...poly,
path: newPath,
history: [newPath]
};
checkPoly(newPoly);
return newPoly;
}
async function clearHeadTail(polyP: Promise<ChallengeFile>) {
const poly = await polyP;
checkPoly(poly);
return {
...poly,
head: '',
tail: ''
};
}
export async function compileHeadTail(padding = '', poly: ChallengeFile) {
return clearHeadTail(
transformContents(
() => [poly.head, poly.contents, poly.tail].join(padding),
poly
)
);
}
type Wrapper = (x: string) => Promise<string> | string;
// transformContents will keep a copy of the original
// code in the `source` property. If the original polyvinyl
// already contains a source, this version will continue as
// the source property
export async function transformContents(
wrap: Wrapper,
polyP: ChallengeFile | Promise<ChallengeFile>
) {
const poly = await polyP;
const newPoly = setContent(await wrap(poly.contents), poly);
// if no source exist, set the original contents as source
newPoly.source = poly.source || poly.contents;
return newPoly;
}
export async function transformHeadTailAndContents(
wrap: Wrapper,
polyP: ChallengeFile | Promise<ChallengeFile>
) {
const poly = await polyP;
const contents = await transformContents(wrap, poly);
const head = await wrap(poly.head);
const tail = await wrap(poly.tail);
return {
...contents,
head,
tail
};
}
// createSource(poly: PolyVinyl) => PolyVinyl
export function createSource(poly: Pick<ChallengeFile, 'contents' | 'source'>) {
return {
...poly,
source: poly.source || poly.contents
};
}