diff --git a/src/actions/index.ts b/src/actions/index.ts index 4d5e543e..e053e6e3 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -5,5 +5,4 @@ export { default as onValidateSetup } from './onValidateSetup' export { default as onRunReset } from './onRunReset' export { default as onErrorPage } from './onErrorPage' export { onRunTest, onTestPass } from './onTest' -export { onSetupActions, onSolutionActions } from './onActions' export { onOpenLogs } from './onOpenLogs' diff --git a/src/actions/onActions.ts b/src/actions/onActions.ts deleted file mode 100644 index 613522a5..00000000 --- a/src/actions/onActions.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as T from 'typings' -import * as TT from 'typings/tutorial' -import * as git from '../services/git' -import loadWatchers from './utils/loadWatchers' -import openFiles from './utils/openFiles' -import runCommands from './utils/runCommands' -import { onError } from '../services/telemetry' -import logger from '../services/logger' - -interface SetupActions { - actions: TT.StepActions - send: T.Send - dir?: string -} - -export const onSetupActions = async ({ actions, send, dir }: SetupActions): Promise => { - if (!actions) { - return - } - const { commands, commits, files, watchers } = actions - - // validate commit is new - let alreadyLoaded = false - - // 1. run commits - if (commits) { - // load the current list of commits for validation - const currentCommits: string[] = await git.loadCommitHistory() - for (const commit of commits) { - // validate that commit has not already been created as a safety net - if (currentCommits.includes(git.getShortHash(commit))) { - logger(`Commit ${commit} already loaded`) - alreadyLoaded = true - continue - } - await git.loadCommit(commit).catch(onError) - } - } - - // 2. open files - openFiles(files || []) - - // 3. start file watchers - loadWatchers(watchers || []) - - // 4. run command - if (!alreadyLoaded) { - await runCommands({ commands: commands || [], send, dir }).catch(onError) - } -} - -export const onSolutionActions = async (params: SetupActions): Promise => { - await git.clear() - return onSetupActions(params).catch(onError) -} diff --git a/src/channel.ts b/src/channel.ts index 316bee74..862e50f4 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -5,6 +5,7 @@ import Context from './services/context/context' import logger from './services/logger' import { openWorkspace } from './services/workspace' import * as actions from './actions' +import * as hooks from './services/hooks' interface Channel { receive(action: T.Action): Promise @@ -56,14 +57,12 @@ class Channel implements Channel { // load step actions (git commits, commands, open files) case 'SETUP_ACTIONS': await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position) - actions.onSetupActions({ actions: action.payload.actions, send: this.send }) + hooks.onSetupEnter(action.payload.actions) return // load solution step actions (git commits, commands, open files) case 'SOLUTION_ACTIONS': await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position) - await actions.onSolutionActions({ actions: action.payload.actions, send: this.send }) - // run test following solution to update position - actions.onRunTest() + hooks.onSolutionEnter(action.payload.actions) return case 'EDITOR_SYNC_POSITION': // update progress when a level is deemed complete in the client diff --git a/src/commands.ts b/src/commands.ts index fa66916e..f5756696 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,9 +2,9 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' import createTestRunner from './services/testRunner' -import { onSetupActions } from './actions/onActions' import createWebView from './services/webview' import logger from './services/logger' +import * as hooks from './services/hooks' export const COMMANDS = { START: 'coderoad.start', @@ -19,6 +19,16 @@ interface CreateCommandProps { workspaceState: vscode.Memento } +let sendToClient = (action: T.Action): void => { + // function is replaced when webclient loads +} + +// This makes it easier to pass the send +// function throughout the codebase +export const send = (action: T.Action): void => { + sendToClient(action) +} + export const createCommands = ({ extensionPath, workspaceState }: CreateCommandProps): { [key: string]: any } => { // React panel webview let webview: any @@ -36,41 +46,37 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP extensionPath, workspaceState, }) + // make send to client function exportable + // as "send". + sendToClient = webview.send } }, [COMMANDS.CONFIG_TEST_RUNNER]: async (data: TT.Tutorial) => { - const testRunnerConfig = data.config.testRunner - const setup = testRunnerConfig.setup || testRunnerConfig.actions // TODO: deprecate and remove config.actions - if (setup) { - // setup tutorial test runner commits - // assumes git already exists - await onSetupActions({ - actions: setup, - send: webview.send, - dir: testRunnerConfig.directory || testRunnerConfig.path, - }) // TODO: deprecate and remove config.path + const setupActions = data.config.setup + if (setupActions) { + hooks.onInit(setupActions) } testRunner = createTestRunner(data, { onSuccess: (position: T.Position) => { logger('test pass position', position) // send test pass message back to client - webview.send({ type: 'TEST_PASS', payload: { position: { ...position, complete: true } } }) + send({ type: 'TEST_PASS', payload: { position: { ...position, complete: true } } }) }, onFail: (position: T.Position, failSummary: T.TestFail): void => { // send test fail message back to client with failure message - webview.send({ type: 'TEST_FAIL', payload: { position, fail: failSummary } }) + send({ type: 'TEST_FAIL', payload: { position, fail: failSummary } }) }, onError: (position: T.Position) => { // TODO: send test error message back to client const message = 'Error with test runner' - webview.send({ type: 'TEST_ERROR', payload: { position, message } }) + send({ type: 'TEST_ERROR', payload: { position, message } }) }, onRun: (position: T.Position) => { // send test run message back to client - webview.send({ type: 'TEST_RUNNING', payload: { position } }) + send({ type: 'TEST_RUNNING', payload: { position } }) }, onLoadSubtasks: ({ summary }) => { - webview.send({ type: 'LOAD_SUBTASK_RESULTS', payload: { summary } }) + send({ type: 'LOAD_SUBTASK_RESULTS', payload: { summary } }) }, }) }, @@ -85,7 +91,7 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP testRunner({ position: currentPosition, onSuccess: callbacks?.onSuccess, subtasks }) }, [COMMANDS.ENTER]: () => { - webview.send({ type: 'KEY_PRESS_ENTER' }) + send({ type: 'KEY_PRESS_ENTER' }) }, } } diff --git a/src/services/hooks/index.ts b/src/services/hooks/index.ts new file mode 100644 index 00000000..91d58810 --- /dev/null +++ b/src/services/hooks/index.ts @@ -0,0 +1,43 @@ +import * as TT from 'typings/tutorial' +import * as git from '../git' +import loadCommits from './utils/loadCommits' +import loadWatchers from './utils/loadWatchers' +import openFiles from './utils/openFiles' +import runCommands from './utils/runCommands' +import runVSCodeCommands from './utils/runVSCodeCommands' +import { onError as telemetryOnError } from '../telemetry' +import { onRunTest } from '../../actions/onTest' + +export const onInit = async (actions: TT.StepActions): Promise => { + await loadCommits(actions.commits) + await runCommands(actions.commands) + await runVSCodeCommands(actions.vscodeCommands) +} + +export const onLevelEnter = async (actions: TT.StepActions): Promise => { + await loadCommits(actions.commits) + await runCommands(actions.commands) +} + +export const onSetupEnter = async (actions: TT.StepActions): Promise => { + // TODO: set position + await loadCommits(actions.commits) + await openFiles(actions.files) + await loadWatchers(actions.watchers) + await runCommands(actions.commands) + await runVSCodeCommands(actions.vscodeCommands) +} + +export const onSolutionEnter = async (actions: TT.StepActions): Promise => { + // TODO: set position + await git.clear() + await loadCommits(actions.commits) + await openFiles(actions.files) + await runCommands(actions.commands) + await runVSCodeCommands(actions.vscodeCommands) + await onRunTest() +} + +export const onError = async (error: Error): Promise => { + telemetryOnError(error) +} diff --git a/src/services/hooks/utils/loadCommits.ts b/src/services/hooks/utils/loadCommits.ts new file mode 100644 index 00000000..8b134eef --- /dev/null +++ b/src/services/hooks/utils/loadCommits.ts @@ -0,0 +1,12 @@ +import * as git from '../../git' + +const loadCommits = async (commits: string[]): Promise => { + if (commits) { + // load the current list of commits for validation + for (const commit of commits) { + await git.loadCommit(commit) + } + } +} + +export default loadCommits diff --git a/src/actions/utils/loadWatchers.ts b/src/services/hooks/utils/loadWatchers.ts similarity index 91% rename from src/actions/utils/loadWatchers.ts rename to src/services/hooks/utils/loadWatchers.ts index 3931ec50..046dacc1 100644 --- a/src/actions/utils/loadWatchers.ts +++ b/src/services/hooks/utils/loadWatchers.ts @@ -1,7 +1,7 @@ import * as chokidar from 'chokidar' import * as vscode from 'vscode' -import { COMMANDS } from '../../commands' -import { WORKSPACE_ROOT } from '../../environment' +import { COMMANDS } from '../../../commands' +import { WORKSPACE_ROOT } from '../../../environment' // NOTE: vscode createFileWatcher doesn't seem to detect changes outside of vscode // such as `npm install` of a package. Went with chokidar instead @@ -14,7 +14,7 @@ const disposeWatcher = (watcher: string) => { delete watcherObject[watcher] } -const loadWatchers = (watchers: string[]): void => { +const loadWatchers = (watchers: string[] = []): void => { if (!watchers.length) { // remove all watchers for (const watcher of Object.keys(watcherObject)) { diff --git a/src/actions/utils/openFiles.ts b/src/services/hooks/utils/openFiles.ts similarity index 88% rename from src/actions/utils/openFiles.ts rename to src/services/hooks/utils/openFiles.ts index e8bbd499..a57a7fc0 100644 --- a/src/actions/utils/openFiles.ts +++ b/src/services/hooks/utils/openFiles.ts @@ -1,8 +1,7 @@ import { join } from 'path' import * as vscode from 'vscode' -import { COMMANDS } from '../../commands' -const openFiles = async (files: string[]): Promise => { +const openFiles = async (files: string[] = []): Promise => { if (!files.length) { return } diff --git a/src/actions/utils/runCommands.ts b/src/services/hooks/utils/runCommands.ts similarity index 63% rename from src/actions/utils/runCommands.ts rename to src/services/hooks/utils/runCommands.ts index 41b28ef0..ed3b871d 100644 --- a/src/actions/utils/runCommands.ts +++ b/src/services/hooks/utils/runCommands.ts @@ -1,13 +1,7 @@ -import * as T from 'typings' -import { exec } from '../../services/node' +import { exec } from '../../node' +import { send } from '../../../commands' -interface RunCommands { - commands: string[] - send: (action: T.Action) => void - dir?: string -} - -const runCommands = async ({ commands, send, dir }: RunCommands): Promise => { +const runCommands = async (commands: string[] = []): Promise => { if (!commands.length) { return } @@ -19,10 +13,10 @@ const runCommands = async ({ commands, send, dir }: RunCommands): Promise send({ type: 'COMMAND_START', payload: { process: { ...process, status: 'RUNNING' } } }) let result: { stdout: string; stderr: string } try { - result = await exec({ command, dir }) + result = await exec({ command }) console.log(result) } catch (error) { - console.log(`Test failed: ${error.message}`) + console.error(`Command failed: ${error.message}`) send({ type: 'COMMAND_FAIL', payload: { process: { ...process, status: 'FAIL' } } }) return } diff --git a/src/services/hooks/utils/runVSCodeCommands.ts b/src/services/hooks/utils/runVSCodeCommands.ts new file mode 100644 index 00000000..6e4e9b0e --- /dev/null +++ b/src/services/hooks/utils/runVSCodeCommands.ts @@ -0,0 +1,26 @@ +import * as vscode from 'vscode' +import * as TT from 'typings/tutorial' + +// what are VSCode commands? +// - https://code.visualstudio.com/api/references/vscode-api#commands +// a list of commands: +// - https://code.visualstudio.com/api/references/commands (note many take params) +// - https://code.visualstudio.com/docs/getstarted/keybindings (anything keybound is a command) + +const runVSCodeCommands = async (commands: TT.VSCodeCommand[] = []): Promise => { + if (!commands.length) { + return + } + for (const command of commands) { + if (typeof command === 'string') { + // string named commands + await vscode.commands.executeCommand(command) + } else if (Array.isArray(command)) { + // array commands with params + const [name, params] = command + await vscode.commands.executeCommand(name, params) + } + } +} + +export default runVSCodeCommands diff --git a/src/services/reset/lastHash.test.ts b/src/services/reset/lastHash.test.ts index 891d8615..d6481ea0 100644 --- a/src/services/reset/lastHash.test.ts +++ b/src/services/reset/lastHash.test.ts @@ -65,10 +65,9 @@ describe('lastHash', () => { const tutorial: TT.Tutorial = { config: { // @ts-ignore - testRunner: { - setup: { - commits: ['abcdef2', 'abcdef3'], - }, + testRunner: {}, + setup: { + commits: ['abcdef2', 'abcdef3'], }, }, levels: [ diff --git a/src/services/reset/lastHash.ts b/src/services/reset/lastHash.ts index 9bd0d742..34b6337a 100644 --- a/src/services/reset/lastHash.ts +++ b/src/services/reset/lastHash.ts @@ -28,7 +28,7 @@ const getLastCommitHash = (position: T.Position, tutorial: TT.Tutorial | null): level = levels[levelIndex - 1] } else { // use init commit - const configCommits = tutorial.config.testRunner.setup?.commits + const configCommits = tutorial.config.setup?.commits if (!configCommits) { throw new Error('No commits found to reset back to') } diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts index db9ba8cd..ab2653f0 100644 --- a/src/services/testRunner/index.ts +++ b/src/services/testRunner/index.ts @@ -74,7 +74,7 @@ const createTestRunner = (data: TT.Tutorial, callbacks: Callbacks): ((params: an } } logger('COMMAND', command) - result = await exec({ command, dir: testRunnerConfig.directory || testRunnerConfig.path }) // TODO: remove config.path later + result = await exec({ command, dir: testRunnerConfig.directory }) } catch (err) { result = { stdout: err.stdout, stderr: err.stack } } diff --git a/src/services/webview/render.ts b/src/services/webview/render.ts index 5ff9c0fe..3ebef8a5 100644 --- a/src/services/webview/render.ts +++ b/src/services/webview/render.ts @@ -12,7 +12,7 @@ const getNonce = (): string => { return text } -async function render(panel: vscode.WebviewPanel, rootPath: string) { +async function render(panel: vscode.WebviewPanel, rootPath: string): Promise { try { // load copied index.html from web app build const dom = await JSDOM.fromFile(path.join(rootPath, 'index.html')) diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index a75ca39d..aaa357bb 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -11,6 +11,7 @@ export type TutorialConfig = { testRunner: TestRunnerConfig repo: TutorialRepo dependencies?: TutorialDependency[] + setup?: StepActions reset?: ConfigReset } @@ -60,6 +61,7 @@ export type StepActions = { files?: string[] watchers?: string[] filter?: string + vscodeCommands?: VSCodeCommand[] } export interface TestRunnerArgs { @@ -70,10 +72,7 @@ export interface TestRunnerArgs { export interface TestRunnerConfig { command: string args: TestRunnerArgs - path?: string // deprecated directory?: string - actions?: StepActions // deprecated - setup?: StepActions } export interface TutorialRepo { @@ -90,3 +89,5 @@ export interface TutorialDependency { export interface TutorialAppVersions { vscode: string } + +export type VSCodeCommand = string | [string, any]