diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e9694b..df154936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how }, { "name": "npm", - "version": ">5" + "version": ">=5" } ] } @@ -52,4 +52,42 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [0.3.0] -- Validate the extension version against the tutorial config version. This should allow us to manage breaking changes in tutorial schema in upcoming versions +- Validate the extension version against the tutorial config version. This should allow us to manage breaking changes in tutorial schema in upcoming versions. See [node-semver](https://github.com/npm/node-semver#advanced-range-syntax) for possible version ranges and options. + +```json +{ +"config": { + "appVersions": { + "vscode": ">=0.2" + }, +} +``` + +- Configure the CodeRoad to load and run in a different directory. The example below will: + - load a commit and run npm install to setup the test runner in its own folder. + - run "npm test" in the \$ROOT/coderoad directory on save + +```json +{ +"config": { + "testRunner": { + "command": "npm test", // runs in path location or root + "path": "coderoad", + "actions": { + "commits": ["a974aea"], + "commands": ["npm install"] // runs in path location or root + } + }, +} +``` + +Resulting in a folder structure like the following: + +``` +- .vscode +- coderoad (test runner files only with their own setup) + - package.json + - tests +- package.json +- server.js +``` diff --git a/src/actions/setupActions.ts b/src/actions/setupActions.ts index cb58a3a1..9b9a2a0a 100644 --- a/src/actions/setupActions.ts +++ b/src/actions/setupActions.ts @@ -6,10 +6,19 @@ import openFiles from './utils/openFiles' import runCommands from './utils/runCommands' import onError from '../services/sentry/onError' -const setupActions = async ( - actions: TT.StepActions, - send: (action: T.Action) => void, // send messages to client -): Promise => { +async function wait(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +interface SetupActions { + actions: TT.StepActions + send: (action: T.Action) => void // send messages to client + path?: string +} + +export const setupActions = async ({ actions, send, path }: SetupActions): Promise => { const { commands, commits, files, watchers } = actions // 1. run commits @@ -26,8 +35,13 @@ const setupActions = async ( // 3. start file watchers loadWatchers(watchers || []) + await wait(1000) + // 4. run command - await runCommands(commands || [], send).catch(onError) + await runCommands({ commands: commands || [], send, path }).catch(onError) } -export default setupActions +export const solutionActions = async (params: SetupActions): Promise => { + await git.clear() + return setupActions(params).catch(onError) +} diff --git a/src/actions/solutionActions.ts b/src/actions/solutionActions.ts deleted file mode 100644 index d269dec3..00000000 --- a/src/actions/solutionActions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as T from 'typings' -import * as TT from 'typings/tutorial' -import * as git from '../services/git' -import setupActions from './setupActions' -import onError from '../services/sentry/onError' - -const solutionActions = async (stepActions: TT.StepActions, send: (action: T.Action) => void): Promise => { - await git.clear() - return setupActions(stepActions, send).catch(onError) -} - -export default solutionActions diff --git a/src/actions/utils/runCommands.ts b/src/actions/utils/runCommands.ts index ca4c770d..9e28bcb7 100644 --- a/src/actions/utils/runCommands.ts +++ b/src/actions/utils/runCommands.ts @@ -1,7 +1,13 @@ import * as T from 'typings' import { exec } from '../../services/node' -const runCommands = async (commands: string[], send: (action: T.Action) => void) => { +interface RunCommands { + commands: string[] + send: (action: T.Action) => void + path?: string +} + +const runCommands = async ({ commands, send, path }: RunCommands) => { if (!commands.length) { return } @@ -13,7 +19,8 @@ const runCommands = async (commands: string[], send: (action: T.Action) => void) send({ type: 'COMMAND_START', payload: { process: { ...process, status: 'RUNNING' } } }) let result: { stdout: string; stderr: string } try { - result = await exec(command) + result = await exec({ command, path }) + console.log(result) } catch (error) { console.log(`Test failed: ${error.message}`) send({ type: 'COMMAND_FAIL', payload: { process: { ...process, status: 'FAIL' } } }) diff --git a/src/channel/index.ts b/src/channel/index.ts index d43e9e8f..133df3eb 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -4,8 +4,7 @@ import * as E from 'typings/error' import * as vscode from 'vscode' import { satisfies } from 'semver' import saveCommit from '../actions/saveCommit' -import setupActions from '../actions/setupActions' -import solutionActions from '../actions/solutionActions' +import { setupActions, solutionActions } from '../actions/setupActions' import tutorialConfig from '../actions/tutorialConfig' import { COMMANDS } from '../editor/commands' import logger from '../services/logger' @@ -287,11 +286,11 @@ class Channel implements Channel { // load step actions (git commits, commands, open files) case 'SETUP_ACTIONS': await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_STEP, action.payload) - setupActions(action.payload, this.send) + setupActions({ actions: action.payload, send: this.send }) return // load solution step actions (git commits, commands, open files) case 'SOLUTION_ACTIONS': - await solutionActions(action.payload, this.send) + await solutionActions({ actions: action.payload, send: this.send }) // run test following solution to update position vscode.commands.executeCommand(COMMANDS.RUN_TEST, action.payload) return diff --git a/src/editor/commands.ts b/src/editor/commands.ts index 5c356750..50f57ee7 100644 --- a/src/editor/commands.ts +++ b/src/editor/commands.ts @@ -1,6 +1,7 @@ import * as TT from 'typings/tutorial' import * as vscode from 'vscode' import createTestRunner, { Payload } from '../services/testRunner' +import { setupActions } from '../actions/setupActions' import createWebView from '../webview' export const COMMANDS = { @@ -47,7 +48,12 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP // setup 1x1 horizontal layout webview.createOrShow() }, - [COMMANDS.CONFIG_TEST_RUNNER]: (config: TT.TutorialTestRunner) => { + [COMMANDS.CONFIG_TEST_RUNNER]: async (config: TT.TutorialTestRunnerConfig) => { + if (config.actions) { + // setup tutorial test runner commits + // assumes git already exists + await setupActions({ actions: config.actions, send: webview.send, path: config.path }) + } testRunner = createTestRunner(config, { onSuccess: (payload: Payload) => { // send test pass message back to client diff --git a/src/services/dependencies/index.ts b/src/services/dependencies/index.ts index 155c9f0c..332455b9 100644 --- a/src/services/dependencies/index.ts +++ b/src/services/dependencies/index.ts @@ -5,7 +5,7 @@ const semverRegex = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*) export const version = async (name: string): Promise => { try { - const { stdout, stderr } = await exec(`${name} --version`) + const { stdout, stderr } = await exec({ command: `${name} --version` }) if (!stderr) { const match = stdout.match(semverRegex) if (match) { diff --git a/src/services/git/index.ts b/src/services/git/index.ts index f2dfc23f..beda0a4e 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -6,7 +6,7 @@ const gitOrigin = 'coderoad' const stashAllFiles = async (): Promise => { // stash files including untracked (eg. newly created file) - const { stdout, stderr } = await exec(`git stash --include-untracked`) + const { stdout, stderr } = await exec({ command: `git stash --include-untracked` }) if (stderr) { console.error(stderr) throw new Error('Error stashing files') @@ -21,7 +21,7 @@ const cherryPickCommit = async (commit: string, count = 0): Promise { */ export async function saveCommit(message: string): Promise { - const { stdout, stderr } = await exec(`git commit -am '${message}'`) + const { stdout, stderr } = await exec({ command: `git commit -am '${message}'` }) if (stderr) { console.error(stderr) throw new Error('Error saving progress to Git') @@ -58,7 +58,7 @@ export async function saveCommit(message: string): Promise { export async function clear(): Promise { try { // commit progress to git - const { stderr } = await exec('git reset HEAD --hard && git clean -fd') + const { stderr } = await exec({ command: 'git reset HEAD --hard && git clean -fd' }) if (!stderr) { return } @@ -70,7 +70,7 @@ export async function clear(): Promise { } async function init(): Promise { - const { stderr } = await exec('git init') + const { stderr } = await exec({ command: 'git init' }) if (stderr) { throw new Error('Error initializing Git') } @@ -85,13 +85,13 @@ export async function initIfNotExists(): Promise { export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise { // check for git repo - const externalRepoExists = await exec(`git ls-remote --exit-code --heads ${repo.uri}`) + const externalRepoExists = await exec({ command: `git ls-remote --exit-code --heads ${repo.uri}` }) if (externalRepoExists.stderr) { // no repo found or no internet connection throw new Error(externalRepoExists.stderr) } // check for git repo branch - const { stderr, stdout } = await exec(`git ls-remote --exit-code --heads ${repo.uri} ${repo.branch}`) + const { stderr, stdout } = await exec({ command: `git ls-remote --exit-code --heads ${repo.uri} ${repo.branch}` }) if (stderr) { throw new Error(stderr) } @@ -101,7 +101,7 @@ export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise { - const { stderr } = await exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`) + const { stderr } = await exec({ command: `git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}` }) if (stderr) { const alreadyExists = stderr.match(`${gitOrigin} already exists.`) const successfulNewBranch = stderr.match('new branch') @@ -116,7 +116,7 @@ export async function addRemote(repo: string): Promise { export async function checkRemoteExists(): Promise { try { - const { stdout, stderr } = await exec('git remote -v') + const { stdout, stderr } = await exec({ command: 'git remote -v' }) if (stderr) { return false } diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 1dbae70b..e4f6720a 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -6,10 +6,14 @@ import { WORKSPACE_ROOT } from '../../environment' const asyncExec = promisify(cpExec) -export const exec = (cmd: string): Promise<{ stdout: string; stderr: string }> | never => { - return asyncExec(cmd, { - cwd: WORKSPACE_ROOT, - }) +interface ExecParams { + command: string + path?: string +} + +export const exec = (params: ExecParams): Promise<{ stdout: string; stderr: string }> | never => { + const cwd = join(WORKSPACE_ROOT, params.path || '') + return asyncExec(params.command, { cwd }) } export const exists = (...paths: string[]): boolean | never => { diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts index b36d9575..16c097c8 100644 --- a/src/services/testRunner/index.ts +++ b/src/services/testRunner/index.ts @@ -1,3 +1,4 @@ +import { TutorialTestRunnerConfig } from 'typings/tutorial' import { exec } from '../node' import logger from '../logger' import parser from './parser' @@ -17,14 +18,10 @@ interface Callbacks { onError(payload: Payload): void } -interface TestRunnerConfig { - command: string -} - const failChannelName = 'CodeRoad (Tests)' const logChannelName = 'CodeRoad (Logs)' -const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => { +const createTestRunner = (config: TutorialTestRunnerConfig, callbacks: Callbacks) => { return async (payload: Payload, onSuccess?: () => void): Promise => { const startTime = throttle() // throttle time early @@ -39,7 +36,7 @@ const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => { let result: { stdout: string | undefined; stderr: string | undefined } try { - result = await exec(config.command) + result = await exec({ command: config.command, path: config.path }) } catch (err) { result = { stdout: err.stdout, stderr: err.stack } } diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index b6c3303b..afe3021b 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -2,7 +2,7 @@ export type Maybe = T | null export type TutorialConfig = { appVersions: TutorialAppVersions - testRunner: TutorialTestRunner + testRunner: TutorialTestRunnerConfig repo: TutorialRepo dependencies?: TutorialDependency[] } @@ -51,8 +51,10 @@ export type StepActions = { watchers: string[] } -export interface TutorialTestRunner { +export interface TutorialTestRunnerConfig { command: string + path?: string + actions?: StepActions } export interface TutorialRepo {