diff --git a/src/build.ts b/src/build.ts index 7bbd230..9659fad 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,171 +1,171 @@ -import * as yamlParser from "js-yaml"; -import * as path from "path"; -import * as fs from "fs"; -import * as util from "util"; -import { parse } from "./utils/parse"; -import { getArg } from "./utils/args"; -import { getCommits, CommitLogObject } from "./utils/commits"; -import skeletonSchema from "./schema/skeleton"; -import tutorialSchema from "./schema/tutorial"; -import { validateSchema } from "./utils/validateSchema"; -import { validateMarkdown } from "./utils/validateMarkdown"; -import * as T from "../typings/tutorial"; - -const write = util.promisify(fs.writeFile); -const read = util.promisify(fs.readFile); +import * as yamlParser from 'js-yaml' +import * as path from 'path' +import * as fs from 'fs' +import * as util from 'util' +import { parse } from './utils/parse' +import { getArg } from './utils/args' +import { getCommits, CommitLogObject } from './utils/commits' +import skeletonSchema from './schema/skeleton' +import tutorialSchema from './schema/tutorial' +import { validateSchema } from './utils/validateSchema' +import { validateMarkdown } from './utils/validateMarkdown' +import * as T from '../typings/tutorial' + +const write = util.promisify(fs.writeFile) +const read = util.promisify(fs.readFile) export type BuildConfigOptions = { - text: string; // text document from markdown - config: T.Tutorial; // yaml config file converted to json - commits: CommitLogObject; // an object of tutorial positions with a list of commit hashes -}; + text: string // text document from markdown + config: T.Tutorial // yaml config file converted to json + commits: CommitLogObject // an object of tutorial positions with a list of commit hashes +} type BuildArgs = { - dir: string; - markdown: string; - yaml: string; - output: string; - validate: boolean; -}; + dir: string + markdown: string + yaml: string + output: string + validate: boolean +} -async function build(args: string[]) { - let options: BuildArgs; +async function build (args: string[]) { + let options: BuildArgs try { // dir - default . - const dir = !args.length || args[0].match(/^-/) ? "." : args[0]; + const dir = !args.length || args[0].match(/^-/) ? '.' : args[0] // -m --markdown - default TUTORIAL.md const markdown = - getArg(args, { name: "markdown", alias: "m" }) || "TUTORIAL.md"; + getArg(args, { name: 'markdown', alias: 'm' }) || 'TUTORIAL.md' // -y --yaml - default coderoad-config.yml - const yaml = getArg(args, { name: "yaml", alias: "y" }) || "coderoad.yaml"; + const yaml = getArg(args, { name: 'yaml', alias: 'y' }) || 'coderoad.yaml' // -o --output - default coderoad.json const output = - getArg(args, { name: "output", alias: "o" }) || "tutorial.json"; - const validate = getArg(args, { name: "validate", alias: "v" }) !== "false"; + getArg(args, { name: 'output', alias: 'o' }) || 'tutorial.json' + const validate = getArg(args, { name: 'validate', alias: 'v' }) !== 'false' - console.log(`Building CodeRoad ${output}...`); + console.log(`Building CodeRoad ${output}...`) options = { dir, output, markdown, yaml, - validate, - }; + validate + } } catch (e) { - console.error("Error parsing build logs"); - console.error(e.message); - return; + console.error('Error parsing build logs') + console.error(e.message) + return } // path to run build from - const localPath = path.join(process.cwd(), options.dir); + const localPath = path.join(process.cwd(), options.dir) // load markdown and files - let _markdown: string; - let _yaml: string; + let _markdown: string + let _yaml: string try { - [_markdown, _yaml] = await Promise.all([ - read(path.join(localPath, options.markdown), "utf8"), - read(path.join(localPath, options.yaml), "utf8"), - ]); + ;[_markdown, _yaml] = await Promise.all([ + read(path.join(localPath, options.markdown), 'utf8'), + read(path.join(localPath, options.yaml), 'utf8') + ]) } catch (e) { - console.error("Error reading file:"); - console.error(e.message); - return; + console.error('Error reading file:') + console.error(e.message) + return } // validate markdown loosely try { - const isValid = validateMarkdown(_markdown); + const isValid = validateMarkdown(_markdown) if (!isValid) { - console.warn("Invalid markdown"); + console.warn('Invalid markdown') } } catch (e) { - console.error("Error validating markdown:"); - console.error(e.message); - return; + console.error('Error validating markdown:') + console.error(e.message) + return } // parse yaml skeleton config - let skeleton; + let skeleton try { - skeleton = yamlParser.load(_yaml); + skeleton = yamlParser.load(_yaml) if (!skeleton || !Object.keys(skeleton).length) { - throw new Error(`Skeleton at "${options.yaml}" is invalid`); + throw new Error(`Skeleton at "${options.yaml}" is invalid`) } } catch (e) { - console.error("Error parsing yaml"); - console.error(e.message); - return; + console.error('Error parsing yaml') + console.error(e.message) + return } // validate skeleton based on skeleton json schema try { - const valid = validateSchema(skeletonSchema, skeleton); + const valid = validateSchema(skeletonSchema, skeleton) if (!valid) { - console.error("Skeleton validation failed. See above to see what to fix"); - return; + console.error('Skeleton validation failed. See above to see what to fix') + return } } catch (e) { - console.error("Error validating tutorial schema:"); - console.error(e.message); + console.error('Error validating tutorial schema:') + console.error(e.message) } // load git commits to use in parse step - let commits: CommitLogObject; + let commits: CommitLogObject try { commits = await getCommits({ localDir: localPath, - codeBranch: skeleton.config.repo.branch, - }); + codeBranch: skeleton.config.repo.branch + }) } catch (e) { - console.error("Error loading commits:"); - console.error(e.message); - return; + console.error('Error loading commits:') + console.error(e.message) + return } // parse tutorial from markdown and yaml - let tutorial: T.Tutorial; + let tutorial: T.Tutorial try { tutorial = await parse({ text: _markdown, skeleton, - commits, - }); + commits + }) } catch (e) { - console.error("Error parsing tutorial:"); - console.error(e.message); - return; + console.error('Error parsing tutorial:') + console.error(e.message) + return } // validate tutorial based on tutorial json schema try { if (options.validate) { - const valid = validateSchema(tutorialSchema, tutorial); + const valid = validateSchema(tutorialSchema, tutorial) if (!valid) { console.error( - "Tutorial validation failed. See above to see what to fix" - ); + 'Tutorial validation failed. See above to see what to fix' + ) // continue rather than exiting early } } } catch (e) { - console.error("Error validating tutorial schema:"); - console.error(e.message); + console.error('Error validating tutorial schema:') + console.error(e.message) } // write tutorial if (tutorial) { try { - await write(options.output, JSON.stringify(tutorial, null, 2), "utf8"); - console.info(`Success! See output at ${options.output}`); + await write(options.output, JSON.stringify(tutorial, null, 2), 'utf8') + console.info(`Success! See output at ${options.output}`) } catch (e) { - console.error("Error writing tutorial json file:"); - console.error(e.message); + console.error('Error writing tutorial json file:') + console.error(e.message) } } } -export default build; +export default build diff --git a/src/cli.ts b/src/cli.ts index 7f2edb9..5122abf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,47 +1,47 @@ -import "./utils/logs"; -import build from "./build"; -import create from "./create"; -import validate from "./validate"; -import * as help from "./help"; +import './utils/logs' +import build from './build' +import create from './create' +import validate from './validate' +import * as help from './help' -export async function cli(rawArgs: string[]): Promise { - const command: string = rawArgs[2]; - const args = rawArgs.slice(3); +export async function cli (rawArgs: string[]): Promise { + const command: string = rawArgs[2] + const args = rawArgs.slice(3) switch (command) { - case "--version": - case "-v": - const version = require("../package.json").version; - console.log(`v${version}`); - return; + case '--version': + case '-v': + const version = require('../package.json').version + console.log(`v${version}`) + return - case "build": - if (args.length && ["--help", "-h"].includes(args[0])) { - help.build(); - return; + case 'build': + if (args.length && ['--help', '-h'].includes(args[0])) { + help.build() + return } - build(args); - break; + build(args) + break - case "create": - if (args.length && ["--help", "-h"].includes(args[0])) { - help.create(); - return; + case 'create': + if (args.length && ['--help', '-h'].includes(args[0])) { + help.create() + return } - create(args); - break; + create(args) + break - case "validate": - if (args.length && ["--help", "-h"].includes(args[0])) { - help.validate(); - return; + case 'validate': + if (args.length && ['--help', '-h'].includes(args[0])) { + help.validate() + return } - validate(args); - break; + validate(args) + break - case "--help": - case "-h": + case '--help': + case '-h': default: - help.main(); + help.main() } } diff --git a/src/create.ts b/src/create.ts index ad6b223..9211090 100644 --- a/src/create.ts +++ b/src/create.ts @@ -1,76 +1,76 @@ -import ncp from "ncp"; -import * as path from "path"; -import { promisify } from "util"; -import { getArg } from "./utils/args"; +import ncp from 'ncp' +import * as path from 'path' +import { promisify } from 'util' +import { getArg } from './utils/args' -const copy = promisify(ncp); +const copy = promisify(ncp) type CreateArgs = { - dir: string; - lang: string; - testRunner: string; -}; + dir: string + lang: string + testRunner: string +} -async function create(args: string[]): Promise { - let options: CreateArgs; +async function create (args: string[]): Promise { + let options: CreateArgs // dir - default . - const dir = !args.length || args[0].match(/^-/) ? "." : args[0]; + const dir = !args.length || args[0].match(/^-/) ? '.' : args[0] // lang - default js - const lang: string = getArg(args, { name: "lang", alias: "l" }) || "js"; + const lang: string = getArg(args, { name: 'lang', alias: 'l' }) || 'js' // testRunner - default mocha const testRunner: string = - getArg(args, { name: "testRunner", alias: "t" }) || "mocha"; + getArg(args, { name: 'testRunner', alias: 't' }) || 'mocha' // validate lang - if (!["js"].includes(lang)) { - throw new Error(`Language ${lang} not supported yet in create`); + if (!['js'].includes(lang)) { + throw new Error(`Language ${lang} not supported yet in create`) } // validate test runner - if (!["mocha"].includes(testRunner)) { - throw new Error(`Test Runner ${testRunner} not supported yet in create`); + if (!['mocha'].includes(testRunner)) { + throw new Error(`Test Runner ${testRunner} not supported yet in create`) } - console.info(`Creating CodeRoad project for ${lang} ${testRunner}`); + console.info(`Creating CodeRoad project for ${lang} ${testRunner}`) options = { dir, lang, - testRunner, - }; + testRunner + } - const localPath = path.join(process.cwd(), options.dir); + const localPath = path.join(process.cwd(), options.dir) // TODO: git init ? // copy tutorial file - const pathToSrc = path.join(__dirname, "..", "src"); - const templateDirectory = path.resolve(pathToSrc, "templates"); + const pathToSrc = path.join(__dirname, '..', 'src') + const templateDirectory = path.resolve(pathToSrc, 'templates') - const markdownPath = path.join(templateDirectory, "TUTORIAL.md"); - const targetMarkdownPath = path.join(localPath, "TUTORIAL.md"); + const markdownPath = path.join(templateDirectory, 'TUTORIAL.md') + const targetMarkdownPath = path.join(localPath, 'TUTORIAL.md') try { await copy(markdownPath, targetMarkdownPath, { - clobber: false, - }); + clobber: false + }) } catch (e) { - console.error("Error on creating markdown file"); - console.error(e.message); + console.error('Error on creating markdown file') + console.error(e.message) } // TODO: copy master yaml - const pathToYaml = path.join(templateDirectory, `${lang}-${testRunner}`); + const pathToYaml = path.join(templateDirectory, `${lang}-${testRunner}`) try { await copy(pathToYaml, localPath, { - clobber: false, - }); + clobber: false + }) } catch (e) { - console.error("Error on creating yaml file"); - console.error(e.message); + console.error('Error on creating yaml file') + console.error(e.message) } // TODO: copy code files with commits } -export default create; +export default create diff --git a/src/help.ts b/src/help.ts index 5cbc637..e6b86a0 100644 --- a/src/help.ts +++ b/src/help.ts @@ -1,4 +1,4 @@ -export function main() { +export function main () { console.log(` CodeRoad CLI Usage: coderoad [options] @@ -10,10 +10,10 @@ create start a new tutorial from a template build generate a coderoad.json file from markdown and yaml validate run a variety of tests to ensure a tutorial works as intended -More docs at https://github.com/coderoad/coderoad-cli`); +More docs at https://github.com/coderoad/coderoad-cli`) } -export function create() { +export function create () { console.log(`Create a new CodeRoad tutorial project from a template. Usage: coderoad create [path] [options] @@ -23,10 +23,10 @@ Options: --lang (-l) programming language for template --testRunner (-t) test runner module for template -More docs at https://github.com/coderoad/coderoad-cli`); +More docs at https://github.com/coderoad/coderoad-cli`) } -export function build() { +export function build () { console.log(`Compile a coderoad.json file from markdown & yaml. Usage: coderoad build [path] [options] @@ -37,10 +37,10 @@ Options: --yaml (-y) (coderoad.yaml) custom path to the tutorial yaml file --output (-o) (coderoad.json) custom path to tutorial json config file -More docs at https://github.com/coderoad/coderoad-cli`); +More docs at https://github.com/coderoad/coderoad-cli`) } -export function validate() { +export function validate () { console.log(`Validates commits and tests across a tutorial. Usage: coderoad validate [path] [options] @@ -50,5 +50,5 @@ Options: --validate (-v) (true) run tutorial schema validation. Set to false to block validation. --clean (-c) (false) set to false to preserve .tmp folder. Helpful for debugging -More docs at https://github.com/coderoad/coderoad-cli`); +More docs at https://github.com/coderoad/coderoad-cli`) } diff --git a/src/schema/meta.ts b/src/schema/meta.ts index 25b71d5..8778860 100644 --- a/src/schema/meta.ts +++ b/src/schema/meta.ts @@ -1,155 +1,155 @@ export default { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "https://coderoad.io/tutorial-schema.json", + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://coderoad.io/tutorial-schema.json', definitions: { semantic_version: { - type: "string", + type: 'string', description: 'A semantic version, such as "1.0.0". Learn more at https://semver.org/', - pattern: "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$", + pattern: '^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$', minLength: 5, maxLength: 14, - examples: ["0.1.0", "1.0.0"], + examples: ['0.1.0', '1.0.0'] }, sha1_hash: { - type: "string", - description: "A SHA1 hash created by Git", - pattern: "^[0-9a-f]{5,40}$", + type: 'string', + description: 'A SHA1 hash created by Git', + pattern: '^[0-9a-f]{5,40}$', minLength: 5, - maxLength: 40, + maxLength: 40 }, title: { - type: "string", + type: 'string', minLength: 1, - maxLength: 100, + maxLength: 100 }, file_path: { - type: "string", - description: "A path to a file", - pattern: "(\\\\?([^\\/]*[\\/])*)([^\\/]+)$", + type: 'string', + description: 'A path to a file', + pattern: '(\\\\?([^\\/]*[\\/])*)([^\\/]+)$', minLength: 4, - examples: ["src/file.js"], + examples: ['src/file.js'] }, file_array: { - type: "array", + type: 'array', description: - "An array of files which will be opened by the editor when entering the level or step", + 'An array of files which will be opened by the editor when entering the level or step', items: { - $ref: "#/definitions/file_path", + $ref: '#/definitions/file_path' // uniqueItems: true, - }, + } }, command_array: { - type: "array", + type: 'array', description: - "An array of command line commands that will be called when the user enters the level or step. Currently commands are limited for security purposes", + 'An array of command line commands that will be called when the user enters the level or step. Currently commands are limited for security purposes', items: { - type: "string", - }, + type: 'string' + } }, vscode_command_array: { - type: "array", + type: 'array', description: - "An array of VSCode commands that can be called using the vscode commands API.", + 'An array of VSCode commands that can be called using the vscode commands API.', items: { anyOf: [ { - type: "string", - description: "A VSCode command without params", + type: 'string', + description: 'A VSCode command without params' }, { - type: "array", - description: "A VSCode command with params", + type: 'array', + description: 'A VSCode command with params', minLength: 2, - maxLength: 2, - }, - ], - }, + maxLength: 2 + } + ] + } }, commit_array: { - type: "array", + type: 'array', description: - "An array of git commits which will be loaded when the level/step or solution is loaded", + 'An array of git commits which will be loaded when the level/step or solution is loaded', items: { - $ref: "#/definitions/sha1_hash", + $ref: '#/definitions/sha1_hash' // uniqueItems: true, - }, + } }, setup_action: { - type: "object", + type: 'object', description: - "A collection of files/commits/commands that run when a level/step or solution is loaded", + 'A collection of files/commits/commands that run when a level/step or solution is loaded', properties: { files: { - $ref: "#/definitions/file_array", + $ref: '#/definitions/file_array' }, commits: { - $ref: "#/definitions/commit_array", + $ref: '#/definitions/commit_array' }, commands: { - $ref: "#/definitions/command_array", + $ref: '#/definitions/command_array' }, watchers: { - type: "array", + type: 'array', items: { - $ref: "#/definitions/file_path", + $ref: '#/definitions/file_path' // uniqueItems: true, }, description: - "An array file paths that, when updated, will trigger the test runner to run", + 'An array file paths that, when updated, will trigger the test runner to run' }, filter: { - type: "string", + type: 'string', description: - "A regex pattern that will be passed to the test runner to limit the number of tests running", - examples: ["^TestSuiteName"], + 'A regex pattern that will be passed to the test runner to limit the number of tests running', + examples: ['^TestSuiteName'] }, subtasks: { - type: "boolean", + type: 'boolean', description: 'A feature that shows subtasks: all active test names and the status of the tests (pass/fail). Use together with "filter"', - examples: [true], - }, + examples: [true] + } }, - additionalProperties: false, + additionalProperties: false }, setup_action_without_commits: { - type: "object", + type: 'object', description: - "A collection of files/commands that run when a level/step or solution is loaded", + 'A collection of files/commands that run when a level/step or solution is loaded', properties: { files: { - $ref: "#/definitions/file_array", + $ref: '#/definitions/file_array' }, commands: { - $ref: "#/definitions/command_array", + $ref: '#/definitions/command_array' }, vscodeCommands: { - $ref: "#/definitions/vscode_command_array", + $ref: '#/definitions/vscode_command_array' }, watchers: { - type: "array", + type: 'array', items: { - $ref: "#/definitions/file_path", + $ref: '#/definitions/file_path' // uniqueItems: true, }, description: - "An array file paths that, when updated, will trigger the test runner to run", + 'An array file paths that, when updated, will trigger the test runner to run' }, filter: { - type: "string", + type: 'string', description: - "A regex pattern that will be passed to the test runner to limit the number of tests running", - examples: ["^TestSuiteName"], + 'A regex pattern that will be passed to the test runner to limit the number of tests running', + examples: ['^TestSuiteName'] }, subtasks: { - type: "boolean", + type: 'boolean', description: 'A feature that shows subtasks: all active test names and the status of the tests (pass/fail). Use together with "filter"', - examples: [true], - }, + examples: [true] + } }, - additionalProperties: false, - }, - }, -}; + additionalProperties: false + } + } +} diff --git a/src/schema/skeleton.ts b/src/schema/skeleton.ts index 6d955bc..5a128df 100644 --- a/src/schema/skeleton.ts +++ b/src/schema/skeleton.ts @@ -1,208 +1,208 @@ -import meta from "./meta"; +import meta from './meta' export default { - title: "Skeleton Schema", + title: 'Skeleton Schema', description: - "A CodeRoad tutorial config schema. This data is paired up with the markdown to create a tutorial", + 'A CodeRoad tutorial config schema. This data is paired up with the markdown to create a tutorial', ...meta, - type: "object", + type: 'object', properties: { version: { - $ref: "#/definitions/semantic_version", - description: "The tutorial version. Must be unique for the tutorial.", - examples: ["0.1.0", "1.0.0"], + $ref: '#/definitions/semantic_version', + description: 'The tutorial version. Must be unique for the tutorial.', + examples: ['0.1.0', '1.0.0'] }, // config config: { - type: "object", + type: 'object', properties: { testRunner: { - type: "object", - description: "The test runner configuration", + type: 'object', + description: 'The test runner configuration', properties: { command: { - type: "string", - description: "Command line to start the test runner", - examples: ["./node_modules/.bin/mocha"], + type: 'string', + description: 'Command line to start the test runner', + examples: ['./node_modules/.bin/mocha'] }, args: { - type: "object", + type: 'object', description: - "A configuration of command line args for your test runner", + 'A configuration of command line args for your test runner', properties: { filter: { - type: "string", + type: 'string', description: - "the command line arg for filtering tests with a regex pattern", - examples: ["--grep"], + 'the command line arg for filtering tests with a regex pattern', + examples: ['--grep'] }, tap: { - type: "string", + type: 'string', description: - "The command line arg for configuring a TAP reporter. See https://github.com/sindresorhus/awesome-tap for examples.", - examples: ["--reporter=mocha-tap-reporter"], - }, + 'The command line arg for configuring a TAP reporter. See https://github.com/sindresorhus/awesome-tap for examples.', + examples: ['--reporter=mocha-tap-reporter'] + } }, additionalProperties: false, - required: ["tap"], + required: ['tap'] }, directory: { - type: "string", - description: "An optional folder for the test runner", - examples: ["coderoad"], - }, + type: 'string', + description: 'An optional folder for the test runner', + examples: ['coderoad'] + } }, - required: ["command", "args"], + required: ['command', 'args'] }, setup: { - type: "object", + type: 'object', description: - "Setup commits or commands used for setting up the test runner on tutorial launch", + 'Setup commits or commands used for setting up the test runner on tutorial launch', properties: { commits: { - $ref: "#/definitions/commit_array", + $ref: '#/definitions/commit_array' }, commands: { - $ref: "#/definitions/command_array", + $ref: '#/definitions/command_array' }, vscodeCommands: { - $ref: "#/definitions/vscode_command_array", - }, - }, + $ref: '#/definitions/vscode_command_array' + } + } }, repo: { - type: "object", - description: "The repo holding the git commits for the tutorial", + type: 'object', + description: 'The repo holding the git commits for the tutorial', properties: { uri: { - type: "string", - description: "The uri source of the tutorial", - format: "uri", - examples: ["https://github.com/name/tutorial-name.git"], + type: 'string', + description: 'The uri source of the tutorial', + format: 'uri', + examples: ['https://github.com/name/tutorial-name.git'] }, branch: { description: - "The branch of the repo where the tutorial config file exists", - type: "string", - examples: ["master"], - }, + 'The branch of the repo where the tutorial config file exists', + type: 'string', + examples: ['master'] + } }, additionalProperties: false, - required: ["uri", "branch"], + required: ['uri', 'branch'] }, reset: { - type: "object", - description: "Configuration options for resetting a tutorial", + type: 'object', + description: 'Configuration options for resetting a tutorial', properties: { commands: { - $ref: "#/definitions/command_array", + $ref: '#/definitions/command_array' }, vscodeCommands: { - $ref: "#/definitions/vscode_command_array", - }, + $ref: '#/definitions/vscode_command_array' + } }, - additionalProperties: false, + additionalProperties: false }, dependencies: { - type: "array", - description: "A list of tutorial dependencies", + type: 'array', + description: 'A list of tutorial dependencies', items: { - type: "object", + type: 'object', properties: { name: { - type: "string", + type: 'string', description: - "The command line process name of the dependency. It will be checked by running `name --version`", - examples: ["node", "python"], + 'The command line process name of the dependency. It will be checked by running `name --version`', + examples: ['node', 'python'] }, version: { - type: "string", + type: 'string', description: - "The version requirement. See https://github.com/npm/node-semver for options", - examples: [">=10"], - }, + 'The version requirement. See https://github.com/npm/node-semver for options', + examples: ['>=10'] + } }, - required: ["name", "version"], - }, + required: ['name', 'version'] + } }, appVersions: { - type: "object", + type: 'object', description: - "A list of compatable coderoad versions. Currently only a VSCode extension.", + 'A list of compatable coderoad versions. Currently only a VSCode extension.', properties: { vscode: { - type: "string", + type: 'string', description: - "The version range for coderoad-vscode that this tutorial is compatable with", - examples: [">=0.7.0"], - }, - }, - }, + 'The version range for coderoad-vscode that this tutorial is compatable with', + examples: ['>=0.7.0'] + } + } + } }, additionalProperties: false, - required: ["testRunner", "repo"], + required: ['testRunner', 'repo'] }, // levels levels: { - type: "array", + type: 'array', description: 'Levels are the stages a user goes through in the tutorial. A level may contain a group of tasks called "steps" that must be completed to proceed', items: { - type: "object", + type: 'object', properties: { id: { - type: "string", - description: "A level id", - examples: ["1", "11"], + type: 'string', + description: 'A level id', + examples: ['1', '11'] }, setup: { - $ref: "#/definitions/setup_action_without_commits", + $ref: '#/definitions/setup_action_without_commits', description: - "An optional point for running actions, commands or opening files", + 'An optional point for running actions, commands or opening files' }, steps: { - type: "array", + type: 'array', items: { - type: "object", + type: 'object', properties: { id: { - type: "string", - description: "A level id", - examples: ["1.1", "11.12"], + type: 'string', + description: 'A level id', + examples: ['1.1', '11.12'] }, setup: { allOf: [ { - $ref: "#/definitions/setup_action_without_commits", + $ref: '#/definitions/setup_action_without_commits', description: - "A point for running actions, commands and/or opening files", - }, - ], + 'A point for running actions, commands and/or opening files' + } + ] }, solution: { allOf: [ { - $ref: "#/definitions/setup_action_without_commits", + $ref: '#/definitions/setup_action_without_commits', description: - "The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files", + 'The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files' }, { - required: [], - }, - ], - }, + required: [] + } + ] + } }, - required: ["id"], - }, - }, + required: ['id'] + } + } }, - required: ["id"], + required: ['id'] }, - minItems: 1, - }, + minItems: 1 + } }, additionalProperties: false, - required: ["version", "config", "levels"], -}; + required: ['version', 'config', 'levels'] +} diff --git a/src/schema/tutorial.ts b/src/schema/tutorial.ts index 915df70..7c8b677 100644 --- a/src/schema/tutorial.ts +++ b/src/schema/tutorial.ts @@ -1,247 +1,247 @@ -import meta from "./meta"; +import meta from './meta' export default { - title: "Tutorial Schema", + title: 'Tutorial Schema', description: - "A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension", + 'A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension', ...meta, - type: "object", + type: 'object', properties: { version: { - $ref: "#/definitions/semantic_version", - description: "The tutorial version. Must be unique for the tutorial.", - examples: ["0.1.0", "1.0.0"], + $ref: '#/definitions/semantic_version', + description: 'The tutorial version. Must be unique for the tutorial.', + examples: ['0.1.0', '1.0.0'] }, // summary summary: { - type: "object", + type: 'object', properties: { title: { - $ref: "#/definitions/title", - description: "The title of tutorial", + $ref: '#/definitions/title', + description: 'The title of tutorial' }, description: { - type: "string", - description: "A summary of the the tutorial", + type: 'string', + description: 'A summary of the the tutorial', minLength: 10, - maxLength: 400, - }, + maxLength: 400 + } }, additionalProperties: false, - required: ["title", "description"], + required: ['title', 'description'] }, // config config: { - type: "object", + type: 'object', properties: { testRunner: { - type: "object", - description: "The test runner configuration", + type: 'object', + description: 'The test runner configuration', properties: { command: { - type: "string", - description: "Command line to start the test runner", - examples: ["./node_modules/.bin/mocha"], + type: 'string', + description: 'Command line to start the test runner', + examples: ['./node_modules/.bin/mocha'] }, args: { - type: "object", + type: 'object', description: - "A configuration of command line args for your test runner", + 'A configuration of command line args for your test runner', properties: { filter: { - type: "string", + type: 'string', description: - "the command line arg for filtering tests with a regex pattern", - examples: ["--grep"], + 'the command line arg for filtering tests with a regex pattern', + examples: ['--grep'] }, tap: { - type: "string", + type: 'string', description: - "The command line arg for configuring a TAP reporter. See https://github.com/sindresorhus/awesome-tap for examples.", - examples: ["--reporter=mocha-tap-reporter"], - }, + 'The command line arg for configuring a TAP reporter. See https://github.com/sindresorhus/awesome-tap for examples.', + examples: ['--reporter=mocha-tap-reporter'] + } }, additionalProperties: false, - required: ["tap"], + required: ['tap'] }, directory: { - type: "string", - description: "An optional folder for the test runner", - examples: ["coderoad"], - }, + type: 'string', + description: 'An optional folder for the test runner', + examples: ['coderoad'] + } }, - required: ["command", "args"], + required: ['command', 'args'] }, setup: { - type: "object", + type: 'object', description: - "Setup commits or commands used for setting up the test runner on tutorial launch", + 'Setup commits or commands used for setting up the test runner on tutorial launch', properties: { commits: { - $ref: "#/definitions/commit_array", + $ref: '#/definitions/commit_array' }, commands: { - $ref: "#/definitions/command_array", + $ref: '#/definitions/command_array' }, vscodeCommands: { - $ref: "#/definitions/vscode_command_array", - }, - }, + $ref: '#/definitions/vscode_command_array' + } + } }, repo: { - type: "object", - description: "The repo holding the git commits for the tutorial", + type: 'object', + description: 'The repo holding the git commits for the tutorial', properties: { uri: { - type: "string", - description: "The uri source of the tutorial", - format: "uri", - examples: ["https://github.com/name/tutorial-name.git"], + type: 'string', + description: 'The uri source of the tutorial', + format: 'uri', + examples: ['https://github.com/name/tutorial-name.git'] }, branch: { description: - "The branch of the repo where the tutorial config file exists", - type: "string", - examples: ["master"], - }, + 'The branch of the repo where the tutorial config file exists', + type: 'string', + examples: ['master'] + } }, additionalProperties: false, - required: ["uri", "branch"], + required: ['uri', 'branch'] }, reset: { - type: "object", - description: "Configuration options for resetting a tutorial", + type: 'object', + description: 'Configuration options for resetting a tutorial', properties: { commands: { - $ref: "#/definitions/command_array", + $ref: '#/definitions/command_array' }, vscodeCommands: { - $ref: "#/definitions/vscode_command_array", - }, + $ref: '#/definitions/vscode_command_array' + } }, - additionalProperties: false, + additionalProperties: false }, dependencies: { - type: "array", - description: "A list of tutorial dependencies", + type: 'array', + description: 'A list of tutorial dependencies', items: { - type: "object", + type: 'object', properties: { name: { - type: "string", + type: 'string', description: - "The command line process name of the dependency. It will be checked by running `name --version`", - examples: ["node", "python"], + 'The command line process name of the dependency. It will be checked by running `name --version`', + examples: ['node', 'python'] }, version: { - type: "string", + type: 'string', description: - "The version requirement. See https://github.com/npm/node-semver for options", - examples: [">=10"], - }, + 'The version requirement. See https://github.com/npm/node-semver for options', + examples: ['>=10'] + } }, - required: ["name", "version"], - }, + required: ['name', 'version'] + } }, appVersions: { - type: "object", + type: 'object', description: - "A list of compatable coderoad versions. Currently only a VSCode extension.", + 'A list of compatable coderoad versions. Currently only a VSCode extension.', properties: { vscode: { - type: "string", + type: 'string', description: - "The version range for coderoad-vscode that this tutorial is compatable with", - examples: [">=0.7.0"], - }, - }, - }, + 'The version range for coderoad-vscode that this tutorial is compatable with', + examples: ['>=0.7.0'] + } + } + } }, additionalProperties: false, - required: ["testRunner", "repo"], + required: ['testRunner', 'repo'] }, // levels levels: { - type: "array", + type: 'array', description: 'Levels are the stages a user goes through in the tutorial. A level may contain a group of tasks called "steps" that must be completed to proceed', items: { - type: "object", + type: 'object', properties: { title: { - $ref: "#/definitions/title", - description: "A title for the level", + $ref: '#/definitions/title', + description: 'A title for the level' }, summary: { - type: "string", - description: "A high-level summary of the level", - maxLength: 250, + type: 'string', + description: 'A high-level summary of the level', + maxLength: 250 }, content: { - type: "string", - description: "Content for a tutorial written as Markdown", + type: 'string', + description: 'Content for a tutorial written as Markdown' }, setup: { - $ref: "#/definitions/setup_action", + $ref: '#/definitions/setup_action', description: - "An optional point for loading commits, running commands or opening files", + 'An optional point for loading commits, running commands or opening files' }, steps: { - type: "array", + type: 'array', items: { - type: "object", + type: 'object', properties: { content: { - type: "string", + type: 'string', description: - "The text displayed explaining information about the current task, written as markdown", + 'The text displayed explaining information about the current task, written as markdown' }, setup: { allOf: [ { - $ref: "#/definitions/setup_action", + $ref: '#/definitions/setup_action', description: - "A point for loading commits. It can also run commands and/or open files", + 'A point for loading commits. It can also run commands and/or open files' }, { - required: ["commits"], - }, - ], + required: ['commits'] + } + ] }, solution: { allOf: [ { - $ref: "#/definitions/setup_action", + $ref: '#/definitions/setup_action', description: - "The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files", + 'The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files' }, { - required: [], - }, - ], - }, + required: [] + } + ] + } }, hints: { - type: "array", + type: 'array', description: - "An optional array of hints to provide helpful feedback to users", + 'An optional array of hints to provide helpful feedback to users', items: { - type: "string", - description: "A hint to provide to the user", - examples: ["Have you tried doing X?"], - }, + type: 'string', + description: 'A hint to provide to the user', + examples: ['Have you tried doing X?'] + } }, - required: ["content", "setup"], - }, - }, + required: ['content', 'setup'] + } + } }, - required: ["title", "summary", "content"], - }, - }, + required: ['title', 'summary', 'content'] + } + } }, additionalProperties: false, - required: ["version", "summary", "config", "levels"], -}; + required: ['version', 'summary', 'config', 'levels'] +} diff --git a/src/utils/args.ts b/src/utils/args.ts index ae342f3..5b3e4d6 100644 --- a/src/utils/args.ts +++ b/src/utils/args.ts @@ -1,41 +1,41 @@ type ArgValueParams = { - name: string; - alias?: string; -}; + name: string + alias?: string +} -function checkValue(args: string[], string: string): string | null { - const nameIndex = args.indexOf(string); +function checkValue (args: string[], string: string): string | null { + const nameIndex = args.indexOf(string) if (nameIndex >= 0) { - const nextArg = args[nameIndex + 1]; + const nextArg = args[nameIndex + 1] if (nextArg !== undefined) { - const nextIsCommand = !!nextArg.match(/^\-/); + const nextIsCommand = !!nextArg.match(/^\-/) if (nextIsCommand) { - return null; + return null } - return nextArg; + return nextArg } else { // no secondary set value - return null; + return null } } - return null; + return null } -export function getArg( +export function getArg ( args: string[], options: ArgValueParams ): string | null { - let stringValue: null | string = null; + let stringValue: null | string = null if (options.alias) { - const aliasString = `-${options.alias}`; - stringValue = checkValue(args, aliasString); + const aliasString = `-${options.alias}` + stringValue = checkValue(args, aliasString) } if (!stringValue) { - const nameString = `--${options.name}`; - stringValue = checkValue(args, nameString); + const nameString = `--${options.name}` + stringValue = checkValue(args, nameString) } - return stringValue; + return stringValue } diff --git a/src/utils/commits.ts b/src/utils/commits.ts index 91c3dbd..948ce14 100644 --- a/src/utils/commits.ts +++ b/src/utils/commits.ts @@ -1,120 +1,120 @@ -import * as fs from "fs"; -import util from "util"; -import * as path from "path"; -import { ListLogSummary } from "simple-git/typings/response"; -import gitP, { SimpleGit } from "simple-git/promise"; -import { validateCommitOrder } from "./validateCommits"; +import * as fs from 'fs' +import util from 'util' +import * as path from 'path' +import { ListLogSummary } from 'simple-git/typings/response' +import gitP, { SimpleGit } from 'simple-git/promise' +import { validateCommitOrder } from './validateCommits' -const mkdir = util.promisify(fs.mkdir); -const exists = util.promisify(fs.exists); -const rmdir = util.promisify(fs.rmdir); +const mkdir = util.promisify(fs.mkdir) +const exists = util.promisify(fs.exists) +const rmdir = util.promisify(fs.rmdir) type GetCommitOptions = { - localDir: string; - codeBranch: string; -}; + localDir: string + codeBranch: string +} -export type CommitLogObject = { [position: string]: string[] }; +export type CommitLogObject = { [position: string]: string[] } -export function parseCommits( +export function parseCommits ( logs: ListLogSummary ): { [hash: string]: string[] } { // Filter relevant logs - const commits: CommitLogObject = {}; - const positions: string[] = []; + const commits: CommitLogObject = {} + const positions: string[] = [] for (const commit of logs.all) { const matches = commit.message.match( /^(?INIT)|(L?(?\d+)[S|\.]?(?\d+)?(?[Q|A|T|S])?)/ - ); + ) if (matches && matches.length) { // Use an object of commit arrays to collect all commits - const { groups } = matches; - let position; + const { groups } = matches + let position if (groups.init) { - position = "INIT"; + position = 'INIT' } else if (groups.levelId && groups.stepId) { - let stepType; + let stepType // @deprecated Q - if (!groups.stepType || ["Q", "T"].includes(groups.stepType)) { - stepType = "T"; // test + if (!groups.stepType || ['Q', 'T'].includes(groups.stepType)) { + stepType = 'T' // test // @deprecated A - } else if (!groups.stepType || ["A", "S"].includes(groups.stepType)) { - stepType = "S"; // solution + } else if (!groups.stepType || ['A', 'S'].includes(groups.stepType)) { + stepType = 'S' // solution } - position = `${groups.levelId}.${groups.stepId}:${stepType}`; + position = `${groups.levelId}.${groups.stepId}:${stepType}` } else if (groups.levelId) { - position = groups.levelId; + position = groups.levelId } else { - console.warn(`No matcher for commit "${commit.message}"`); + console.warn(`No matcher for commit "${commit.message}"`) } - commits[position] = [...(commits[position] || []), commit.hash]; - positions.unshift(position); + commits[position] = [...(commits[position] || []), commit.hash] + positions.unshift(position) } else { - const initMatches = commit.message.match(/^INIT/); + const initMatches = commit.message.match(/^INIT/) if (initMatches && initMatches.length) { - commits.INIT = [commit.hash, ...(commits.INIT || [])]; - positions.unshift("INIT"); + commits.INIT = [commit.hash, ...(commits.INIT || [])] + positions.unshift('INIT') } } } // validate order - validateCommitOrder(positions); - return commits; + validateCommitOrder(positions) + return commits } -export async function getCommits({ +export async function getCommits ({ localDir, - codeBranch, + codeBranch }: GetCommitOptions): Promise { - const git: SimpleGit = gitP(localDir); + const git: SimpleGit = gitP(localDir) // check that a repo is created - const isRepo = await git.checkIsRepo(); + const isRepo = await git.checkIsRepo() if (!isRepo) { - throw new Error("No git repo provided"); + throw new Error('No git repo provided') } // setup .tmp directory - const tmpDir = path.join(localDir, ".tmp"); - const tmpDirExists = await exists(tmpDir); + const tmpDir = path.join(localDir, '.tmp') + const tmpDirExists = await exists(tmpDir) if (tmpDirExists) { - await rmdir(tmpDir, { recursive: true }); + await rmdir(tmpDir, { recursive: true }) } - await mkdir(tmpDir); + await mkdir(tmpDir) - const tempGit = gitP(tmpDir); - await tempGit.clone(localDir, tmpDir); + const tempGit = gitP(tmpDir) + await tempGit.clone(localDir, tmpDir) - const branches = await git.branch(); + const branches = await git.branch() if (!branches.all.length) { - throw new Error("No branches found"); + throw new Error('No branches found') } else if (!branches.all.includes(codeBranch)) { - throw new Error(`Code branch "${codeBranch}" not found`); + throw new Error(`Code branch "${codeBranch}" not found`) } // track the original branch in case of failure - const originalBranch = branches.current; + const originalBranch = branches.current try { // Checkout the code branches - await git.checkout(codeBranch); + await git.checkout(codeBranch) // Load all logs - const logs = await git.log(); + const logs = await git.log() - const commits = parseCommits(logs); + const commits = parseCommits(logs) - return commits; + return commits } catch (e) { - console.error("Error with checkout or commit matching"); - throw new Error(e.message); + console.error('Error with checkout or commit matching') + throw new Error(e.message) } finally { // revert back to the original branch on failure - await git.checkout(originalBranch); + await git.checkout(originalBranch) // cleanup the tmp directory - await rmdir(tmpDir, { recursive: true }); + await rmdir(tmpDir, { recursive: true }) } } diff --git a/src/utils/exec.ts b/src/utils/exec.ts index 1ebc29c..f53593d 100644 --- a/src/utils/exec.ts +++ b/src/utils/exec.ts @@ -1,96 +1,96 @@ -import * as T from "../../typings/tutorial"; -import { exec as cpExec } from "child_process"; -import * as path from "path"; -import { promisify } from "util"; +import * as T from '../../typings/tutorial' +import { exec as cpExec } from 'child_process' +import * as path from 'path' +import { promisify } from 'util' -const asyncExec = promisify(cpExec); +const asyncExec = promisify(cpExec) -export function createExec(cwd: string) { - return async function exec( +export function createExec (cwd: string) { + return async function exec ( command: string ): Promise<{ stdout: string | null; stderr: string }> { try { - const result = await asyncExec(command, { cwd }); - return result; + const result = await asyncExec(command, { cwd }) + return result } catch (e) { - return { stdout: null, stderr: e.message }; + return { stdout: null, stderr: e.message } } - }; + } } -export function createCherryPick(cwd: string) { - return async function cherryPick(commits: string[]): Promise { +export function createCherryPick (cwd: string) { + return async function cherryPick (commits: string[]): Promise { for (const commit of commits) { try { const { stdout, stderr } = await createExec(cwd)( `git cherry-pick -X theirs ${commit}` - ); + ) if (stderr) { - console.warn(stderr); + console.warn(stderr) } if (!stdout) { - console.warn(`No cherry-pick output for ${commit}`); + console.warn(`No cherry-pick output for ${commit}`) } } catch (e) { - console.warn(`Cherry-pick failed for ${commit}`); - console.error(e.message); + console.warn(`Cherry-pick failed for ${commit}`) + console.error(e.message) } } - }; + } } -export function createCommandRunner(cwd: string) { - return async function runCommands( +export function createCommandRunner (cwd: string) { + return async function runCommands ( commands: string[], dir?: string ): Promise { - let errors = []; + let errors = [] for (const command of commands) { try { - console.log(`--> ${command}`); - let cwdDir = cwd; + console.log(`--> ${command}`) + let cwdDir = cwd if (dir) { - cwdDir = path.join(cwd, dir); + cwdDir = path.join(cwd, dir) } - const { stdout, stderr } = await createExec(cwdDir)(command); + const { stdout, stderr } = await createExec(cwdDir)(command) - console.log(stdout); - console.warn(stderr); + console.log(stdout) + console.warn(stderr) } catch (e) { - console.error(`Command failed: "${command}"`); - console.warn(e.message); - errors.push(e.message); + console.error(`Command failed: "${command}"`) + console.warn(e.message) + errors.push(e.message) } } - return !!errors.length; - }; + return !!errors.length + } } // function isAbsolute(p: string) { // return path.normalize(p + "/") === path.normalize(path.resolve(p) + "/"); // } -export function createTestRunner(cwd: string, config: T.TestRunnerConfig) { - const { command, args, directory } = config; +export function createTestRunner (cwd: string, config: T.TestRunnerConfig) { + const { command, args, directory } = config // const commandIsAbsolute = isAbsolute(command); - let wd = cwd; + let wd = cwd if (directory) { - wd = path.join(cwd, directory); + wd = path.join(cwd, directory) } - const commandWithArgs = `${command} ${args.tap}`; + const commandWithArgs = `${command} ${args.tap}` - return async function runTest(): Promise<{ - stdout: string | null; - stderr: string | null; + return async function runTest (): Promise<{ + stdout: string | null + stderr: string | null }> { try { // console.log(await createExec(wd)("ls -a node_modules/.bin")); - return await createExec(wd)(commandWithArgs); + return await createExec(wd)(commandWithArgs) } catch (e) { - return Promise.resolve({ stdout: null, stderr: e.message }); + return Promise.resolve({ stdout: null, stderr: e.message }) } - }; + } } diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 42b44f4..b0889fb 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -1,21 +1,21 @@ -import { red, yellow, blue } from "kleur"; +import { red, yellow, blue } from 'kleur' -const _error = console.error; -const _warn = console.warn; -const _info = console.info; +const _error = console.error +const _warn = console.warn +const _info = console.info // const log = console.log; console.error = function () { // @ts-ignore - _error(red.apply(console, arguments)); -}; + _error(red.apply(console, arguments)) +} console.warn = function () { // @ts-ignore - _warn(yellow.apply(console, arguments)); -}; + _warn(yellow.apply(console, arguments)) +} console.info = function () { // @ts-ignore - _info(blue.apply(console, arguments)); -}; + _info(blue.apply(console, arguments)) +} diff --git a/src/utils/parse.ts b/src/utils/parse.ts index b912e48..dfdbc4d 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -1,11 +1,11 @@ -import { truncate } from "lodash"; -import { CommitLogObject } from "./commits"; -import * as T from "../../typings/tutorial"; +import { truncate } from 'lodash' +import { CommitLogObject } from './commits' +import * as T from '../../typings/tutorial' type TutorialFrame = { - summary: T.TutorialSummary; - levels: T.Level[]; -}; + summary: T.TutorialSummary + levels: T.Level[] +} const R = { summary: /^#\s(?.*)[\n\r]+(?[^]*)/, @@ -13,59 +13,59 @@ const R = { step: /^(#{3}\s(?.*)[\n\r]+(?[^]*))/, hints: /^(#{4}\sHINTS[\n\r]+([\*|\-]\s(?[^]*))[\n\r]+)+/, subtasks: /^(#{4}\sSUBTASKS[\n\r]+([\*|\-]\s(?[^]*))[\n\r]+)+/, - listItem: /[\n\r]+[\*|\-]\s/, -}; + listItem: /[\n\r]+[\*|\-]\s/ +} -export function parseMdContent(md: string): TutorialFrame | never { - let start: number = -1; - const parts: any[] = []; +export function parseMdContent (md: string): TutorialFrame | never { + let start: number = -1 + const parts: any[] = [] - const lines = md.split("\n"); + const lines = md.split('\n') - // Split the multiple parts - This way enables the creator to use 4/5 level headers inside the content. + // Split the header based sections lines.forEach((line, index) => { - if (line.match(/#{1,3}\s/) || index === lines.length - 1) { + if (line.match(/^#{1,5}\s/) || index === lines.length - 1) { if (start !== -1) { - parts.push(lines.slice(start, index).join("\n")); - start = index; + parts.push(lines.slice(start, index).join('\n')) + start = index } else { - start = index; + start = index } } - }); + }) const mdContent: TutorialFrame = { summary: { - title: "", - description: "", + title: '', + description: '' }, - levels: [], - }; + levels: [] + } // Capture summary - const summaryMatch = parts.shift().match(R.summary); + const summaryMatch = parts.shift().match(R.summary) + if (summaryMatch.groups.tutorialTitle) { - mdContent.summary.title = summaryMatch.groups.tutorialTitle.trim(); + mdContent.summary.title = summaryMatch.groups.tutorialTitle.trim() } if (summaryMatch.groups.tutorialDescription) { - mdContent.summary.description = summaryMatch.groups.tutorialDescription.trim(); + mdContent.summary.description = summaryMatch.groups.tutorialDescription.trim() } - let current = { levelId: "", levelIndex: -1, stepIndex: -1 }; + let current = { levelId: '', levelIndex: -1, stepIndex: -1 } // Identify each part of the content parts.forEach((section: string) => { // match level - const levelMatch: RegExpMatchArray | null = section.match(R.level); - + const levelMatch: RegExpMatchArray | null = section.match(R.level) if (levelMatch && levelMatch.groups) { - const levelId = levelMatch.groups.levelId.replace(".", ""); + const levelId = levelMatch.groups.levelId.replace('.', '') current = { levelId: levelId, levelIndex: current.levelIndex + 1, - stepIndex: -1, - }; - const { levelTitle, levelSummary, levelContent } = levelMatch.groups; + stepIndex: -1 + } + const { levelTitle, levelSummary, levelContent } = levelMatch.groups // @ts-ignore mdContent.levels[current.levelIndex] = { @@ -76,28 +76,28 @@ export function parseMdContent(md: string): TutorialFrame | never { ? levelSummary.trim() : truncate(levelContent.split(/[\n\r]+/)[0].trim(), { length: 80, - omission: "...", + omission: '...' }), content: levelContent.trim(), - steps: [], - }; + steps: [] + } } else { // match step - const stepMatch: RegExpMatchArray | null = section.match(R.step); + const stepMatch: RegExpMatchArray | null = section.match(R.step) if (stepMatch && stepMatch.groups) { current = { levelId: current.levelId, levelIndex: current.levelIndex, - stepIndex: current.stepIndex + 1, - }; - const { stepId, stepContent } = stepMatch.groups; + stepIndex: current.stepIndex + 1 + } + const { stepId, stepContent } = stepMatch.groups mdContent.levels[current.levelIndex].steps[current.stepIndex] = { id: `${current.levelId}.${current.stepIndex + 1}`, - content: stepContent.trim(), - }; + content: stepContent.trim() + } } else { - const hintMatch = section.match(R.hints); - const subtaskMatch = section.match(R.subtasks); + const hintMatch = section.match(R.hints) + const subtaskMatch = section.match(R.subtasks) switch (true) { // parse hints from stepContent @@ -105,58 +105,58 @@ export function parseMdContent(md: string): TutorialFrame | never { const hints = section .split(R.listItem) .slice(1) // remove #### HINTS - .map((h) => h.trim()); + .map(h => h.trim()) if (hints.length) { mdContent.levels[current.levelIndex].steps[ current.stepIndex - ].hints = hints; + ].hints = hints } - return; + return // parse subtasks from stepContent case !!subtaskMatch: const subtasks = section .split(R.listItem) .slice(1) // remove #### SUBTASKS - .map((h) => h.trim()); + .map(h => h.trim()) if (subtasks.length) { mdContent.levels[current.levelIndex].steps[ current.stepIndex - ].subtasks = subtasks; + ].subtasks = subtasks } - return; + return default: - console.warn(`No build parser match found for:\n${section}\n`); + console.warn(`No build parser match found for:\n${section}\n`) } } } - }); + }) - return mdContent; + return mdContent } type ParseParams = { - text: string; - skeleton: Partial; - commits: CommitLogObject; -}; + text: string + skeleton: Partial + commits: CommitLogObject +} -export function parse(params: ParseParams): any { - const mdContent: TutorialFrame = parseMdContent(params.text + "\n\n"); +export function parse (params: ParseParams): any { + const mdContent: TutorialFrame = parseMdContent(params.text + '\n\n') const parsed: Partial = { version: params.skeleton.version, summary: mdContent.summary, config: params.skeleton.config || {}, - levels: [], - }; + levels: [] + } // add init commits if (params.commits.INIT && params.commits.INIT.length) { // @ts-ignore parsed.config.setup = { ...(parsed.config?.setup || {}), - commits: params.commits.INIT, - }; + commits: params.commits.INIT + } } // merge content levels and tutorial @@ -164,75 +164,75 @@ export function parse(params: ParseParams): any { parsed.levels = mdContent.levels.map( (mdLevel: T.Level, mdLevelIndex: number) => { // add level setup commits - let level: T.Level = { ...mdLevel }; + let level: T.Level = { ...mdLevel } - const configLevel = params.skeleton.levels[mdLevelIndex]; + const configLevel = params.skeleton.levels[mdLevelIndex] if (configLevel) { // add level step commits - const { steps, ...configLevelProps } = configLevel; - level = { ...configLevelProps, ...level }; + const { steps, ...configLevelProps } = configLevel + level = { ...configLevelProps, ...level } if (steps) { steps.forEach((step: T.Step, index: number) => { try { - const mdStep = level.steps[index]; + const mdStep = level.steps[index] step = { ...step, - ...mdStep, - }; + ...mdStep + } - const stepSetupKey = `${step.id}:T`; + const stepSetupKey = `${step.id}:T` if (!step?.setup) { - step.setup = {}; + step.setup = {} } if (!step.setup.commits) { - step.setup.commits = []; + step.setup.commits = [] } if (params.commits[stepSetupKey]) { - step.setup.commits = params.commits[stepSetupKey]; + step.setup.commits = params.commits[stepSetupKey] } - const stepSolutionKey = `${step.id}:S`; + const stepSolutionKey = `${step.id}:S` if (params.commits[stepSolutionKey]) { if (!step.solution) { step.solution = { - commits: [], - }; + commits: [] + } } - step.solution.commits = params.commits[stepSolutionKey]; + step.solution.commits = params.commits[stepSolutionKey] } } catch (error) { - console.error("Error parsing level steps"); - console.warn(JSON.stringify(level.steps)); - console.error(error.message); + console.error('Error parsing level steps') + console.warn(JSON.stringify(level.steps)) + console.error(error.message) } // update level step - level.steps[index] = step; - }); + level.steps[index] = step + }) } } if (params.commits[level.id]) { if (!level.setup) { - level.setup = {}; + level.setup = {} } - level.setup.commits = params.commits[level.id]; + level.setup.commits = params.commits[level.id] } // @deprecated L1 system if (params.commits[`L${level.id}`]) { if (!level.setup) { - level.setup = {}; + level.setup = {} } - level.setup.commits = params.commits[`L${level.id}`]; + level.setup.commits = params.commits[`L${level.id}`] } - return level; + return level } - ); + ) - return parsed; + return parsed } diff --git a/src/utils/validateCommits.ts b/src/utils/validateCommits.ts index f4d8890..7928ed8 100644 --- a/src/utils/validateCommits.ts +++ b/src/utils/validateCommits.ts @@ -1,81 +1,81 @@ // should flag commits that are out of order based on the previous commit // position is a string like 'INIT', 'L1', 'L1S1' -export function validateCommitOrder(positions: string[]): boolean { +export function validateCommitOrder (positions: string[]): boolean { // loop over positions - const errors: number[] = []; - let previous = { level: 0, step: 0, type: "" }; - let current = { level: 0, step: 0, type: "" }; + const errors: number[] = [] + let previous = { level: 0, step: 0, type: '' } + let current = { level: 0, step: 0, type: '' } positions.forEach((position: string, index: number) => { - if (position === "INIT") { + if (position === 'INIT') { if (previous.level !== 0 && previous.step !== 0) { - errors.push(index); + errors.push(index) } - current = { level: 0, step: 0, type: "" }; - return; + current = { level: 0, step: 0, type: '' } + return } else { // @deprecate - remove L|Q - const levelMatch = position.match(/^(?[0-9]+)$/); + const levelMatch = position.match(/^(?[0-9]+)$/) // @deprecate - remove S|Q|A const stepMatch = position.match( /^(?[0-9]+)\.(?[0-9]+):(?[T|S])$/ - ); + ) if (levelMatch) { // allows next level or step - const levelString = levelMatch?.groups?.level; + const levelString = levelMatch?.groups?.level if (!levelString) { - console.warn(`No commit level match for ${position}`); - return; + console.warn(`No commit level match for ${position}`) + return } - const level = Number(levelString); - current = { level, step: 0, type: "" }; + const level = Number(levelString) + current = { level, step: 0, type: '' } } else if (stepMatch) { // allows next level or step if (!stepMatch?.groups?.level || !stepMatch?.groups.step) { - console.warn(`No commit step match for ${position}`); - return; + console.warn(`No commit step match for ${position}`) + return } - const { level: levelString, step: stepString } = stepMatch.groups; + const { level: levelString, step: stepString } = stepMatch.groups - const level = Number(levelString); - const step = Number(stepString); - const type = stepMatch?.groups.stepType; + const level = Number(levelString) + const step = Number(stepString) + const type = stepMatch?.groups.stepType - const sameStep = previous.level === level && previous.step === step; + const sameStep = previous.level === level && previous.step === step if ( // tests should come before the solution - (sameStep && type === "T" && previous.type === "S") || + (sameStep && type === 'T' && previous.type === 'S') || // step should have tests - (!sameStep && type === "S") + (!sameStep && type === 'S') ) { - errors.push(index); + errors.push(index) } - current = { level, step, type }; + current = { level, step, type } } else { // error - console.warn(`Invalid commit position: ${position}`); - return; + console.warn(`Invalid commit position: ${position}`) + return } if ( // levels or steps are out of order current.level < previous.level || (current.level === previous.level && current.step < previous.step) ) { - errors.push(index); + errors.push(index) } } - previous = current; - }); + previous = current + }) - if (errors.length && process.env.NODE_ENV !== "test") { - console.warn("Found commit positions out of order"); + if (errors.length && process.env.NODE_ENV !== 'test') { + console.warn('Found commit positions out of order') positions.forEach((position, index) => { if (errors.includes(index)) { - console.warn(`${position} <-`); + console.warn(`${position} <-`) } else { - console.log(position); + console.log(position) } - }); + }) } - return !errors.length; + return !errors.length } diff --git a/src/utils/validateMarkdown.ts b/src/utils/validateMarkdown.ts index 6703786..3f20863 100644 --- a/src/utils/validateMarkdown.ts +++ b/src/utils/validateMarkdown.ts @@ -1,71 +1,71 @@ type Validation = { - message: string; - validate: (t: string) => boolean; -}; + message: string + validate: (t: string) => boolean +} const validations: Validation[] = [ { - message: "should start with a title", - validate: (t) => !!t.match(/^#\s.+/), + message: 'should start with a title', + validate: t => !!t.match(/^#\s.+/) }, { - message: "should not have multiple `#` headers", - validate: (t) => !t.match(/[\n\r]#\s/), + message: 'should not have multiple `#` headers', + validate: t => !t.match(/[\n\r]#\s/) }, { - message: "should have a summary description under the title", - validate: (t) => { - const [summary] = t.split(/[\n\r]##/) || [""]; + message: 'should have a summary description under the title', + validate: t => { + const [summary] = t.split(/[\n\r]##/) || [''] const description = summary .split(/\n/) .slice(1) - .filter((l) => l.length); - return !!description.length; - }, + .filter(l => l.length) + return !!description.length + } }, { - message: "should have a level `##` with a format of `[0-9]+.`", - validate: (t) => { - const headers = t.match(/^#{2}\s(.+)$/gm) || []; + message: 'should have a level `##` with a format of `[0-9]+.`', + validate: t => { + const headers = t.match(/^#{2}\s(.+)$/gm) || [] for (const header of headers) { if (!header.match(/^#{2}\s(\d+\.)\s(.+)$/)) { - return false; + return false } } - return true; - }, + return true + } }, { - message: "should have a step `###` with a format of `[0-9].[0-9]+`", - validate: (t) => { - const headers = t.match(/^#{3}\s(.+)$/gm) || []; + message: 'should have a step `###` with a format of `[0-9].[0-9]+`', + validate: t => { + const headers = t.match(/^#{3}\s(.+)$/gm) || [] for (const header of headers) { if (!header.match(/^#{3}\s(\d+\.\d+)/)) { - return false; + return false } } - return true; - }, - }, -]; + return true + } + } +] -const codeBlockRegex = /```[a-z]*\n[\s\S]*?\n```/gm; +const codeBlockRegex = /```[a-z]*\n[\s\S]*?\n```/gm -export function validateMarkdown(md: string): boolean { +export function validateMarkdown (md: string): boolean { // remove codeblocks which might contain any valid combinations // trim white space - const text = md.replace(codeBlockRegex, "").trim(); + const text = md.replace(codeBlockRegex, '').trim() - let valid = true; + let valid = true for (const v of validations) { if (!v.validate(text)) { - valid = false; - if (process.env.NODE_ENV !== "test") { - console.warn(v.message); + valid = false + if (process.env.NODE_ENV !== 'test') { + console.warn(v.message) } } } - return valid; + return valid } diff --git a/src/utils/validateSchema.ts b/src/utils/validateSchema.ts index aeca139..1e4952a 100644 --- a/src/utils/validateSchema.ts +++ b/src/utils/validateSchema.ts @@ -1,30 +1,28 @@ // https://www.npmjs.com/package/ajv // @ts-ignore ajv typings not working -import JsonSchema from "ajv"; +import JsonSchema from 'ajv' -export function validateSchema( +export function validateSchema ( schema: any, json: any ): boolean | PromiseLike { // validate using https://json-schema.org/ const jsonSchema = new JsonSchema({ - allErrors: true, + allErrors: true // verbose: true, - }); + }) - const valid = jsonSchema.validate(schema, json); + const valid = jsonSchema.validate(schema, json) if (!valid) { // log errors /* istanbul ignore next */ - if (process.env.NODE_ENV !== "test") { + if (process.env.NODE_ENV !== 'test') { jsonSchema.errors?.forEach((error: JsonSchema.ErrorObject) => { - console.warn( - `Validation error at ${error.dataPath} - ${error.message}` - ); - }); + console.warn(`Validation error at ${error.dataPath} - ${error.message}`) + }) } } - return valid; + return valid } diff --git a/src/validate.ts b/src/validate.ts index 9413cf3..291acfd 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -1,172 +1,172 @@ -import * as path from "path"; -import * as fs from "fs-extra"; -import * as yamlParser from "js-yaml"; -import { getArg } from "./utils/args"; -import gitP, { SimpleGit } from "simple-git/promise"; +import * as path from 'path' +import * as fs from 'fs-extra' +import * as yamlParser from 'js-yaml' +import { getArg } from './utils/args' +import gitP, { SimpleGit } from 'simple-git/promise' import { createCommandRunner, createCherryPick, - createTestRunner, -} from "./utils/exec"; -import { getCommits, CommitLogObject } from "./utils/commits"; + createTestRunner +} from './utils/exec' +import { getCommits, CommitLogObject } from './utils/commits' interface Options { - yaml: string; - clean: boolean; + yaml: string + clean: boolean } -async function validate(args: string[]) { +async function validate (args: string[]) { // dir - default . - const dir = !args.length || args[0].match(/^-/) ? "." : args[0]; - const localDir = path.join(process.cwd(), dir); + const dir = !args.length || args[0].match(/^-/) ? '.' : args[0] + const localDir = path.join(process.cwd(), dir) // -y --yaml - default coderoad-config.yml const options: Options = { - yaml: getArg(args, { name: "yaml", alias: "y" }) || "coderoad.yaml", - clean: getArg(args, { name: "clean", alias: "c" }) !== "false", - }; + yaml: getArg(args, { name: 'yaml', alias: 'y' }) || 'coderoad.yaml', + clean: getArg(args, { name: 'clean', alias: 'c' }) !== 'false' + } const _yaml: string = await fs.readFile( path.join(localDir, options.yaml), - "utf8" - ); + 'utf8' + ) // parse yaml config - let skeleton; + let skeleton try { - skeleton = yamlParser.load(_yaml); + skeleton = yamlParser.load(_yaml) if (!skeleton) { - throw new Error("Invalid yaml file contents"); + throw new Error('Invalid yaml file contents') } } catch (e) { - console.error("Error parsing yaml"); - console.error(e.message); + console.error('Error parsing yaml') + console.error(e.message) } - const codeBranch: string = skeleton.config.repo.branch; + const codeBranch: string = skeleton.config.repo.branch // validate commits - const commits: CommitLogObject = await getCommits({ localDir, codeBranch }); + const commits: CommitLogObject = await getCommits({ localDir, codeBranch }) // setup tmp dir - const tmpDir = path.join(localDir, ".tmp"); + const tmpDir = path.join(localDir, '.tmp') try { if (!(await fs.pathExists(tmpDir))) { - await fs.emptyDir(tmpDir); + await fs.emptyDir(tmpDir) } - const tempGit: SimpleGit = gitP(tmpDir); + const tempGit: SimpleGit = gitP(tmpDir) - await tempGit.init(); - await tempGit.addRemote("origin", skeleton.config.repo.uri); - await tempGit.fetch("origin", skeleton.config.repo.branch); + await tempGit.init() + await tempGit.addRemote('origin', skeleton.config.repo.uri) + await tempGit.fetch('origin', skeleton.config.repo.branch) // no js cherry pick implementation - const cherryPick = createCherryPick(tmpDir); - const runCommands = createCommandRunner(tmpDir); - const runTest = createTestRunner(tmpDir, skeleton.config.testRunner); + const cherryPick = createCherryPick(tmpDir) + const runCommands = createCommandRunner(tmpDir) + const runTest = createTestRunner(tmpDir, skeleton.config.testRunner) // setup - console.info("* Setup"); + console.info('* Setup') if (commits.INIT) { // load commits - console.info("-- Loading commits..."); - await cherryPick(commits.INIT); + console.info('-- Loading commits...') + await cherryPick(commits.INIT) // run commands if (skeleton.config?.setup?.commands) { - console.info("-- Running commands..."); + console.info('-- Running commands...') - await runCommands(skeleton.config?.setup?.commands); + await runCommands(skeleton.config?.setup?.commands) } } for (const level of skeleton.levels) { - console.info(`* ${level.id}`); + console.info(`* ${level.id}`) if (level?.setup) { // load commits if (commits[`${level.id}`]) { - console.log(`-- Loading commits...`); - await cherryPick(commits[level.id]); + console.log(`-- Loading commits...`) + await cherryPick(commits[level.id]) } // run commands if (level.setup?.commands) { - console.log(`-- Running commands...`); - await runCommands(level.setup.commands); + console.log(`-- Running commands...`) + await runCommands(level.setup.commands) } } // steps if (level.steps) { for (const step of level.steps) { - console.info(`** ${step.id}`); + console.info(`** ${step.id}`) // load commits - const stepSetupCommits = commits[`${step.id}:T`]; + const stepSetupCommits = commits[`${step.id}:T`] if (stepSetupCommits) { - console.info(`--- Loading setup commits...`); - await cherryPick(stepSetupCommits); + console.info(`--- Loading setup commits...`) + await cherryPick(stepSetupCommits) } // run commands if (step.setup.commands) { - console.info(`--- Running setup commands...`); - await runCommands(step.setup.commands); + console.info(`--- Running setup commands...`) + await runCommands(step.setup.commands) } - const stepSolutionCommits = commits[`${step.id}:S`]; - const hasSolution = step.solution || stepSolutionCommits; + const stepSolutionCommits = commits[`${step.id}:S`] + const hasSolution = step.solution || stepSolutionCommits // ignore running tests on steps with no solution if (hasSolution) { // run test - console.info("--- Running setup test..."); + console.info('--- Running setup test...') // expect fail - const { stdout, stderr } = await runTest(); + const { stdout, stderr } = await runTest() if (stdout) { console.error( `--- Expected ${step.id} setup tests to fail, but passed` - ); + ) // log tests - console.log(stdout); + console.log(stdout) } } if (stepSolutionCommits) { - console.info(`--- Loading solution commits...`); - await cherryPick(stepSolutionCommits); + console.info(`--- Loading solution commits...`) + await cherryPick(stepSolutionCommits) } // run commands if (step?.solution?.commands) { - console.info(`--- Running solution commands...`); - await runCommands(step.solution.commands); + console.info(`--- Running solution commands...`) + await runCommands(step.solution.commands) } if (hasSolution) { // run test - console.info("--- Running solution test..."); + console.info('--- Running solution test...') // expect pass - const { stdout, stderr } = await runTest(); + const { stdout, stderr } = await runTest() if (stderr) { console.error( `--- Expected ${step.id} solution tests to pass, but failed` - ); + ) // log tests - console.log(stderr); + console.log(stderr) } } } } } - console.info(`\n✔ Success!`); + console.info(`\n✔ Success!`) } catch (e) { - console.error("\n✘ Fail!"); - console.error(e.message); + console.error('\n✘ Fail!') + console.error(e.message) } finally { // cleanup if (options.clean) { - await fs.emptyDir(tmpDir); + await fs.emptyDir(tmpDir) } } } -export default validate; +export default validate diff --git a/tests/args.test.ts b/tests/args.test.ts index 746b3b1..e1fff60 100644 --- a/tests/args.test.ts +++ b/tests/args.test.ts @@ -1,64 +1,64 @@ -import { getArg } from "../src/utils/args"; +import { getArg } from '../src/utils/args' -describe("args", () => { - it("should capture an arg name from text", () => { - const args = ["--name", "value"]; - const result = getArg(args, { name: "name" }); - expect(result).toBe("value"); - }); - it("should capture an arg alias from text", () => { - const args = ["-n", "value"]; - const result = getArg(args, { name: "name", alias: "n" }); - expect(result).toBe("value"); - }); - it("should capture an arg name from text when starting values", () => { - const args = ["dir", "--name", "value"]; - const result = getArg(args, { name: "name" }); - expect(result).toBe("value"); - }); - it("should capture an arg alias from text", () => { - const args = ["dir", "-n", "value"]; - const result = getArg(args, { name: "name", alias: "n" }); - expect(result).toBe("value"); - }); - it("should convert bool string to true", () => { - const args = ["--someBool", "true"]; +describe('args', () => { + it('should capture an arg name from text', () => { + const args = ['--name', 'value'] + const result = getArg(args, { name: 'name' }) + expect(result).toBe('value') + }) + it('should capture an arg alias from text', () => { + const args = ['-n', 'value'] + const result = getArg(args, { name: 'name', alias: 'n' }) + expect(result).toBe('value') + }) + it('should capture an arg name from text when starting values', () => { + const args = ['dir', '--name', 'value'] + const result = getArg(args, { name: 'name' }) + expect(result).toBe('value') + }) + it('should capture an arg alias from text', () => { + const args = ['dir', '-n', 'value'] + const result = getArg(args, { name: 'name', alias: 'n' }) + expect(result).toBe('value') + }) + it('should convert bool string to true', () => { + const args = ['--someBool', 'true'] const result = getArg(args, { - name: "someBool", - alias: "sb", - }); - expect(result).toBe("true"); - }); - it("should convert bool string to false", () => { - const args = ["--someBool", "false"]; + name: 'someBool', + alias: 'sb' + }) + expect(result).toBe('true') + }) + it('should convert bool string to false', () => { + const args = ['--someBool', 'false'] const result = getArg(args, { - name: "someBool", - alias: "sb", - }); - expect(result).toBe("false"); - }); - it("should default value to true if no next value", () => { - const args = ["--someBool"]; + name: 'someBool', + alias: 'sb' + }) + expect(result).toBe('false') + }) + it('should default value to true if no next value', () => { + const args = ['--someBool'] const result = getArg(args, { - name: "someBool", - alias: "sb", - }); - expect(result).toBe(null); - }); - it("should default value to true if next value is --name", () => { - const args = ["--someBool", "--someOtherBool"]; + name: 'someBool', + alias: 'sb' + }) + expect(result).toBe(null) + }) + it('should default value to true if next value is --name', () => { + const args = ['--someBool', '--someOtherBool'] const result = getArg(args, { - name: "someBool", - alias: "sb", - }); - expect(result).toBe(null); - }); - it("should default value to true if next value is -alias", () => { - const args = ["--someBool", "-a"]; + name: 'someBool', + alias: 'sb' + }) + expect(result).toBe(null) + }) + it('should default value to true if next value is -alias', () => { + const args = ['--someBool', '-a'] const result = getArg(args, { - name: "someBool", - alias: "sb", - }); - expect(result).toBe(null); - }); -}); + name: 'someBool', + alias: 'sb' + }) + expect(result).toBe(null) + }) +}) diff --git a/tests/commitOrder.test.ts b/tests/commitOrder.test.ts index 640b25f..3599e09 100644 --- a/tests/commitOrder.test.ts +++ b/tests/commitOrder.test.ts @@ -1,61 +1,61 @@ -import { validateCommitOrder } from "../src/utils/validateCommits"; +import { validateCommitOrder } from '../src/utils/validateCommits' -describe("commitOrder", () => { - describe("#.# format", () => { - it("should return true if order is valid", () => { - const positions = ["INIT", "1", "1.1:T", "1.2:T", "2", "2.1:T"]; - const result = validateCommitOrder(positions); - expect(result).toBe(true); - }); - it("should return true if valid with duplicates", () => { +describe('commitOrder', () => { + describe('#.# format', () => { + it('should return true if order is valid', () => { + const positions = ['INIT', '1', '1.1:T', '1.2:T', '2', '2.1:T'] + const result = validateCommitOrder(positions) + expect(result).toBe(true) + }) + it('should return true if valid with duplicates', () => { const positions = [ - "INIT", - "INIT", - "1", - "1", - "1.1:T", - "1.1:T", - "1.1:S", - "1.1:S", - "1.2:T", - "1.2:S", - "2", - "2", - "2.1:T", - "2.1:S", - ]; - const result = validateCommitOrder(positions); - expect(result).toBe(true); - }); - it("should return false if INIT is out of order", () => { - const positions = ["INIT", "1", "1.1:T", "1.2:T", "INIT", "2", "2.1:T"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if level after step is out of order", () => { - const positions = ["INIT", "1", "1.1:T", "1.2:T", "2.1:T", "2"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if level is out of order", () => { - const positions = ["INIT", "1", "3", "2"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if step is out of order", () => { - const positions = ["INIT", "1", "1.1:T", "1.3:T", "1.2:T"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if solution is before step", () => { - const positions = ["INIT", "1", "1.1:S", "1.1:T", "1.2:T"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if solution but no test step", () => { - const positions = ["INIT", "1", "1.1:S", "1.2:T"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - }); -}); + 'INIT', + 'INIT', + '1', + '1', + '1.1:T', + '1.1:T', + '1.1:S', + '1.1:S', + '1.2:T', + '1.2:S', + '2', + '2', + '2.1:T', + '2.1:S' + ] + const result = validateCommitOrder(positions) + expect(result).toBe(true) + }) + it('should return false if INIT is out of order', () => { + const positions = ['INIT', '1', '1.1:T', '1.2:T', 'INIT', '2', '2.1:T'] + const result = validateCommitOrder(positions) + expect(result).toBe(false) + }) + it('should return false if level after step is out of order', () => { + const positions = ['INIT', '1', '1.1:T', '1.2:T', '2.1:T', '2'] + const result = validateCommitOrder(positions) + expect(result).toBe(false) + }) + it('should return false if level is out of order', () => { + const positions = ['INIT', '1', '3', '2'] + const result = validateCommitOrder(positions) + expect(result).toBe(false) + }) + it('should return false if step is out of order', () => { + const positions = ['INIT', '1', '1.1:T', '1.3:T', '1.2:T'] + const result = validateCommitOrder(positions) + expect(result).toBe(false) + }) + it('should return false if solution is before step', () => { + const positions = ['INIT', '1', '1.1:S', '1.1:T', '1.2:T'] + const result = validateCommitOrder(positions) + expect(result).toBe(false) + }) + it('should return false if solution but no test step', () => { + const positions = ['INIT', '1', '1.1:S', '1.2:T'] + const result = validateCommitOrder(positions) + expect(result).toBe(false) + }) + }) +}) diff --git a/tests/commitParse.test.ts b/tests/commitParse.test.ts index 00e1548..2343eb3 100644 --- a/tests/commitParse.test.ts +++ b/tests/commitParse.test.ts @@ -1,151 +1,151 @@ -import { parseCommits } from "../src/utils/commits"; +import { parseCommits } from '../src/utils/commits' -describe("commitParse", () => { - it("should parse out #. commits", () => { +describe('commitParse', () => { + it('should parse out #. commits', () => { const logs = { all: [ { - message: "INIT", - hash: "1", + message: 'INIT', + hash: '1' }, { - message: "1. First Level", - hash: "2", + message: '1. First Level', + hash: '2' }, { - message: "1.1 First Step", - hash: "3", - }, + message: '1.1 First Step', + hash: '3' + } ], total: 2, - latest: {}, - }; - const commits = parseCommits(logs); + latest: {} + } + const commits = parseCommits(logs) expect(commits).toEqual({ - INIT: ["1"], - "1": ["2"], - "1.1:T": ["3"], - }); - }); + INIT: ['1'], + '1': ['2'], + '1.1:T': ['3'] + }) + }) // @deprecated - remove L# - it("should parse out L# commits", () => { + it('should parse out L# commits', () => { const logs = { all: [ { - message: "INIT", - hash: "1", + message: 'INIT', + hash: '1' }, { - message: "L1 First Level", - hash: "2", + message: 'L1 First Level', + hash: '2' }, { - message: "L1S1 First Step", - hash: "3", - }, + message: 'L1S1 First Step', + hash: '3' + } ], total: 2, - latest: {}, - }; - const commits = parseCommits(logs); + latest: {} + } + const commits = parseCommits(logs) expect(commits).toEqual({ - INIT: ["1"], - "1": ["2"], - "1.1:T": ["3"], - }); - }); + INIT: ['1'], + '1': ['2'], + '1.1:T': ['3'] + }) + }) // @deprecated - remove with QA - it("should parse out #.Q|A commits", () => { + it('should parse out #.Q|A commits', () => { const logs = { all: [ { - message: "INIT", - hash: "1", + message: 'INIT', + hash: '1' }, { - message: "1. First Level", - hash: "2", + message: '1. First Level', + hash: '2' }, { - message: "1.1Q First Step", - hash: "3", + message: '1.1Q First Step', + hash: '3' }, { - message: "1.1A First Step Solution", - hash: "4", - }, + message: '1.1A First Step Solution', + hash: '4' + } ], total: 2, - latest: {}, - }; - const commits = parseCommits(logs); + latest: {} + } + const commits = parseCommits(logs) expect(commits).toEqual({ - INIT: ["1"], - "1": ["2"], - "1.1:T": ["3"], - "1.1:S": ["4"], - }); - }); - it("should parse out #.T|S commits", () => { + INIT: ['1'], + '1': ['2'], + '1.1:T': ['3'], + '1.1:S': ['4'] + }) + }) + it('should parse out #.T|S commits', () => { const logs = { all: [ { - message: "INIT", - hash: "1", + message: 'INIT', + hash: '1' }, { - message: "1. First Level", - hash: "2", + message: '1. First Level', + hash: '2' }, { - message: "1.1T First Step", - hash: "3", + message: '1.1T First Step', + hash: '3' }, { - message: "1.1S First Step Solution", - hash: "4", - }, + message: '1.1S First Step Solution', + hash: '4' + } ], total: 2, - latest: {}, - }; - const commits = parseCommits(logs); + latest: {} + } + const commits = parseCommits(logs) expect(commits).toEqual({ - INIT: ["1"], - "1": ["2"], - "1.1:T": ["3"], - "1.1:S": ["4"], - }); - }); - it("should parse out #._|S commits", () => { + INIT: ['1'], + '1': ['2'], + '1.1:T': ['3'], + '1.1:S': ['4'] + }) + }) + it('should parse out #._|S commits', () => { const logs = { all: [ { - message: "INIT", - hash: "1", + message: 'INIT', + hash: '1' }, { - message: "1. First Level", - hash: "2", + message: '1. First Level', + hash: '2' }, { - message: "1.1 First Step", - hash: "3", + message: '1.1 First Step', + hash: '3' }, { - message: "1.1S First Step Solution", - hash: "4", - }, + message: '1.1S First Step Solution', + hash: '4' + } ], total: 2, - latest: {}, - }; - const commits = parseCommits(logs); + latest: {} + } + const commits = parseCommits(logs) expect(commits).toEqual({ - INIT: ["1"], - "1": ["2"], - "1.1:T": ["3"], - "1.1:S": ["4"], - }); - }); -}); + INIT: ['1'], + '1': ['2'], + '1.1:T': ['3'], + '1.1:S': ['4'] + }) + }) +}) diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts index 2e05c48..116c88f 100644 --- a/tests/markdown.test.ts +++ b/tests/markdown.test.ts @@ -1,7 +1,7 @@ -import { validateMarkdown } from "../src/utils/validateMarkdown"; +import { validateMarkdown } from '../src/utils/validateMarkdown' -describe("validate markdown", () => { - it("should return false if missing a summary title (#)", () => { +describe('validate markdown', () => { + it('should return false if missing a summary title (#)', () => { const md = ` Description. @@ -9,11 +9,11 @@ Description. > Level's summary: a short description of the level's content in one line. -Some text that describes the level`; - expect(validateMarkdown(md)).toBe(false); - }); +Some text that describes the level` + expect(validateMarkdown(md)).toBe(false) + }) - it("should return false if contains multiple `#` headers", () => { + it('should return false if contains multiple `#` headers', () => { const md1 = `# A Title Description. @@ -23,7 +23,7 @@ Description. > Level's summary: a short description of the level's content in one line. -Some text that describes the level`; +Some text that describes the level` const md2 = `# A Title Description. @@ -36,13 +36,13 @@ Description. Some text that describes the level # Another title -`; +` - expect(validateMarkdown(md1)).toBe(false); - expect(validateMarkdown(md2)).toBe(false); - }); + expect(validateMarkdown(md1)).toBe(false) + expect(validateMarkdown(md2)).toBe(false) + }) - it("should return false if missing a summary description", () => { + it('should return false if missing a summary description', () => { const md = `# A Title ## 1. Put Level's title here @@ -50,9 +50,9 @@ Some text that describes the level > Level's summary: a short description of the level's content in one line. Some text that describes the level -`; - expect(validateMarkdown(md)).toBe(false); - }); +` + expect(validateMarkdown(md)).toBe(false) + }) it("should return false if `##` doesn't preface a level", () => { const md = `# A Title @@ -64,9 +64,9 @@ A description > Level's summary: a short description of the level's content in one line. Some text that describes the level -`; - expect(validateMarkdown(md)).toBe(false); - }); +` + expect(validateMarkdown(md)).toBe(false) + }) it("should return false if `###` doesn't preface a step", () => { const md = `# A Title @@ -82,11 +82,11 @@ Some text that describes the level ### Missing step id First step -`; - expect(validateMarkdown(md)).toBe(false); - }); +` + expect(validateMarkdown(md)).toBe(false) + }) - it("should return true for valid markdown", () => { + it('should return true for valid markdown', () => { const md = `# Title Description. @@ -99,11 +99,11 @@ Some text that describes the level ### 1.1 -First Step`; - expect(validateMarkdown(md)).toBe(true); - }); +First Step` + expect(validateMarkdown(md)).toBe(true) + }) - it("should allow for empty level content", () => { + it('should allow for empty level content', () => { const md = `# Title Description. @@ -112,11 +112,11 @@ Description. ### 1.1 -First Step`; - expect(validateMarkdown(md)).toBe(true); - }); +First Step` + expect(validateMarkdown(md)).toBe(true) + }) - it("should ignore markdown content in codeblocks", () => { + it('should ignore markdown content in codeblocks', () => { const md = `# Title Description. @@ -142,18 +142,18 @@ Should not be an issue ### 1.1 -First Step`; - expect(validateMarkdown(md)).toBe(true); - }); - it("should ignore empty space at the top", () => { +First Step` + expect(validateMarkdown(md)).toBe(true) + }) + it('should ignore empty space at the top', () => { const md = ` # Title -Description.`; - expect(validateMarkdown(md)).toBe(true); - }); - it("should ignore empty space at the bottom", () => { +Description.` + expect(validateMarkdown(md)).toBe(true) + }) + it('should ignore empty space at the bottom', () => { const md = ` # Title @@ -164,7 +164,7 @@ Description. -`; - expect(validateMarkdown(md)).toBe(true); - }); -}); +` + expect(validateMarkdown(md)).toBe(true) + }) +}) diff --git a/tests/parse.test.ts b/tests/parse.test.ts index 746bf68..508cea2 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -1,33 +1,33 @@ -import { parse } from "../src/utils/parse"; +import { parse } from '../src/utils/parse' -describe("parse", () => { - describe("summary", () => { - it("should parse summary", () => { +describe('parse', () => { + describe('summary', () => { + it('should parse summary', () => { const md = `# Insert Tutorial's Title here Short description to be shown as a tutorial's subtitle. -`; +` - const skeleton = { version: "0.1.0" }; + const skeleton = { version: '0.1.0' } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { summary: { description: "Short description to be shown as a tutorial's subtitle.", - title: "Insert Tutorial's Title here", - }, - }; - expect(result.summary).toEqual(expected.summary); - }); - }); - - describe("levels", () => { - it("should parse a level with no steps", () => { + title: "Insert Tutorial's Title here" + } + } + expect(result.summary).toEqual(expected.summary) + }) + }) + + describe('levels', () => { + it('should parse a level with no steps', () => { const md = `# Title Description. @@ -37,33 +37,33 @@ Description. > Level's summary: a short description of the level's content in one line. Some text -`; +` const skeleton = { - levels: [{ id: "1" }], - }; + levels: [{ id: '1' }] + } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", summary: "Level's summary: a short description of the level's content in one line.", - content: "Some text", - steps: [], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should parse a level with a step", () => { + content: 'Some text', + steps: [] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should parse a level with a step', () => { const md = `# Title Description. @@ -73,41 +73,41 @@ Description. > Level's summary: a short description of the level's content in one line. Some text -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', setup: { files: [], commits: [] }, solution: { files: [], commits: [] }, - steps: [], - }, - ], - }; + steps: [] + } + ] + } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", summary: "Level's summary: a short description of the level's content in one line.", - content: "Some text", + content: 'Some text', setup: { files: [], commits: [] }, solution: { files: [], commits: [] }, - steps: [], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should parse a level with no level description", () => { + steps: [] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should parse a level with no level description', () => { const md = `# Title Description. @@ -115,29 +115,29 @@ Description. ## 1. Put Level's title here Some text that becomes the summary -`; +` - const skeleton = { levels: [{ id: "1" }] }; + const skeleton = { levels: [{ id: '1' }] } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", - summary: "Some text that becomes the summary", - content: "Some text that becomes the summary", - steps: [], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should truncate a level description", () => { + summary: 'Some text that becomes the summary', + content: 'Some text that becomes the summary', + steps: [] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should truncate a level description', () => { const md = `# Title Description. @@ -145,28 +145,28 @@ Description. ## 1. Put Level's title here Some text that becomes the summary and goes beyond the maximum length of 80 so that it gets truncated at the end -`; +` - const skeleton = { levels: [{ id: "1" }] }; + const skeleton = { levels: [{ id: '1' }] } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", - summary: "Some text that becomes the summary", - content: "Some text that becomes the summary", - }, - ], - }; - expect(result.levels[0].summary).toMatch(/\.\.\.$/); - }); - - it("should only truncate the first line of a level description", () => { + summary: 'Some text that becomes the summary', + content: 'Some text that becomes the summary' + } + ] + } + expect(result.levels[0].summary).toMatch(/\.\.\.$/) + }) + + it('should only truncate the first line of a level description', () => { const md = `# Title Description. @@ -176,29 +176,29 @@ Description. Some text. But not including this line. -`; +` - const skeleton = { levels: [{ id: "1" }] }; + const skeleton = { levels: [{ id: '1' }] } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", - summary: "Some text.", - content: "Some text.\n\nBut not including this line.", - steps: [], - }, - ], - }; - expect(result.levels[0]).toEqual(expected.levels[0]); - }); - - it("should truncate a level with an empty summary", () => { + summary: 'Some text.', + content: 'Some text.\n\nBut not including this line.', + steps: [] + } + ] + } + expect(result.levels[0]).toEqual(expected.levels[0]) + }) + + it('should truncate a level with an empty summary', () => { const md = `# Title Description. @@ -210,29 +210,29 @@ Description. Some text. But not including this line. -`; +` - const skeleton = { levels: [{ id: "1" }] }; + const skeleton = { levels: [{ id: '1' }] } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", - summary: "Some text.", - content: "Some text.\n\nBut not including this line.", - steps: [], - }, - ], - }; - expect(result.levels[0]).toEqual(expected.levels[0]); - }); - - it("should allow for empty level content", () => { + summary: 'Some text.', + content: 'Some text.\n\nBut not including this line.', + steps: [] + } + ] + } + expect(result.levels[0]).toEqual(expected.levels[0]) + }) + + it('should allow for empty level content', () => { const md = `# Title Description. @@ -242,34 +242,34 @@ Description. ### 1.1 A step -`; +` - const skeleton = { levels: [{ id: "1" }] }; + const skeleton = { levels: [{ id: '1' }] } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { levels: [ { - id: "1", + id: '1', title: "Put Level's title here", - summary: "", - content: "", + summary: '', + content: '', steps: [ { - id: "1.1", - content: "A step", - }, - ], - }, - ], - }; - expect(result.levels[0]).toEqual(expected.levels[0]); - }); - - it("should match line breaks with double line breaks for proper spacing", () => { + id: '1.1', + content: 'A step' + } + ] + } + ] + } + expect(result.levels[0]).toEqual(expected.levels[0]) + }) + + it('should match line breaks with double line breaks for proper spacing', () => { const md = `# Title Description. @@ -283,31 +283,31 @@ First line Second line Third line -`; +` - const skeleton = { levels: [{ id: "1" }] }; + const skeleton = { levels: [{ id: '1' }] } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { summary: { - description: "Description.\n\nSecond description line", + description: 'Description.\n\nSecond description line' }, levels: [ { - id: "1", - summary: "Some text that becomes the summary", - content: "First line\n\nSecond line\n\nThird line", - }, - ], - }; - expect(result.summary.description).toBe(expected.summary.description); - expect(result.levels[0].content).toBe(expected.levels[0].content); - }); - - it("should load a single commit for a step", () => { + id: '1', + summary: 'Some text that becomes the summary', + content: 'First line\n\nSecond line\n\nThird line' + } + ] + } + expect(result.summary.description).toBe(expected.summary.description) + expect(result.levels[0].content).toBe(expected.levels[0].content) + }) + + it('should load a single commit for a step', () => { const md = `# Title Description. @@ -319,51 +319,51 @@ First line ### 1.1 The first step -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdefg1"], - }, - }); + '1.1:T': ['abcdefg1'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - summary: "First line", - content: "First line", + id: '1', + summary: 'First line', + content: 'First line', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdefg1"], - }, - }, - ], - }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); - - it("should load multiple commits for a step", () => { + commits: ['abcdefg1'] + } + } + ] + } + ] + } + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]) + }) + + it('should load multiple commits for a step', () => { const md = `# Title Description. @@ -375,51 +375,51 @@ First line ### 1.1 Step The first step -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdefg1", "123456789"], - }, - }); + '1.1:T': ['abcdefg1', '123456789'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - summary: "First line", - content: "First line", + id: '1', + summary: 'First line', + content: 'First line', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdefg1", "123456789"], - }, - }, - ], - }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); - - it("should load a single commit for a level", () => { + commits: ['abcdefg1', '123456789'] + } + } + ] + } + ] + } + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]) + }) + + it('should load a single commit for a level', () => { const md = `# Title Description. @@ -431,40 +431,40 @@ First line ### 1.1 The first step -`; +` const skeleton = { levels: [ { - id: "1", - }, - ], - }; + id: '1' + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1": ["abcdefg1"], - }, - }); + '1': ['abcdefg1'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - summary: "First line", - content: "First line", + id: '1', + summary: 'First line', + content: 'First line', setup: { - commits: ["abcdefg1"], - }, - }, - ], - }; - expect(result.levels[0].setup).toEqual(expected.levels[0].setup); - }); - - it("should load multi-line step content", () => { + commits: ['abcdefg1'] + } + } + ] + } + expect(result.levels[0].setup).toEqual(expected.levels[0].setup) + }) + + it('should load multi-line step content', () => { const md = `# Title Description. @@ -484,39 +484,39 @@ var a = 1; \`\`\` Another line -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1": ["abcdefg1"], - "1.1:T": ["12345678"], - }, - }); + '1': ['abcdefg1'], + '1.1:T': ['12345678'] + } + }) const expected = { - id: "1.1", + id: '1.1', setup: { - commits: ["12345678"], + commits: ['12345678'] }, content: - "The first step\n\nA codeblock:\n\n```js\nvar a = 1;\n```\n\nAnother line", - }; - expect(result.levels[0].steps[0]).toEqual(expected); - }); + 'The first step\n\nA codeblock:\n\n```js\nvar a = 1;\n```\n\nAnother line' + } + expect(result.levels[0].steps[0]).toEqual(expected) + }) - it("should load the full config for a step", () => { + it('should load the full config for a step', () => { const md = `# Title Description. @@ -528,71 +528,71 @@ First line ### 1.1 Step The first step -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", + id: '1.1', setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], - }, - ], - }; + commands: ['npm install'], + files: ['someFile.js'] + } + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdefg1", "123456789"], - "1.1:S": ["1gfedcba", "987654321"], - }, - }); + '1.1:T': ['abcdefg1', '123456789'], + '1.1:S': ['1gfedcba', '987654321'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - summary: "First line", - content: "First line", + id: '1', + summary: 'First line', + content: 'First line', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdefg1", "123456789"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commits: ['abcdefg1', '123456789'], + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commits: ["1gfedcba", "987654321"], - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], - }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); - - it("should load the full config for multiple levels & steps", () => { + commits: ['1gfedcba', '987654321'], + commands: ['npm install'], + files: ['someFile.js'] + } + } + ] + } + ] + } + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]) + }) + + it('should load the full config for multiple levels & steps', () => { const md = `# Title Description. @@ -616,149 +616,149 @@ Second level content. ### 2.1 The third step -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", + id: '1.1', setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commands: ["npm install"], - files: ["someFile.js"], - }, + commands: ['npm install'], + files: ['someFile.js'] + } }, { - id: "1.2", + id: '1.2', setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], + commands: ['npm install'], + files: ['someFile.js'] + } + } + ] }, { - id: "2", - summary: "Second level content.", - content: "First level content.", + id: '2', + summary: 'Second level content.', + content: 'First level content.', steps: [ { - id: "2.1", + id: '2.1', setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], - }, - ], - }; + commands: ['npm install'], + files: ['someFile.js'] + } + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1", "123456789"], - "1.1:S": ["1fedcba", "987654321"], - "1.2:T": ["2abcdef"], - "1.2:S": ["3abcdef"], - "2.1:T": ["4abcdef"], - "2.1:S": ["5abcdef"], - }, - }); + '1.1:T': ['abcdef1', '123456789'], + '1.1:S': ['1fedcba', '987654321'], + '1.2:T': ['2abcdef'], + '1.2:S': ['3abcdef'], + '2.1:T': ['4abcdef'], + '2.1:S': ['5abcdef'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdef1", "123456789"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commits: ['abcdef1', '123456789'], + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commits: ["1fedcba", "987654321"], - commands: ["npm install"], - files: ["someFile.js"], - }, + commits: ['1fedcba', '987654321'], + commands: ['npm install'], + files: ['someFile.js'] + } }, { - id: "1.2", - content: "The second step", + id: '1.2', + content: 'The second step', setup: { - commits: ["2abcdef"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commits: ['2abcdef'], + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commits: ["3abcdef"], - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], + commits: ['3abcdef'], + commands: ['npm install'], + files: ['someFile.js'] + } + } + ] }, { - id: "2", - title: "Title 2", - summary: "Second level content.", - content: "Second level content.", + id: '2', + title: 'Title 2', + summary: 'Second level content.', + content: 'Second level content.', steps: [ { - id: "2.1", - content: "The third step", + id: '2.1', + content: 'The third step', setup: { - commits: ["4abcdef"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", + commits: ['4abcdef'], + commands: ['npm install'], + files: ['someFile.js'], + watchers: ['someFile.js'], + filter: 'someFilter' }, solution: { - commits: ["5abcdef"], - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should handle steps with no solution", () => { + commits: ['5abcdef'], + commands: ['npm install'], + files: ['someFile.js'] + } + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should handle steps with no solution', () => { const md = `# Title Description. @@ -771,51 +771,51 @@ First level content. The first step -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1", "123456789"], - }, - }); + '1.1:T': ['abcdef1', '123456789'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdef1", "123456789"], - }, - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - it("should load no commits if none found for a step", () => { + commits: ['abcdef1', '123456789'] + } + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + it('should load no commits if none found for a step', () => { const md = `# Title Description. @@ -827,46 +827,46 @@ First line ### 1.1 The first step -`; +` const skeleton = { levels: [ { - id: "1", - steps: [{ id: "1" }], - }, - ], - }; + id: '1', + steps: [{ id: '1' }] + } + ] + } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - summary: "First line", - content: "First line", + id: '1', + summary: 'First line', + content: 'First line', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: [], - }, - }, - ], - }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); - }); - - it("should load when commits are not in direct order (100, 200, 201)", () => { + commits: [] + } + } + ] + } + ] + } + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]) + }) + }) + + it('should load when commits are not in direct order (100, 200, 201)', () => { const md = `# Title Description. @@ -894,237 +894,284 @@ Third line ### 201.1 The third step -`; +` const skeleton = { levels: [ { - id: "100", - steps: [{ id: "100.1" }], + id: '100', + steps: [{ id: '100.1' }] }, { - id: "200", - steps: [{ id: "200.1" }], + id: '200', + steps: [{ id: '200.1' }] }, { - id: "201", - steps: [{ id: "201.1" }], - }, - ], - }; + id: '201', + steps: [{ id: '201.1' }] + } + ] + } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "100", - title: "First Title", - summary: "First line", - content: "First line", + id: '100', + title: 'First Title', + summary: 'First line', + content: 'First line', steps: [ { - id: "100.1", - content: "The first step", + id: '100.1', + content: 'The first step', setup: { - commits: [], - }, - }, - ], + commits: [] + } + } + ] }, { - id: "200", - title: "Second Title", - summary: "Second line", - content: "Second line", + id: '200', + title: 'Second Title', + summary: 'Second line', + content: 'Second line', steps: [ { - id: "200.1", - content: "The second step", + id: '200.1', + content: 'The second step', setup: { - commits: [], - }, - }, - ], + commits: [] + } + } + ] }, { - id: "201", - title: "Third Title", - summary: "Third line", - content: "Third line", + id: '201', + title: 'Third Title', + summary: 'Third line', + content: 'Third line', steps: [ { - id: "201.1", - content: "The third step", + id: '201.1', + content: 'The third step', setup: { - commits: [], - }, - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); + commits: [] + } + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should load content with #', () => { + // https://github.com/coderoad/coderoad-vscode/issues/479 - describe("config", () => { - it("should parse the tutorial config", () => { + const md = `# Title + +Description line with # content and #content and ## content + +## 1. Title + +First line with # content and #content and ## content + +### 1.1 + +Content line with # content and #content and ## content` + const skeleton = { + levels: [ + { + id: '1' + } + ] + } + const result = parse({ + text: md, + skeleton, + commits: { + '1': ['abcdefg1'] + } + }) + const expected = { + summary: { + description: + 'Description line with # content and #content and ## content' + }, + levels: [ + { + id: '1', + summary: 'First line with # content and #content and ## content', + content: 'Content line with # content and #content and ## content', + setup: { + commits: ['abcdefg1'] + } + } + ] + } + expect(result.levels[0].setup).toEqual(expected.levels[0].setup) + }) + + describe('config', () => { + it('should parse the tutorial config', () => { const md = `# Title Description. -`; +` const skeleton = { config: { testRunner: { - command: "./node_modules/.bin/mocha", + command: './node_modules/.bin/mocha', args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", + filter: '--grep', + tap: '--reporter=mocha-tap-reporter' }, - directory: "coderoad", + directory: 'coderoad' }, setup: { - commands: [], + commands: [] }, appVersions: { - vscode: ">=0.7.0", + vscode: '>=0.7.0' }, repo: { - uri: "https://path.to/repo", - branch: "aBranch", + uri: 'https://path.to/repo', + branch: 'aBranch' }, reset: { - commands: ["some command"], + commands: ['some command'] }, dependencies: [ { - name: "node", - version: ">=10", - }, - ], - }, - }; + name: 'node', + version: '>=10' + } + ] + } + } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { summary: { - description: "Description.\n\nSecond description line", + description: 'Description.\n\nSecond description line' }, config: { testRunner: { - command: "./node_modules/.bin/mocha", + command: './node_modules/.bin/mocha', args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", + filter: '--grep', + tap: '--reporter=mocha-tap-reporter' }, - directory: "coderoad", + directory: 'coderoad' }, setup: { - commands: [], + commands: [] }, repo: { - uri: "https://path.to/repo", - branch: "aBranch", + uri: 'https://path.to/repo', + branch: 'aBranch' }, reset: { - commands: ["some command"], + commands: ['some command'] }, dependencies: [ { - name: "node", - version: ">=10", - }, + name: 'node', + version: '>=10' + } ], appVersions: { - vscode: ">=0.7.0", - }, - }, - }; - expect(result.config).toEqual(expected.config); - }); - - it("should parse the tutorial config with INIT commits", () => { + vscode: '>=0.7.0' + } + } + } + expect(result.config).toEqual(expected.config) + }) + + it('should parse the tutorial config with INIT commits', () => { const md = `# Title Description. -`; +` const skeleton = { config: { testRunner: { - command: "./node_modules/.bin/mocha", + command: './node_modules/.bin/mocha', args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", + filter: '--grep', + tap: '--reporter=mocha-tap-reporter' }, - directory: "coderoad", + directory: 'coderoad' }, appVersions: { - vscode: ">=0.7.0", + vscode: '>=0.7.0' }, repo: { - uri: "https://path.to/repo", - branch: "aBranch", + uri: 'https://path.to/repo', + branch: 'aBranch' }, dependencies: [ { - name: "node", - version: ">=10", - }, - ], - }, - }; + name: 'node', + version: '>=10' + } + ] + } + } const result = parse({ text: md, skeleton, commits: { - INIT: ["abcdef1", "123456789"], - }, - }); + INIT: ['abcdef1', '123456789'] + } + }) const expected = { summary: { - description: "Description.\n\nSecond description line", + description: 'Description.\n\nSecond description line' }, config: { testRunner: { - command: "./node_modules/.bin/mocha", + command: './node_modules/.bin/mocha', args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", + filter: '--grep', + tap: '--reporter=mocha-tap-reporter' }, - directory: "coderoad", + directory: 'coderoad' }, setup: { - commits: ["abcdef1", "123456789"], + commits: ['abcdef1', '123456789'] }, repo: { - uri: "https://path.to/repo", - branch: "aBranch", + uri: 'https://path.to/repo', + branch: 'aBranch' }, dependencies: [ { - name: "node", - version: ">=10", - }, + name: 'node', + version: '>=10' + } ], appVersions: { - vscode: ">=0.7.0", - }, - }, - }; - expect(result.config).toEqual(expected.config); - }); - }); - - describe("hints", () => { + vscode: '>=0.7.0' + } + } + } + expect(result.config).toEqual(expected.config) + }) + }) + + describe('hints', () => { it("should parse hints for a step with '*", () => { const md = `# Title @@ -1143,53 +1190,53 @@ The first step * First Hint * Second Hint -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1", "123456789"], - }, - }); + '1.1:T': ['abcdef1', '123456789'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdef1", "123456789"], + commits: ['abcdef1', '123456789'] }, - hints: ["First Hint", "Second Hint"], - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should parse without spaces at the end", () => { + hints: ['First Hint', 'Second Hint'] + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should parse without spaces at the end', () => { const md = `# Title Description. @@ -1204,49 +1251,49 @@ The first step #### HINTS -- A test with a \`backtick\``; +- A test with a \`backtick\`` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, - commits: {}, - }); + commits: {} + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: [], + commits: [] }, - hints: ["A test with a `backtick`"], - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); + hints: ['A test with a `backtick`'] + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) it("should parse hints for a step with '-'", () => { const md = `# Title @@ -1266,53 +1313,53 @@ The first step - First Hint - Second Hint -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1", "123456789"], - }, - }); + '1.1:T': ['abcdef1', '123456789'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdef1", "123456789"], + commits: ['abcdef1', '123456789'] }, - hints: ["First Hint", "Second Hint"], - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should parse hints for a step", () => { + hints: ['First Hint', 'Second Hint'] + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should parse hints for a step', () => { const md = `# Title Description. @@ -1335,56 +1382,56 @@ var a = 1; \`\`\` And spans multiple lines. -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1", "123456789"], - }, - }); + '1.1:T': ['abcdef1', '123456789'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdef1", "123456789"], + commits: ['abcdef1', '123456789'] }, hints: [ - "First Hint with `markdown`. See **bold**", - "Second Hint has a codeblock\n\n```js\nvar a = 1;\n```\n\nAnd spans multiple lines.", - ], - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - - it("should parse hints and not interrupt next step", () => { + 'First Hint with `markdown`. See **bold**', + 'Second Hint has a codeblock\n\n```js\nvar a = 1;\n```\n\nAnd spans multiple lines.' + ] + } + ] + } + ] + } + expect(result.levels).toEqual(expected.levels) + }) + + it('should parse hints and not interrupt next step', () => { const md = `# Title Description. @@ -1411,73 +1458,73 @@ And spans multiple lines. ### 1.2 The second uninterrupted step -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", + id: '1.1' }, { - id: "1.2", - }, - ], - }, - ], - }; + id: '1.2' + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1"], - "1.1:S": ["123456789"], - "1.2:T": ["fedcba1"], - }, - }); + '1.1:T': ['abcdef1'], + '1.1:S': ['123456789'], + '1.2:T': ['fedcba1'] + } + }) const expected = { summary: { - description: "Description.", + description: 'Description.' }, levels: [ { - id: "1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", + id: '1', + title: 'Title 1', + summary: 'First level content.', + content: 'First level content.', steps: [ { - id: "1.1", - content: "The first step", + id: '1.1', + content: 'The first step', setup: { - commits: ["abcdef1"], + commits: ['abcdef1'] }, solution: { - commits: ["123456789"], + commits: ['123456789'] }, hints: [ - "First Hint with `markdown`. See **bold**", - "Second Hint has a codeblock\n\n```js\nvar a = 1;\n```\n\nAnd spans multiple lines.", - ], + 'First Hint with `markdown`. See **bold**', + 'Second Hint has a codeblock\n\n```js\nvar a = 1;\n```\n\nAnd spans multiple lines.' + ] }, { - id: "1.2", - content: "The second uninterrupted step", + id: '1.2', + content: 'The second uninterrupted step', setup: { - commits: ["fedcba1"], - }, - }, - ], + commits: ['fedcba1'] + } + } + ] }, - {}, - ], - }; - expect(result.levels[0]).toEqual(expected.levels[0]); - }); - }); - describe("subtasks", () => { - it("should parse subtasks", () => { + {} + ] + } + expect(result.levels[0]).toEqual(expected.levels[0]) + }) + }) + describe('subtasks', () => { + it('should parse subtasks', () => { const md = `# Subtask Demo A demo demonstrating how to use subtasks @@ -1495,56 +1542,56 @@ Create a function \`add\` that can take a variety of params. - Add one number - Add two numbers - Add three numbers -`; +` const skeleton = { levels: [ { - id: "1", + id: '1', steps: [ { - id: "1.1", - }, - ], - }, - ], - }; + id: '1.1' + } + ] + } + ] + } const expected = { levels: [ { - id: "1", - title: "Subtask Example", - summary: "A subtask example", - content: "A subtask example", + id: '1', + title: 'Subtask Example', + summary: 'A subtask example', + content: 'A subtask example', steps: [ { - id: "1.1", + id: '1.1', setup: { - commits: ["abcdef1"], + commits: ['abcdef1'] }, content: - "Create a function `add` that can take a variety of params.", + 'Create a function `add` that can take a variety of params.', solution: { - commits: ["abcdef2"], + commits: ['abcdef2'] }, subtasks: [ - "Add one number", - "Add two numbers", - "Add three numbers", - ], - }, - ], - }, - ], - }; + 'Add one number', + 'Add two numbers', + 'Add three numbers' + ] + } + ] + } + ] + } const result = parse({ text: md, skeleton, commits: { - "1.1:T": ["abcdef1"], - "1.1:S": ["abcdef2"], - }, - }); - expect(result.levels[0]).toEqual(expected.levels[0]); - }); - }); -}); + '1.1:T': ['abcdef1'], + '1.1:S': ['abcdef2'] + } + }) + expect(result.levels[0]).toEqual(expected.levels[0]) + }) + }) +}) diff --git a/tests/skeleton.test.ts b/tests/skeleton.test.ts index 81fbfb6..758b83e 100644 --- a/tests/skeleton.test.ts +++ b/tests/skeleton.test.ts @@ -1,271 +1,271 @@ -import { validateSchema } from "../src/utils/validateSchema"; -import skeletonSchema from "../src/schema/skeleton"; +import { validateSchema } from '../src/utils/validateSchema' +import skeletonSchema from '../src/schema/skeleton' -const validateSkeleton = (json: any) => validateSchema(skeletonSchema, json); +const validateSkeleton = (json: any) => validateSchema(skeletonSchema, json) const validJson = { - version: "0.1.0", + version: '0.1.0', config: { testRunner: { - directory: "coderoad", + directory: 'coderoad', args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", + filter: '--grep', + tap: '--reporter=mocha-tap-reporter' }, - command: "./node_modules/.bin/mocha", + command: './node_modules/.bin/mocha' }, setup: { - commands: [], + commands: [] }, repo: { - uri: "http://github.com/somePath/toRepo.git", - branch: "codeBranch", + uri: 'http://github.com/somePath/toRepo.git', + branch: 'codeBranch' }, dependencies: [], appVersions: { - vscode: ">=0.7.0", - }, + vscode: '>=0.7.0' + } }, levels: [ { steps: [ { - id: "1.1", + id: '1.1', setup: { - files: ["package.json"], + files: ['package.json'] }, solution: { - files: ["package.json"], - }, + files: ['package.json'] + } }, { - id: "1.2", + id: '1.2', setup: { - commands: ["npm install"], + commands: ['npm install'] }, solution: { - commands: ["npm install"], - }, + commands: ['npm install'] + } }, { - id: "1.3", + id: '1.3', setup: { - files: ["package.json"], - watchers: ["package.json", "node_modules/some-package"], + files: ['package.json'], + watchers: ['package.json', 'node_modules/some-package'] }, solution: { - files: ["package.json"], - }, + files: ['package.json'] + } }, { - id: "1.4", + id: '1.4', setup: { commands: [], - filter: "^Example 2", - subtasks: true, - }, - }, + filter: '^Example 2', + subtasks: true + } + } ], - id: "1", - }, - ], -}; + id: '1' + } + ] +} -describe("validate skeleton", () => { - it("should fail an empty skeleton file", () => { - const json = {}; +describe('validate skeleton', () => { + it('should fail an empty skeleton file', () => { + const json = {} - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should parse a valid skeleton file", () => { - const json = { ...validJson }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should parse a valid skeleton file', () => { + const json = { ...validJson } - const valid = validateSkeleton(json); - expect(valid).toBe(true); - }); - it("should fail if version is invalid", () => { - const json = { ...validJson, version: "NOT A VERSION" }; + const valid = validateSkeleton(json) + expect(valid).toBe(true) + }) + it('should fail if version is invalid', () => { + const json = { ...validJson, version: 'NOT A VERSION' } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if version is missing", () => { - const json = { ...validJson, version: undefined }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if version is missing', () => { + const json = { ...validJson, version: undefined } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if config is missing", () => { - const json = { ...validJson, config: undefined }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if config is missing', () => { + const json = { ...validJson, config: undefined } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if config testRunner is missing", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if config testRunner is missing', () => { const json = { ...validJson, - config: { ...validJson.config, testRunner: undefined }, - }; + config: { ...validJson.config, testRunner: undefined } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if config testRunner command is missing", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if config testRunner command is missing', () => { const json = { ...validJson, config: { ...validJson.config, - testRunner: { ...validJson.config.testRunner, command: undefined }, - }, - }; + testRunner: { ...validJson.config.testRunner, command: undefined } + } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if config testRunner args tap is missing", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if config testRunner args tap is missing', () => { const json = { ...validJson, config: { ...validJson.config, testRunner: { ...validJson.config.testRunner, - args: { ...validJson.config.testRunner.args, tap: undefined }, - }, - }, - }; + args: { ...validJson.config.testRunner.args, tap: undefined } + } + } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if repo is missing", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if repo is missing', () => { const json = { ...validJson, config: { ...validJson.config, - repo: undefined, - }, - }; + repo: undefined + } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if repo uri is missing", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if repo uri is missing', () => { const json = { ...validJson, config: { ...validJson.config, - repo: { ...validJson.config.repo, uri: undefined }, - }, - }; + repo: { ...validJson.config.repo, uri: undefined } + } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if repo uri is invalid", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if repo uri is invalid', () => { const json = { ...validJson, config: { ...validJson.config, - repo: { ...validJson.config.repo, uri: "NOT A VALID URI" }, - }, - }; + repo: { ...validJson.config.repo, uri: 'NOT A VALID URI' } + } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if repo branch is missing", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if repo branch is missing', () => { const json = { ...validJson, config: { ...validJson.config, - repo: { ...validJson.config.repo, branch: undefined }, - }, - }; + repo: { ...validJson.config.repo, branch: undefined } + } + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if level is missing id", () => { - const level1 = { ...validJson.levels[0], id: undefined }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if level is missing id', () => { + const level1 = { ...validJson.levels[0], id: undefined } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if level setup is invalid", () => { - const level1 = { ...validJson.levels[0], setup: { invalidThing: [] } }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if level setup is invalid', () => { + const level1 = { ...validJson.levels[0], setup: { invalidThing: [] } } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should fail if step is missing id", () => { - const step1 = { ...validJson.levels[0].steps[0], id: undefined }; - const level1 = { ...validJson.levels[0], steps: [step1] }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if step is missing id', () => { + const step1 = { ...validJson.levels[0].steps[0], id: undefined } + const level1 = { ...validJson.levels[0], steps: [step1] } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should not fail if step setup is missing", () => { - const step1 = { ...validJson.levels[0].steps[0], setup: undefined }; - const level1 = { ...validJson.levels[0], steps: [step1] }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should not fail if step setup is missing', () => { + const step1 = { ...validJson.levels[0].steps[0], setup: undefined } + const level1 = { ...validJson.levels[0], steps: [step1] } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(true); - }); - it("should fail if step setup is invalid", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(true) + }) + it('should fail if step setup is invalid', () => { const step1 = { ...validJson.levels[0].steps[0], - setup: { invalidThing: [] }, - }; - const level1 = { ...validJson.levels[0], steps: [step1] }; + setup: { invalidThing: [] } + } + const level1 = { ...validJson.levels[0], steps: [step1] } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); - it("should not fail if step solution is missing", () => { - const step1 = { ...validJson.levels[0].steps[0], solution: undefined }; - const level1 = { ...validJson.levels[0], steps: [step1] }; + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should not fail if step solution is missing', () => { + const step1 = { ...validJson.levels[0].steps[0], solution: undefined } + const level1 = { ...validJson.levels[0], steps: [step1] } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(true); - }); - it("should fail if step solution is invalid", () => { + const valid = validateSkeleton(json) + expect(valid).toBe(true) + }) + it('should fail if step solution is invalid', () => { const step1 = { ...validJson.levels[0].steps[0], - solution: { invalidThing: [] }, - }; - const level1 = { ...validJson.levels[0], steps: [step1] }; + solution: { invalidThing: [] } + } + const level1 = { ...validJson.levels[0], steps: [step1] } const json = { ...validJson, - levels: [level1], - }; + levels: [level1] + } - const valid = validateSkeleton(json); - expect(valid).toBe(false); - }); -}); + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) +}) diff --git a/tests/tutorial.test.ts b/tests/tutorial.test.ts index 3958267..3c2526e 100644 --- a/tests/tutorial.test.ts +++ b/tests/tutorial.test.ts @@ -1,134 +1,134 @@ -import * as T from "../typings/tutorial"; -import tutorialSchema from "../src/schema/tutorial"; -import { validateSchema } from "../src/utils/validateSchema"; +import * as T from '../typings/tutorial' +import tutorialSchema from '../src/schema/tutorial' +import { validateSchema } from '../src/utils/validateSchema' const validJson: Partial = { - version: "0.1.0", - summary: { title: "Title", description: "Description" }, + version: '0.1.0', + summary: { title: 'Title', description: 'Description' }, config: { testRunner: { - command: "aCommand", + command: 'aCommand', args: { - filter: "filter", - tap: "tap", + filter: 'filter', + tap: 'tap' }, - directory: "coderoad", + directory: 'coderoad' }, setup: { - commits: ["abcdef1"], - commands: ["npm install"], + commits: ['abcdef1'], + commands: ['npm install'] }, repo: { - uri: "https://github.com/some-repo.git", - branch: "someBranch", + uri: 'https://github.com/some-repo.git', + branch: 'someBranch' }, - dependencies: [{ name: "name", version: ">=1" }], + dependencies: [{ name: 'name', version: '>=1' }], appVersions: { - vscode: ">=0.7.0", - }, + vscode: '>=0.7.0' + } }, levels: [ { - id: "1", - title: "Level 1", - summary: "The first level", - content: "The first level", + id: '1', + title: 'Level 1', + summary: 'The first level', + content: 'The first level', steps: [ { - id: "1.1", - content: "The first step", - setup: { commits: ["abcdefa"] }, - solution: { commits: ["abcdefb"] }, - }, - ], - }, - ], -}; + id: '1.1', + content: 'The first step', + setup: { commits: ['abcdefa'] }, + solution: { commits: ['abcdefb'] } + } + ] + } + ] +} -const validateTutorial = (json: any) => validateSchema(tutorialSchema, json); +const validateTutorial = (json: any) => validateSchema(tutorialSchema, json) -describe("validate tutorial", () => { - it("should reject an empty tutorial", () => { - const json = { version: "here" }; +describe('validate tutorial', () => { + it('should reject an empty tutorial', () => { + const json = { version: 'here' } - const valid = validateTutorial(json); - expect(valid).toBe(false); - }); - it("should return true for a valid tutorial", () => { - const valid = validateTutorial({ ...validJson }); - expect(valid).toBe(true); - }); - it("should return true for a tutorial with no level content", () => { + const valid = validateTutorial(json) + expect(valid).toBe(false) + }) + it('should return true for a valid tutorial', () => { + const valid = validateTutorial({ ...validJson }) + expect(valid).toBe(true) + }) + it('should return true for a tutorial with no level content', () => { const json = { ...validJson, levels: [ { - id: "1", - title: "Level 1", - summary: "", - content: "", - steps: [], - }, - ], - }; + id: '1', + title: 'Level 1', + summary: '', + content: '', + steps: [] + } + ] + } - const valid = validateTutorial(json); - expect(valid).toBe(true); - }); - it("should allow a step with no solution", () => { + const valid = validateTutorial(json) + expect(valid).toBe(true) + }) + it('should allow a step with no solution', () => { const json = { ...validJson, levels: [ { - id: "1", - title: "Level 1", - summary: "summary", - content: "content", + id: '1', + title: 'Level 1', + summary: 'summary', + content: 'content', steps: [ { - id: "1.1", - content: "The first step", - setup: { commits: ["abcdefa"] }, + id: '1.1', + content: 'The first step', + setup: { commits: ['abcdefa'] } }, { - id: "1.2", - content: "The second step", - setup: { commits: ["abcdefb"] }, - }, - ], - }, - ], - }; + id: '1.2', + content: 'The second step', + setup: { commits: ['abcdefb'] } + } + ] + } + ] + } - const valid = validateTutorial(json); - expect(valid).toBe(true); - }); + const valid = validateTutorial(json) + expect(valid).toBe(true) + }) it("shouldn't allow a step with no setup but a solution", () => { const json = { ...validJson, levels: [ { - id: "1", - title: "Level 1", - summary: "summary", - content: "content", + id: '1', + title: 'Level 1', + summary: 'summary', + content: 'content', steps: [ { - id: "1.1", - content: "The first step", - solution: { commits: ["abcdefa"] }, + id: '1.1', + content: 'The first step', + solution: { commits: ['abcdefa'] } }, { - id: "1.2", - content: "The second step", - solution: { commits: ["abcdefb"] }, - }, - ], - }, - ], - }; + id: '1.2', + content: 'The second step', + solution: { commits: ['abcdefb'] } + } + ] + } + ] + } - const valid = validateTutorial(json); - expect(valid).toBe(false); - }); -}); + const valid = validateTutorial(json) + expect(valid).toBe(false) + }) +}) diff --git a/typings/lib.d.ts b/typings/lib.d.ts index a9b884c..8b6cb85 100644 --- a/typings/lib.d.ts +++ b/typings/lib.d.ts @@ -1,4 +1,4 @@ -declare module "arg" { +declare module 'arg' { export default (args: { [key: string]: any }, options: { argv: string[] }) => - any; + any } diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index 43596ca..3cc993c 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -1,88 +1,88 @@ -export type Maybe = T | null; +export type Maybe = T | null export type ConfigReset = { - command?: string; -}; + command?: string +} export type TutorialConfig = { - appVersions?: TutorialAppVersions; - testRunner: TestRunnerConfig; - repo: TutorialRepo; - dependencies?: TutorialDependency[]; - reset?: ConfigReset; - setup?: StepActions; -}; + appVersions?: TutorialAppVersions + testRunner: TestRunnerConfig + repo: TutorialRepo + dependencies?: TutorialDependency[] + reset?: ConfigReset + setup?: StepActions +} /** Logical groupings of tasks */ export type Level = { - id: string; - title: string; + id: string + title: string /** A summary of the level */ - summary: string; + summary: string /** The lesson content of the level, parsed as markdown */ - content: string; + content: string /** Actions run on level start up for configuring setup */ - setup?: Maybe; + setup?: Maybe /** A set of tasks for users linked to unit tests */ - steps: Array; -}; + steps: Array +} /** A level task */ export type Step = { - id: string; - content: string; - setup?: StepActions; - solution?: Maybe; - subtasks?: string[]; - hints?: string[]; -}; + id: string + content: string + setup?: StepActions + solution?: Maybe + subtasks?: string[] + hints?: string[] +} /** A tutorial for use in VSCode CodeRoad */ export type Tutorial = { - id: string; - version: string; - summary: TutorialSummary; - config: TutorialConfig; - levels: Array; -}; + id: string + version: string + summary: TutorialSummary + config: TutorialConfig + levels: Array +} /** Summary of tutorial used when selecting tutorial */ export type TutorialSummary = { - title: string; - description: string; -}; + title: string + description: string +} export type StepActions = { - commands?: string[]; - commits?: string[]; - files?: string[]; - watchers?: string[]; - filter?: string; - subtasks?: string[]; -}; + commands?: string[] + commits?: string[] + files?: string[] + watchers?: string[] + filter?: string + subtasks?: string[] +} export interface TestRunnerArgs { - filter?: string; - tap: string; + filter?: string + tap: string } export interface TestRunnerConfig { - command: string; - args: TestRunnerArgs; - directory?: string; + command: string + args: TestRunnerArgs + directory?: string } export interface TutorialRepo { - uri: string; - branch: string; + uri: string + branch: string } export interface TutorialDependency { - name: string; - version: string; - message?: string; + name: string + version: string + message?: string } export interface TutorialAppVersions { - vscode: string; + vscode: string }