refactor: organise TypeScript config and migrate helpers (#44747)
* feat: allow more 1000 steps to be created at once * refactor: start migrating to typescript * refactor: delete-step to ts * refactor: migrated some helpers * refactor: migrate create-empty-steps * refactor: migrate create-step-between * refactor: finish migrating to TS * refactor: migrate tests * fix: ensure mock.restore is done after each test * fix: prevent double-tscing * fix: repair the tests * chore: use ts-node for scripts We don't need the performance boost of incremental compilation and ts-node is easier to work with * refactor: consolidate tsconfigs * refactor: replace gulp * fix: use ts-node for build-curriculum * fix: allow ts compilation of config * feat: create and use create:config script * fix: add /config to eslint projects * fix: remove gulp script
This commit is contained in:
committed by
GitHub
parent
82ca6d8441
commit
7216ca55cc
@ -1 +0,0 @@
|
||||
export declare function getArgValues(argv: string[]): Record<string, string>;
|
@ -1,4 +1,4 @@
|
||||
const { getArgValues } = require('./get-arg-values');
|
||||
import { getArgValues } from './get-arg-values';
|
||||
|
||||
describe('getArgValues helper', () => {
|
||||
it('should be able to run if there are no values to process', () => {
|
@ -1,6 +1,6 @@
|
||||
// Creates an object with the values starting at the third position of argv,
|
||||
// ['lorem', 'ipsum', 'one=1', 'two=2', ...] => { one: 1, two: 2, ...}
|
||||
function getArgValues(argv = []) {
|
||||
function getArgValues(argv: string[] = []): Record<string, string> {
|
||||
return argv.slice(2).reduce((argsObj, arg) => {
|
||||
const [argument, value] = arg.replace(/\s/g, '').split('=');
|
||||
|
||||
@ -12,4 +12,4 @@ function getArgValues(argv = []) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
exports.getArgValues = getArgValues;
|
||||
export { getArgValues };
|
@ -1 +0,0 @@
|
||||
export declare function getExistingStepNums(options: string[]): number[];
|
@ -1,5 +1,5 @@
|
||||
const mock = require('mock-fs');
|
||||
const { getExistingStepNums } = require('./get-existing-step-nums');
|
||||
import mock from 'mock-fs';
|
||||
import { getExistingStepNums } from './get-existing-step-nums';
|
||||
|
||||
// NOTE:
|
||||
// Use `console.log()` before mocking the filesystem or use
|
||||
@ -63,12 +63,12 @@ describe('getExistingStepNums helper', () => {
|
||||
|
||||
const folder = `${process.cwd()}/mock-project/`;
|
||||
const steps = getExistingStepNums(folder);
|
||||
const expected = [];
|
||||
const expected: number[] = [];
|
||||
|
||||
expect(steps).toEqual(expected);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
});
|
@ -1,30 +1,30 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Generates an array with the output of processing filenames with an expected
|
||||
// format (`step-###.md`).
|
||||
// ['step-001.md', 'step-002.md'] => [1, 2]
|
||||
function getExistingStepNums(projectPath) {
|
||||
function getExistingStepNums(projectPath: string): number[] {
|
||||
return fs.readdirSync(projectPath).reduce((stepNums, fileName) => {
|
||||
if (
|
||||
path.extname(fileName).toLowerCase() === '.md' &&
|
||||
!fileName.endsWith('final.md')
|
||||
) {
|
||||
let stepNum = fileName.split('.')[0].split('-')[1];
|
||||
const stepNumString = fileName.split('.')[0].split('-')[1];
|
||||
|
||||
if (!/^\d{3}$/.test(stepNum)) {
|
||||
if (!/^\d{3}$/.test(stepNumString)) {
|
||||
throw (
|
||||
`Step not created. File ${fileName} has a step number containing non-digits.` +
|
||||
' Please run reorder-steps script first.'
|
||||
);
|
||||
}
|
||||
|
||||
stepNum = parseInt(stepNum, 10);
|
||||
const stepNum = parseInt(stepNumString, 10);
|
||||
stepNums.push(stepNum);
|
||||
}
|
||||
|
||||
return stepNums;
|
||||
}, []);
|
||||
}, [] as number[]);
|
||||
}
|
||||
|
||||
exports.getExistingStepNums = getExistingStepNums;
|
||||
export { getExistingStepNums };
|
@ -1 +0,0 @@
|
||||
export declare function getLastStepFileContent(): Record<string, unknown>;
|
@ -1,5 +1,5 @@
|
||||
const mock = require('mock-fs');
|
||||
const { getLastStepFileContent } = require('./get-last-step-file-content');
|
||||
import mock from 'mock-fs';
|
||||
import { getLastStepFileContent } from './get-last-step-file-content';
|
||||
|
||||
jest.mock('./get-project-path', () => {
|
||||
return {
|
||||
@ -30,8 +30,6 @@ describe('getLastStepFileContent helper', () => {
|
||||
expect(() => {
|
||||
getLastStepFileContent();
|
||||
}).toThrow();
|
||||
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should return information if steps count is correct', () => {
|
||||
@ -51,7 +49,9 @@ describe('getLastStepFileContent helper', () => {
|
||||
};
|
||||
|
||||
expect(getLastStepFileContent()).toEqual(expected);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
});
|
@ -1,13 +1,17 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getChallengeSeeds } = require('../utils');
|
||||
const { getProjectPath } = require('./get-project-path');
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getChallengeSeeds } from '../utils';
|
||||
import { getProjectPath } from './get-project-path';
|
||||
import { ChallengeSeed } from './get-step-template';
|
||||
|
||||
// Looks up the last file found with format `step-###.md` in a directory and
|
||||
// returns associated information to it. At the same time validates that the
|
||||
// number of files match the names used to name these.
|
||||
function getLastStepFileContent() {
|
||||
const filesArr = [];
|
||||
function getLastStepFileContent(): {
|
||||
challengeSeeds: Record<string, ChallengeSeed>;
|
||||
nextStepNum: number;
|
||||
} {
|
||||
const filesArr: string[] = [];
|
||||
const projectPath = getProjectPath();
|
||||
|
||||
fs.readdirSync(projectPath).forEach(fileName => {
|
||||
@ -20,16 +24,18 @@ function getLastStepFileContent() {
|
||||
});
|
||||
|
||||
const fileName = filesArr[filesArr.length - 1];
|
||||
let lastStepFileNum = fileName.split('.')[0].split('-')[1];
|
||||
lastStepFileNum = parseInt(lastStepFileNum, 10);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
const lastStepFileString: string = fileName.split('.')[0].split('-')[1];
|
||||
const lastStepFileNum = parseInt(lastStepFileString, 10);
|
||||
if (filesArr.length !== lastStepFileNum) {
|
||||
throw `Error: The last file step is ${lastStepFileNum} and there are ${filesArr.length} files.`;
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
challengeSeeds: getChallengeSeeds(projectPath + fileName),
|
||||
nextStepNum: lastStepFileNum + 1
|
||||
};
|
||||
}
|
||||
|
||||
exports.getLastStepFileContent = getLastStepFileContent;
|
||||
export { getLastStepFileContent };
|
@ -1,4 +0,0 @@
|
||||
export declare function getProjectMetaPath(
|
||||
curriculumPath: string,
|
||||
projectName: string
|
||||
): string;
|
@ -1,19 +1,7 @@
|
||||
const { getProjectMetaPath } = require('./get-project-meta-path');
|
||||
import { getProjectMetaPath } from './get-project-meta-path';
|
||||
|
||||
describe('getProjectMetaPath helper', () => {
|
||||
it('should throw if args are invalid', () => {
|
||||
expect(() => {
|
||||
getProjectMetaPath();
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
getProjectMetaPath('test-curriculum', {});
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
getProjectMetaPath([], []);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
getProjectMetaPath('', '');
|
||||
}).toThrow();
|
@ -1,7 +1,10 @@
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
|
||||
// Returns the path of the meta file associated to arguments provided.
|
||||
const getProjectMetaPath = (curriculumPath, projectName) => {
|
||||
const getProjectMetaPath = (
|
||||
curriculumPath: string,
|
||||
projectName: string
|
||||
): string => {
|
||||
if (typeof curriculumPath !== 'string' || typeof projectName !== 'string') {
|
||||
throw `${curriculumPath} and ${projectName} should be of type string`;
|
||||
}
|
||||
@ -19,4 +22,4 @@ const getProjectMetaPath = (curriculumPath, projectName) => {
|
||||
);
|
||||
};
|
||||
|
||||
exports.getProjectMetaPath = getProjectMetaPath;
|
||||
export { getProjectMetaPath };
|
@ -1 +0,0 @@
|
||||
export declare function getMetaData(string): Record<string, unknown>;
|
@ -1,16 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// Process the contents of a argument (json) to an Object
|
||||
function getMetaData(file) {
|
||||
let metaData;
|
||||
|
||||
try {
|
||||
metaData = fs.readFileSync(file);
|
||||
} catch (err) {
|
||||
throw `No _meta.json file exists at ${file}`;
|
||||
}
|
||||
|
||||
return JSON.parse(metaData);
|
||||
}
|
||||
|
||||
exports.getMetaData = getMetaData;
|
@ -1,3 +1,6 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { getMetaData } from './get-project-path-metadata';
|
||||
|
||||
jest.mock('fs', () => {
|
||||
return {
|
||||
readFileSync: jest.fn()
|
||||
@ -5,11 +8,11 @@ jest.mock('fs', () => {
|
||||
});
|
||||
|
||||
const mockPath = '/mock/path';
|
||||
const { readFileSync } = require('fs');
|
||||
const { getMetaData } = require('./get-project-path-metadata');
|
||||
|
||||
describe('getMetaData helper', () => {
|
||||
it('should process requested file', () => {
|
||||
// @ts-expect-error - readFileSync is mocked
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
readFileSync.mockImplementation(() => '{"name": "Test Project"}');
|
||||
|
||||
const expected = {
|
||||
@ -20,6 +23,8 @@ describe('getMetaData helper', () => {
|
||||
});
|
||||
|
||||
it('should throw if file is not found', () => {
|
||||
// @ts-expect-error - readFileSync is mocked
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
readFileSync.mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
import fs from 'fs';
|
||||
|
||||
// Process the contents of a argument (json) to an Object
|
||||
function getMetaData(file: string): Record<string, unknown> {
|
||||
let metaData;
|
||||
|
||||
try {
|
||||
metaData = fs.readFileSync(file, 'utf8');
|
||||
} catch (err) {
|
||||
throw `No _meta.json file exists at ${file}`;
|
||||
}
|
||||
|
||||
return JSON.parse(metaData) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export { getMetaData };
|
@ -1 +0,0 @@
|
||||
export declare function getProjectPath(): string;
|
@ -1,4 +1,4 @@
|
||||
const { getProjectPath } = require('./get-project-path');
|
||||
import { getProjectPath } from './get-project-path';
|
||||
|
||||
describe('getProjectPath helper', () => {
|
||||
it('should return the calling dir path', () => {
|
@ -1,8 +1,8 @@
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
|
||||
// Returns the path of a project
|
||||
function getProjectPath() {
|
||||
function getProjectPath(): string {
|
||||
return (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
||||
}
|
||||
|
||||
exports.getProjectPath = getProjectPath;
|
||||
export { getProjectPath };
|
@ -1,8 +0,0 @@
|
||||
type StepOptions = {
|
||||
challengeId: string;
|
||||
challengeSeeds: unknown;
|
||||
stepBetween: boolean;
|
||||
stepNum: number;
|
||||
};
|
||||
|
||||
export declare function getStepTemplate(options: StepOptions): string;
|
@ -1,4 +1,5 @@
|
||||
const { getStepTemplate } = require('./get-step-template');
|
||||
import ObjectID from 'bson-objectid';
|
||||
import { getStepTemplate } from './get-step-template';
|
||||
|
||||
// Note: evaluates at highlevel the process, but seedHeads and seedTails could
|
||||
// be tested if more specifics are needed.
|
||||
@ -34,7 +35,7 @@ Test 1
|
||||
\`\`\`\n`;
|
||||
|
||||
const props = {
|
||||
challengeId: '60d4ebe4801158d1abe1b18f',
|
||||
challengeId: new ObjectID('60d4ebe4801158d1abe1b18f'),
|
||||
challengeSeeds: {
|
||||
indexhtml: {
|
||||
contents: '',
|
@ -1,14 +1,15 @@
|
||||
const { insertErms } = require('./insert-erms');
|
||||
import ObjectID from 'bson-objectid';
|
||||
import { insertErms } from './insert-erms';
|
||||
|
||||
// Builds a block
|
||||
function getCodeBlock(label, content) {
|
||||
function getCodeBlock(label: string, content?: string) {
|
||||
return `\`\`\`${label}
|
||||
${typeof content !== 'undefined' ? content : ''}
|
||||
\`\`\`\n`;
|
||||
}
|
||||
|
||||
// Builds a section
|
||||
function getSeedSection(content, label) {
|
||||
function getSeedSection(content: string, label: string) {
|
||||
return content
|
||||
? `
|
||||
|
||||
@ -18,15 +19,30 @@ ${content}`
|
||||
: '';
|
||||
}
|
||||
|
||||
type StepOptions = {
|
||||
challengeId: ObjectID;
|
||||
challengeSeeds: Record<string, ChallengeSeed>;
|
||||
stepBetween: boolean;
|
||||
stepNum: number;
|
||||
};
|
||||
|
||||
export interface ChallengeSeed {
|
||||
contents: string;
|
||||
ext: string;
|
||||
editableRegionBoundaries: number[];
|
||||
head?: string;
|
||||
tail?: string;
|
||||
}
|
||||
|
||||
// Build the base markdown for a step
|
||||
function getStepTemplate({
|
||||
challengeId,
|
||||
challengeSeeds,
|
||||
stepBetween,
|
||||
stepNum
|
||||
}) {
|
||||
}: StepOptions): string {
|
||||
const seedTexts = Object.values(challengeSeeds)
|
||||
.map(({ contents, ext, editableRegionBoundaries }) => {
|
||||
.map(({ contents, ext, editableRegionBoundaries }: ChallengeSeed) => {
|
||||
let fullContents = contents;
|
||||
if (editableRegionBoundaries.length >= 2) {
|
||||
fullContents = insertErms(contents, editableRegionBoundaries);
|
||||
@ -36,13 +52,13 @@ function getStepTemplate({
|
||||
.join('\n');
|
||||
|
||||
const seedHeads = Object.values(challengeSeeds)
|
||||
.filter(({ head }) => head)
|
||||
.map(({ ext, head }) => getCodeBlock(ext, head))
|
||||
.filter(({ head }: ChallengeSeed) => head)
|
||||
.map(({ ext, head }: ChallengeSeed) => getCodeBlock(ext, head))
|
||||
.join('\n');
|
||||
|
||||
const seedTails = Object.values(challengeSeeds)
|
||||
.filter(({ tail }) => tail)
|
||||
.map(({ ext, tail }) => getCodeBlock(ext, tail))
|
||||
.filter(({ tail }: ChallengeSeed) => tail)
|
||||
.map(({ ext, tail }: ChallengeSeed) => getCodeBlock(ext, tail))
|
||||
.join('\n');
|
||||
|
||||
const descStepNum = stepBetween ? stepNum + 1 : stepNum;
|
||||
@ -57,7 +73,7 @@ function getStepTemplate({
|
||||
|
||||
return (
|
||||
`---
|
||||
id: ${challengeId}
|
||||
id: ${challengeId.toString()}
|
||||
title: Step ${stepNum}
|
||||
challengeType: 0
|
||||
dashedName: step-${stepNum}
|
||||
@ -79,4 +95,4 @@ ${getCodeBlock('js')}
|
||||
);
|
||||
}
|
||||
|
||||
exports.getStepTemplate = getStepTemplate;
|
||||
export { getStepTemplate };
|
@ -1 +0,0 @@
|
||||
export declare function insertErms(seedCode: string, erms: number[]): string;
|
@ -1,4 +1,4 @@
|
||||
const { insertErms } = require('./insert-erms');
|
||||
import { insertErms } from './insert-erms';
|
||||
|
||||
describe('insertErms helper', () => {
|
||||
const code = `<h1>Hello World</h1>
|
||||
@ -9,12 +9,6 @@ describe('insertErms helper', () => {
|
||||
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
|
||||
</main>`;
|
||||
|
||||
it('should throw error if erm is undefined', () => {
|
||||
expect(() => {
|
||||
insertErms(code);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should throw error if erm length is less than 2', () => {
|
||||
const items = [[], [1]];
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Update given value with markers (labels)
|
||||
const insertErms = (seedCode, erms) => {
|
||||
const insertErms = (seedCode: string, erms: number[]): string => {
|
||||
if (!erms || erms.length <= 1) {
|
||||
throw `erms should be provided`;
|
||||
}
|
||||
@ -27,4 +27,4 @@ const insertErms = (seedCode, erms) => {
|
||||
return newSeedCode;
|
||||
};
|
||||
|
||||
exports.insertErms = insertErms;
|
||||
export { insertErms };
|
@ -1 +0,0 @@
|
||||
export declare function padWithLeadingZeros(value: number | string): string;
|
@ -1,18 +0,0 @@
|
||||
// Prepends zero's to the given value until length is equal to 3:
|
||||
// '1' -> '001', '12' -> '012', ...
|
||||
// Note: always want file step numbers 3 digits
|
||||
function padWithLeadingZeros(value) {
|
||||
if (!(typeof value === 'number' || typeof value === 'string')) {
|
||||
throw `${value} should be of type number or string`;
|
||||
}
|
||||
|
||||
const newValue = '' + value;
|
||||
|
||||
if (newValue.length > 3) {
|
||||
throw `${newValue} should be less than 4 characters`;
|
||||
}
|
||||
|
||||
return newValue.padStart(3, '0');
|
||||
}
|
||||
|
||||
exports.padWithLeadingZeros = padWithLeadingZeros;
|
@ -13,20 +13,6 @@ describe('padWithLeadingZeros helper', () => {
|
||||
expect(padWithLeadingZeros('111')).toEqual('111');
|
||||
});
|
||||
|
||||
it('should throw on invalid values', () => {
|
||||
const items = ['undefined', null, []];
|
||||
|
||||
items.forEach(item =>
|
||||
expect(() => {
|
||||
if (item !== 'undefined') {
|
||||
padWithLeadingZeros(item);
|
||||
} else {
|
||||
padWithLeadingZeros();
|
||||
}
|
||||
}).toThrow()
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on valid values that are longer that 3 characters', () => {
|
||||
expect(() => {
|
||||
padWithLeadingZeros('19850809');
|
||||
|
@ -0,0 +1,14 @@
|
||||
// Prepends zero's to the given value until length is equal to 3:
|
||||
// '1' -> '001', '12' -> '012', ...
|
||||
// Note: always want file step numbers 3 digits
|
||||
function padWithLeadingZeros(value: number): string {
|
||||
const valueString = value.toString();
|
||||
|
||||
if (valueString.length > 3) {
|
||||
throw `${valueString} should be less than 4 characters`;
|
||||
}
|
||||
|
||||
return valueString.padStart(3, '0');
|
||||
}
|
||||
|
||||
export { padWithLeadingZeros };
|
Reference in New Issue
Block a user