mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
181 lines
4.3 KiB
TypeScript
181 lines
4.3 KiB
TypeScript
const exts = ['js', 'html', 'css', 'jsx', 'ts', 'tsx', 'py'] as const;
|
|
export type Ext = (typeof exts)[number];
|
|
|
|
export interface IncompleteChallengeFile {
|
|
fileKey: string;
|
|
ext: Ext;
|
|
name: string;
|
|
contents: string;
|
|
head?: string;
|
|
tail?: string;
|
|
}
|
|
|
|
export interface ChallengeFile extends IncompleteChallengeFile {
|
|
editableRegionBoundaries?: number[];
|
|
editableContents?: string;
|
|
usesMultifileEditor?: boolean;
|
|
error?: unknown;
|
|
head: string;
|
|
tail: string;
|
|
seed?: string;
|
|
source?: string;
|
|
path: 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 {
|
|
if (typeof name !== 'string') throw new TypeError('name must be a string');
|
|
if (typeof ext !== 'string') throw new TypeError('ext must be a string');
|
|
if (typeof contents !== 'string')
|
|
throw new TypeError('contents must be a string');
|
|
|
|
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 {
|
|
function hasProperties(poly: unknown): poly is Record<string, unknown> {
|
|
return (
|
|
!!poly &&
|
|
typeof poly === 'object' &&
|
|
'contents' in poly &&
|
|
'name' in poly &&
|
|
'ext' in poly &&
|
|
'fileKey' in poly &&
|
|
'head' in poly &&
|
|
'tail' in poly &&
|
|
'history' in poly
|
|
);
|
|
}
|
|
|
|
const hasCorrectTypes = (poly: Record<string, unknown>): boolean =>
|
|
typeof poly.contents === 'string' &&
|
|
typeof poly.name === 'string' &&
|
|
exts.includes(poly.ext as Ext) &&
|
|
typeof poly.fileKey === 'string' &&
|
|
typeof poly.head === 'string' &&
|
|
typeof poly.tail === 'string' &&
|
|
Array.isArray(poly.history);
|
|
|
|
return hasProperties(poly) && hasCorrectTypes(poly);
|
|
}
|
|
|
|
function checkPoly(poly: ChallengeFile) {
|
|
if (!isPoly(poly)) throw Error('Not a PolyVinyl: ' + JSON.stringify(poly));
|
|
}
|
|
|
|
// setContent will lose source if not supplied
|
|
export function setContent(
|
|
contents: string,
|
|
poly: ChallengeFile,
|
|
source?: string
|
|
): ChallengeFile {
|
|
checkPoly(poly);
|
|
return {
|
|
...poly,
|
|
contents,
|
|
source
|
|
};
|
|
}
|
|
|
|
// This is currently only used to add back properties that are not stored in the
|
|
// database.
|
|
export function regenerateMissingProperties(file: IncompleteChallengeFile) {
|
|
const newPath = file.name + '.' + file.ext;
|
|
const newFile = {
|
|
...file,
|
|
path: newPath,
|
|
history: [newPath],
|
|
head: file.head ?? '',
|
|
tail: file.tail ?? ''
|
|
};
|
|
return newFile;
|
|
}
|
|
|
|
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, poly.source);
|
|
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<Rest>(
|
|
poly: Pick<ChallengeFile, 'contents' | 'source'> & Rest
|
|
): Rest & { contents: string; source: string } {
|
|
return {
|
|
...poly,
|
|
source: poly.source ?? poly.contents
|
|
};
|
|
}
|