From 9139d8fcf8100c8b118c3c1f66b1596f1a534f76 Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 20:37:45 -0700 Subject: [PATCH 1/8] refactor activate/deactivate Signed-off-by: shmck --- src/editor/index.ts | 48 --------------------------------------------- src/extension.ts | 46 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 src/editor/index.ts diff --git a/src/editor/index.ts b/src/editor/index.ts deleted file mode 100644 index bf900b5d..00000000 --- a/src/editor/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as vscode from 'vscode' -import { createCommands } from './commands' -import * as telemetry from '../services/telemetry' - -class Editor { - // extension context set on activation - // @ts-ignore - private vscodeExt: vscode.ExtensionContext - - public activate = (vscodeExt: vscode.ExtensionContext): void => { - this.vscodeExt = vscodeExt - - // set out 60/40 layout - vscode.commands.executeCommand('vscode.setEditorLayout', { - orientation: 0, - groups: [{ size: 0.6 }, { size: 0.4 }], - }) - - // commands - const commands = createCommands({ - extensionPath: this.vscodeExt.extensionPath, - // NOTE: local storage must be bound to the vscodeExt.workspaceState - workspaceState: this.vscodeExt.workspaceState, - }) - - const subscribe = (sub: any) => { - this.vscodeExt.subscriptions.push(sub) - } - - // register commands - for (const cmd in commands) { - const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) - subscribe(command) - } - - telemetry.activate(subscribe) - } - public deactivate = (): void => { - // cleanup subscriptions/tasks - for (const disposable of this.vscodeExt.subscriptions) { - disposable.dispose() - } - - telemetry.deactivate() - } -} - -export default Editor diff --git a/src/extension.ts b/src/extension.ts index a12e7e2d..1f64f83b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,46 @@ -import Editor from './editor' +import * as vscode from 'vscode' +import { createCommands } from './editor/commands' +import * as telemetry from './services/telemetry' -// vscode editor -export const editor = new Editor() +let onDeactivate = () => {} // activate run on vscode extension initialization -export const activate = editor.activate +export const activate = (vscodeExt: vscode.ExtensionContext): void => { + // set out default 60/40 layout + vscode.commands.executeCommand('vscode.setEditorLayout', { + orientation: 0, + groups: [{ size: 0.6 }, { size: 0.4 }], + }) + + // commands + const commands = createCommands({ + extensionPath: vscodeExt.extensionPath, + // NOTE: local storage must be bound to the vscodeExt.workspaceState + workspaceState: vscodeExt.workspaceState, + }) + + const subscribe = (sub: any) => { + vscodeExt.subscriptions.push(sub) + } + + // register commands + for (const cmd in commands) { + const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) + subscribe(command) + } + + telemetry.activate(subscribe) + + onDeactivate = () => { + // cleanup subscriptions/tasks + // handled within activate because it requires access to subscriptions + for (const disposable of vscodeExt.subscriptions) { + disposable.dispose() + } + + telemetry.deactivate() + } +} // deactivate run on vscode extension shut down -export const deactivate = editor.deactivate +export const deactivate = (): void => onDeactivate() From 91feec8e8a9301039f2bef2ebf87fce6ffa6b33e Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 20:58:22 -0700 Subject: [PATCH 2/8] refactor send actions Signed-off-by: shmck --- src/actions/index.ts | 2 + src/actions/onErrorPage.ts | 26 ++++++++ src/actions/onTestPass.ts | 16 +++++ src/actions/saveCommit.ts | 7 --- src/actions/tutorialConfig.ts | 2 +- src/actions/utils/loadWatchers.ts | 2 +- src/actions/utils/openFiles.ts | 2 +- src/{channel/index.ts => channel.ts} | 62 +++++-------------- src/{editor => }/commands.ts | 8 +-- src/extension.ts | 2 +- src/{channel => services/context}/context.ts | 0 .../context}/state/Position.ts | 0 .../context}/state/Progress.ts | 2 +- .../context}/state/Tutorial.ts | 2 +- src/services/node/index.ts | 5 ++ 15 files changed, 76 insertions(+), 62 deletions(-) create mode 100644 src/actions/index.ts create mode 100644 src/actions/onErrorPage.ts create mode 100644 src/actions/onTestPass.ts delete mode 100644 src/actions/saveCommit.ts rename src/{channel/index.ts => channel.ts} (86%) rename src/{editor => }/commands.ts (95%) rename src/{channel => services/context}/context.ts (100%) rename src/{channel => services/context}/state/Position.ts (100%) rename src/{channel => services/context}/state/Progress.ts (97%) rename src/{channel => services/context}/state/Tutorial.ts (94%) diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 00000000..37089b12 --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,2 @@ +export { default as onErrorPage } from './onErrorPage' +export { default as onTestPass } from './onTestPass' diff --git a/src/actions/onErrorPage.ts b/src/actions/onErrorPage.ts new file mode 100644 index 00000000..dd4dc742 --- /dev/null +++ b/src/actions/onErrorPage.ts @@ -0,0 +1,26 @@ +import * as T from 'typings' +import { readFile } from '../services/node' +import logger from '../services/logger' + +const onErrorPage = async (action: T.Action) => { + // Error middleware + if (action?.payload?.error?.type) { + // load error markdown message + const error = action.payload.error + const errorMarkdown = await readFile(__dirname, '..', '..', 'errors', `${action.payload.error.type}.md`).catch( + () => { + // onError(new Error(`Error Markdown file not found for ${action.type}`)) + }, + ) + + // log error to console for safe keeping + logger(`ERROR:\n ${errorMarkdown}`) + + if (errorMarkdown) { + // add a clearer error message for the user + error.message = `${errorMarkdown}\n\n${error.message}` + } + } +} + +export default onErrorPage diff --git a/src/actions/onTestPass.ts b/src/actions/onTestPass.ts new file mode 100644 index 00000000..0169a942 --- /dev/null +++ b/src/actions/onTestPass.ts @@ -0,0 +1,16 @@ +import * as git from '../services/git' +import * as T from 'typings' +import Context from '../services/context/context' + +const onTestPass = (action: T.Action, context: Context) => { + const tutorial = context.tutorial.get() + if (!tutorial) { + throw new Error('Error with current tutorial. Tutorial may be missing an id.') + } + // update local storage stepProgress + const progress = context.progress.setStepComplete(tutorial, action.payload.position.stepId) + context.position.setPositionFromProgress(tutorial, progress) + git.saveCommit('Save progress') +} + +export default onTestPass diff --git a/src/actions/saveCommit.ts b/src/actions/saveCommit.ts deleted file mode 100644 index 74002c5e..00000000 --- a/src/actions/saveCommit.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as git from '../services/git' - -async function saveCommit(): Promise { - git.saveCommit('Save progress') -} - -export default saveCommit diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index d01510f3..5084d735 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -1,7 +1,7 @@ import * as E from 'typings/error' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' -import { COMMANDS } from '../editor/commands' +import { COMMANDS } from '../commands' import * as git from '../services/git' import { DISABLE_RUN_ON_SAVE } from '../environment' diff --git a/src/actions/utils/loadWatchers.ts b/src/actions/utils/loadWatchers.ts index 9dab23ec..978a3ce2 100644 --- a/src/actions/utils/loadWatchers.ts +++ b/src/actions/utils/loadWatchers.ts @@ -1,6 +1,6 @@ import * as chokidar from 'chokidar' import * as vscode from 'vscode' -import { COMMANDS } from '../../editor/commands' +import { COMMANDS } from '../../commands' import { WORKSPACE_ROOT } from '../../environment' // NOTE: vscode createFileWatcher doesn't seem to detect changes outside of vscode diff --git a/src/actions/utils/openFiles.ts b/src/actions/utils/openFiles.ts index 34580f04..35e97710 100644 --- a/src/actions/utils/openFiles.ts +++ b/src/actions/utils/openFiles.ts @@ -1,6 +1,6 @@ import { join } from 'path' import * as vscode from 'vscode' -import { COMMANDS } from '../../editor/commands' +import { COMMANDS } from '../../commands' const openFiles = async (files: string[]) => { if (!files.length) { diff --git a/src/channel/index.ts b/src/channel.ts similarity index 86% rename from src/channel/index.ts rename to src/channel.ts index 19cff9e4..4aceefb8 100644 --- a/src/channel/index.ts +++ b/src/channel.ts @@ -4,25 +4,20 @@ import * as E from 'typings/error' import * as vscode from 'vscode' import fetch from 'node-fetch' import { satisfies } from 'semver' -import saveCommit from '../actions/saveCommit' -import { setupActions, solutionActions } from '../actions/setupActions' -import tutorialConfig from '../actions/tutorialConfig' -import { COMMANDS } from '../editor/commands' -import Context from './context' -import { readFile } from 'fs' -import { join } from 'path' -import { promisify } from 'util' -import logger from '../services/logger' -import { version, compareVersions } from '../services/dependencies' -import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' -import { showOutput } from '../services/testRunner/output' -import { exec } from '../services/node' -import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment' -import reset from '../services/reset' -import getLastCommitHash from '../services/reset/lastHash' -import { onEvent } from '../services/telemetry' - -const readFileAsync = promisify(readFile) +import { setupActions, solutionActions } from './actions/setupActions' +import tutorialConfig from './actions/tutorialConfig' +import { COMMANDS } from './commands' +import Context from './services/context/context' +import logger from './services/logger' +import { version, compareVersions } from './services/dependencies' +import { openWorkspace, checkWorkspaceEmpty } from './services/workspace' +import { showOutput } from './services/testRunner/output' +import { exec } from './services/node' +import { WORKSPACE_ROOT, TUTORIAL_URL } from './environment' +import reset from './services/reset' +import getLastCommitHash from './services/reset/lastHash' +import { onEvent } from './services/telemetry' +import * as actions from './actions' interface Channel { receive(action: T.Action): Promise @@ -359,24 +354,8 @@ class Channel implements Channel { } // send to webview public send = async (action: T.Action): Promise => { - // Error middleware - if (action?.payload?.error?.type) { - // load error markdown message - const error = action.payload.error - const errorMarkdownFile = join(__dirname, '..', '..', 'errors', `${action.payload.error.type}.md`) - const errorMarkdown = await readFileAsync(errorMarkdownFile).catch(() => { - // onError(new Error(`Error Markdown file not found for ${action.type}`)) - }) - - // log error to console for safe keeping - logger(`ERROR:\n ${errorMarkdown}`) - - if (errorMarkdown) { - // add a clearer error message for the user - error.message = `${errorMarkdown}\n\n${error.message}` - } - } - + // load error page if error action is triggered + actions.onErrorPage(action) // action may be an object.type or plain string const actionType: string = typeof action === 'string' ? action : action.type @@ -384,14 +363,7 @@ class Channel implements Channel { switch (actionType) { case 'TEST_PASS': - const tutorial = this.context.tutorial.get() - if (!tutorial) { - throw new Error('Error with current tutorial. Tutorial may be missing an id.') - } - // update local storage stepProgress - const progress = this.context.progress.setStepComplete(tutorial, action.payload.position.stepId) - this.context.position.setPositionFromProgress(tutorial, progress) - saveCommit() + actions.onTestPass(action, this.context) } // send message diff --git a/src/editor/commands.ts b/src/commands.ts similarity index 95% rename from src/editor/commands.ts rename to src/commands.ts index a7ab6915..3f9989d4 100644 --- a/src/editor/commands.ts +++ b/src/commands.ts @@ -1,10 +1,10 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' -import createTestRunner from '../services/testRunner' -import { setupActions } from '../actions/setupActions' -import createWebView from '../services/webview' -import logger from '../services/logger' +import createTestRunner from './services/testRunner' +import { setupActions } from './actions/setupActions' +import createWebView from './services/webview' +import logger from './services/logger' export const COMMANDS = { START: 'coderoad.start', diff --git a/src/extension.ts b/src/extension.ts index 1f64f83b..49a65006 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode' -import { createCommands } from './editor/commands' +import { createCommands } from './commands' import * as telemetry from './services/telemetry' let onDeactivate = () => {} diff --git a/src/channel/context.ts b/src/services/context/context.ts similarity index 100% rename from src/channel/context.ts rename to src/services/context/context.ts diff --git a/src/channel/state/Position.ts b/src/services/context/state/Position.ts similarity index 100% rename from src/channel/state/Position.ts rename to src/services/context/state/Position.ts diff --git a/src/channel/state/Progress.ts b/src/services/context/state/Progress.ts similarity index 97% rename from src/channel/state/Progress.ts rename to src/services/context/state/Progress.ts index 691d52bf..8bd2daa8 100644 --- a/src/channel/state/Progress.ts +++ b/src/services/context/state/Progress.ts @@ -1,7 +1,7 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' -import Storage from '../../services/storage' +import Storage from '../../storage' const defaultValue: T.Progress = { levels: {}, diff --git a/src/channel/state/Tutorial.ts b/src/services/context/state/Tutorial.ts similarity index 94% rename from src/channel/state/Tutorial.ts rename to src/services/context/state/Tutorial.ts index 3a91d19c..4f0ccb55 100644 --- a/src/channel/state/Tutorial.ts +++ b/src/services/context/state/Tutorial.ts @@ -1,6 +1,6 @@ import * as TT from 'typings/tutorial' import * as vscode from 'vscode' -import Storage from '../../services/storage' +import Storage from '../../storage' // Tutorial class Tutorial { diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 7026c7a6..a90dd208 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -6,6 +6,7 @@ import { WORKSPACE_ROOT } from '../../environment' const asyncExec = promisify(cpExec) const asyncRemoveFile = promisify(fs.unlink) +const asyncReadFile = promisify(fs.readFile) interface ExecParams { command: string @@ -24,3 +25,7 @@ export const exists = (...paths: string[]): boolean | never => { export const removeFile = (...paths: string[]) => { return asyncRemoveFile(join(WORKSPACE_ROOT, ...paths)) } + +export const readFile = (...paths: string[]) => { + return asyncReadFile(join(...paths)) +} From 7f12a0f362307eda078f803c5a55dab1b1d3de7a Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 21:05:23 -0700 Subject: [PATCH 3/8] refactor onEditorStartup Signed-off-by: shmck --- src/actions/index.ts | 1 + src/actions/onStartup.ts | 79 ++++++++++++++++++++++++++++++++++++++++ src/channel.ts | 65 +-------------------------------- 3 files changed, 81 insertions(+), 64 deletions(-) create mode 100644 src/actions/onStartup.ts diff --git a/src/actions/index.ts b/src/actions/index.ts index 37089b12..ae5091aa 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,2 +1,3 @@ +export { default as onStartup } from './onStartup' export { default as onErrorPage } from './onErrorPage' export { default as onTestPass } from './onTestPass' diff --git a/src/actions/onStartup.ts b/src/actions/onStartup.ts new file mode 100644 index 00000000..2c3de424 --- /dev/null +++ b/src/actions/onStartup.ts @@ -0,0 +1,79 @@ +import * as vscode from 'vscode' +import * as T from 'typings' +import * as TT from 'typings/tutorial' +import * as E from 'typings/error' +import Context from '../services/context/context' +import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment' +import fetch from 'node-fetch' +import logger from '../services/logger' + +const onStartup = async ( + context: Context, + workspaceState: vscode.Memento, + send: (action: T.Action) => Promise, +) => { + try { + // check if a workspace is open, otherwise nothing works + const noActiveWorkspace = !WORKSPACE_ROOT.length + if (noActiveWorkspace) { + const error: E.ErrorMessage = { + type: 'NoWorkspaceFound', + message: '', + actions: [ + { + label: 'Open Workspace', + transition: 'REQUEST_WORKSPACE', + }, + ], + } + send({ type: 'NO_WORKSPACE', payload: { error } }) + return + } + + const env = { + machineId: vscode.env.machineId, + sessionId: vscode.env.sessionId, + } + + // load tutorial from url + if (TUTORIAL_URL) { + try { + const tutorialRes = await fetch(TUTORIAL_URL) + const tutorial = await tutorialRes.json() + send({ type: 'START_TUTORIAL_FROM_URL', payload: { tutorial } }) + return + } catch (e) { + console.log(`Failed to load tutorial from url ${TUTORIAL_URL} with error "${e.message}"`) + } + } + + // continue from tutorial from local storage + const tutorial: TT.Tutorial | null = context.tutorial.get() + + // no stored tutorial, must start new tutorial + if (!tutorial || !tutorial.id) { + send({ type: 'START_NEW_TUTORIAL', payload: { env } }) + return + } + + // load continued tutorial position & progress + const { position, progress } = await context.setTutorial(workspaceState, tutorial) + logger('CONTINUE STATE', position, progress) + + if (progress.complete) { + // tutorial is already complete + send({ type: 'TUTORIAL_ALREADY_COMPLETE', payload: { env } }) + return + } + // communicate to client the tutorial & stepProgress state + send({ type: 'LOAD_STORED_TUTORIAL', payload: { env, tutorial, progress, position } }) + } catch (e) { + const error = { + type: 'UnknownError', + message: `Location: Editor startup\n\n${e.message}`, + } + send({ type: 'EDITOR_STARTUP_FAILED', payload: { error } }) + } +} + +export default onStartup diff --git a/src/channel.ts b/src/channel.ts index 4aceefb8..446ffd7d 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -2,7 +2,6 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' import * as E from 'typings/error' import * as vscode from 'vscode' -import fetch from 'node-fetch' import { satisfies } from 'semver' import { setupActions, solutionActions } from './actions/setupActions' import tutorialConfig from './actions/tutorialConfig' @@ -13,7 +12,6 @@ import { version, compareVersions } from './services/dependencies' import { openWorkspace, checkWorkspaceEmpty } from './services/workspace' import { showOutput } from './services/testRunner/output' import { exec } from './services/node' -import { WORKSPACE_ROOT, TUTORIAL_URL } from './environment' import reset from './services/reset' import getLastCommitHash from './services/reset/lastHash' import { onEvent } from './services/telemetry' @@ -50,68 +48,7 @@ class Channel implements Channel { switch (actionType) { case 'EDITOR_STARTUP': - try { - // check if a workspace is open, otherwise nothing works - const noActiveWorkspace = !WORKSPACE_ROOT.length - if (noActiveWorkspace) { - const error: E.ErrorMessage = { - type: 'NoWorkspaceFound', - message: '', - actions: [ - { - label: 'Open Workspace', - transition: 'REQUEST_WORKSPACE', - }, - ], - } - this.send({ type: 'NO_WORKSPACE', payload: { error } }) - return - } - - const env = { - machineId: vscode.env.machineId, - sessionId: vscode.env.sessionId, - } - - // load tutorial from url - if (TUTORIAL_URL) { - try { - const tutorialRes = await fetch(TUTORIAL_URL) - const tutorial = await tutorialRes.json() - this.send({ type: 'START_TUTORIAL_FROM_URL', payload: { tutorial } }) - return - } catch (e) { - console.log(`Failed to load tutorial from url ${TUTORIAL_URL} with error "${e.message}"`) - } - } - - // continue from tutorial from local storage - const tutorial: TT.Tutorial | null = this.context.tutorial.get() - - // no stored tutorial, must start new tutorial - if (!tutorial || !tutorial.id) { - this.send({ type: 'START_NEW_TUTORIAL', payload: { env } }) - return - } - - // load continued tutorial position & progress - const { position, progress } = await this.context.setTutorial(this.workspaceState, tutorial) - logger('CONTINUE STATE', position, progress) - - if (progress.complete) { - // tutorial is already complete - this.send({ type: 'TUTORIAL_ALREADY_COMPLETE', payload: { env } }) - return - } - // communicate to client the tutorial & stepProgress state - this.send({ type: 'LOAD_STORED_TUTORIAL', payload: { env, tutorial, progress, position } }) - } catch (e) { - const error = { - type: 'UnknownError', - message: `Location: Editor startup\n\n${e.message}`, - } - this.send({ type: 'EDITOR_STARTUP_FAILED', payload: { error } }) - } + actions.onStartup(this.context, this.workspaceState, this.send) return // clear tutorial local storage From ae3ee13736743e18c0651cc3e75ba2192ed2096e Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 21:13:02 -0700 Subject: [PATCH 4/8] refactor onTutorialConfig Signed-off-by: shmck --- src/actions/index.ts | 1 + src/actions/onTutorialConfig.ts | 121 ++++++++++++++++++++++ src/actions/{ => utils}/tutorialConfig.ts | 6 +- src/channel.ts | 116 +-------------------- 4 files changed, 129 insertions(+), 115 deletions(-) create mode 100644 src/actions/onTutorialConfig.ts rename src/actions/{ => utils}/tutorialConfig.ts (93%) diff --git a/src/actions/index.ts b/src/actions/index.ts index ae5091aa..3049b498 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,3 +1,4 @@ export { default as onStartup } from './onStartup' +export { default as onTutorialConfig } from './onTutorialConfig' export { default as onErrorPage } from './onErrorPage' export { default as onTestPass } from './onTestPass' diff --git a/src/actions/onTutorialConfig.ts b/src/actions/onTutorialConfig.ts new file mode 100644 index 00000000..7a1f7221 --- /dev/null +++ b/src/actions/onTutorialConfig.ts @@ -0,0 +1,121 @@ +import * as vscode from 'vscode' +import * as T from 'typings' +import * as TT from 'typings/tutorial' +import * as E from 'typings/error' +import { satisfies } from 'semver' +import { onEvent } from '../services/telemetry' +import { version, compareVersions } from '../services/dependencies' +import Context from '../services/context/context' +import tutorialConfig from './utils/tutorialConfig' + +const onTutorialConfig = async (action: T.Action, context: Context, workspaceState: vscode.Memento, send: any) => { + try { + const data: TT.Tutorial = action.payload.tutorial + + onEvent('tutorial_start', { + tutorial_id: data.id, + tutorial_version: data.version, + tutorial_title: data.summary.title, + }) + + // validate extension version + const expectedAppVersion = data.config?.appVersions?.vscode + if (expectedAppVersion) { + const extension = vscode.extensions.getExtension('coderoad.coderoad') + if (extension) { + const currentAppVersion = extension.packageJSON.version + const satisfied = satisfies(currentAppVersion, expectedAppVersion) + if (!satisfied) { + const error: E.ErrorMessage = { + type: 'UnmetExtensionVersion', + message: `Expected CodeRoad v${expectedAppVersion}, but found v${currentAppVersion}`, + } + send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) + return + } + } + } + + // setup tutorial config (save watcher, test runner, etc) + await context.setTutorial(workspaceState, data) + + // validate dependencies + const dependencies = data.config.dependencies + if (dependencies && dependencies.length) { + for (const dep of dependencies) { + // check dependency is installed + const currentVersion: string | null = await version(dep.name) + if (!currentVersion) { + // use a custom error message + const error: E.ErrorMessage = { + type: 'MissingTutorialDependency', + message: dep.message || `Process "${dep.name}" is required but not found. It may need to be installed`, + actions: [ + { + label: 'Check Again', + transition: 'TRY_AGAIN', + }, + ], + } + send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) + return + } + + // check dependency version + const satisfiedDependency = await compareVersions(currentVersion, dep.version) + + if (!satisfiedDependency) { + const error: E.ErrorMessage = { + type: 'UnmetTutorialDependency', + message: `Expected ${dep.name} to have version ${dep.version}, but found version ${currentVersion}`, + actions: [ + { + label: 'Check Again', + transition: 'TRY_AGAIN', + }, + ], + } + send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) + return + } + + if (satisfiedDependency !== true) { + const error: E.ErrorMessage = satisfiedDependency || { + type: 'UnknownError', + message: `Something went wrong comparing dependency for ${name}`, + actions: [ + { + label: 'Try Again', + transition: 'TRY_AGAIN', + }, + ], + } + send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) + return + } + } + } + + const error: E.ErrorMessage | void = await tutorialConfig({ data }).catch((error: Error) => ({ + type: 'UnknownError', + message: `Location: tutorial config.\n\n${error.message}`, + })) + + // has error + if (error && error.type) { + send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) + return + } + + // report back to the webview that setup is complete + send({ type: 'TUTORIAL_CONFIGURED' }) + } catch (e) { + const error = { + type: 'UnknownError', + message: `Location: EditorTutorialConfig.\n\n ${e.message}`, + } + send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) + } +} + +export default onTutorialConfig diff --git a/src/actions/tutorialConfig.ts b/src/actions/utils/tutorialConfig.ts similarity index 93% rename from src/actions/tutorialConfig.ts rename to src/actions/utils/tutorialConfig.ts index 5084d735..a4ef01d9 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/utils/tutorialConfig.ts @@ -1,9 +1,9 @@ import * as E from 'typings/error' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' -import { COMMANDS } from '../commands' -import * as git from '../services/git' -import { DISABLE_RUN_ON_SAVE } from '../environment' +import { COMMANDS } from '../../commands' +import * as git from '../../services/git' +import { DISABLE_RUN_ON_SAVE } from '../../environment' interface TutorialConfigParams { data: TT.Tutorial diff --git a/src/channel.ts b/src/channel.ts index 446ffd7d..16c11978 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -2,19 +2,18 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' import * as E from 'typings/error' import * as vscode from 'vscode' -import { satisfies } from 'semver' import { setupActions, solutionActions } from './actions/setupActions' -import tutorialConfig from './actions/tutorialConfig' +import tutorialConfig from './actions/utils/tutorialConfig' import { COMMANDS } from './commands' import Context from './services/context/context' import logger from './services/logger' -import { version, compareVersions } from './services/dependencies' +import { version } from './services/dependencies' import { openWorkspace, checkWorkspaceEmpty } from './services/workspace' import { showOutput } from './services/testRunner/output' import { exec } from './services/node' import reset from './services/reset' import getLastCommitHash from './services/reset/lastHash' -import { onEvent } from './services/telemetry' + import * as actions from './actions' interface Channel { @@ -58,114 +57,7 @@ class Channel implements Channel { return // configure test runner, language, git case 'EDITOR_TUTORIAL_CONFIG': - try { - const data: TT.Tutorial = action.payload.tutorial - - onEvent('tutorial_start', { - tutorial_id: data.id, - tutorial_version: data.version, - tutorial_title: data.summary.title, - }) - - // validate extension version - const expectedAppVersion = data.config?.appVersions?.vscode - if (expectedAppVersion) { - const extension = vscode.extensions.getExtension('coderoad.coderoad') - if (extension) { - const currentAppVersion = extension.packageJSON.version - const satisfied = satisfies(currentAppVersion, expectedAppVersion) - if (!satisfied) { - const error: E.ErrorMessage = { - type: 'UnmetExtensionVersion', - message: `Expected CodeRoad v${expectedAppVersion}, but found v${currentAppVersion}`, - } - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) - return - } - } - } - - // setup tutorial config (save watcher, test runner, etc) - await this.context.setTutorial(this.workspaceState, data) - - // validate dependencies - const dependencies = data.config.dependencies - if (dependencies && dependencies.length) { - for (const dep of dependencies) { - // check dependency is installed - const currentVersion: string | null = await version(dep.name) - if (!currentVersion) { - // use a custom error message - const error: E.ErrorMessage = { - type: 'MissingTutorialDependency', - message: - dep.message || `Process "${dep.name}" is required but not found. It may need to be installed`, - actions: [ - { - label: 'Check Again', - transition: 'TRY_AGAIN', - }, - ], - } - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) - return - } - - // check dependency version - const satisfiedDependency = await compareVersions(currentVersion, dep.version) - - if (!satisfiedDependency) { - const error: E.ErrorMessage = { - type: 'UnmetTutorialDependency', - message: `Expected ${dep.name} to have version ${dep.version}, but found version ${currentVersion}`, - actions: [ - { - label: 'Check Again', - transition: 'TRY_AGAIN', - }, - ], - } - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) - return - } - - if (satisfiedDependency !== true) { - const error: E.ErrorMessage = satisfiedDependency || { - type: 'UnknownError', - message: `Something went wrong comparing dependency for ${name}`, - actions: [ - { - label: 'Try Again', - transition: 'TRY_AGAIN', - }, - ], - } - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) - return - } - } - } - - const error: E.ErrorMessage | void = await tutorialConfig({ data }).catch((error: Error) => ({ - type: 'UnknownError', - message: `Location: tutorial config.\n\n${error.message}`, - })) - - // has error - if (error && error.type) { - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) - return - } - - // report back to the webview that setup is complete - this.send({ type: 'TUTORIAL_CONFIGURED' }) - } catch (e) { - const error = { - type: 'UnknownError', - message: `Location: EditorTutorialConfig.\n\n ${e.message}`, - } - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) - } + actions.onTutorialConfig(action, this.context, this.workspaceState, this.send) return case 'EDITOR_TUTORIAL_CONTINUE_CONFIG': try { From 15166d1ac544cf117796fc02cd9a96a0fb29aa2c Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 21:16:49 -0700 Subject: [PATCH 5/8] refactor onTutorialContinueConfig Signed-off-by: shmck --- src/actions/index.ts | 1 + src/actions/onTutorialContinueConfig.ts | 29 +++++++++++++++++++++++++ src/channel.ts | 20 +---------------- 3 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 src/actions/onTutorialContinueConfig.ts diff --git a/src/actions/index.ts b/src/actions/index.ts index 3049b498..975765e3 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,4 +1,5 @@ export { default as onStartup } from './onStartup' export { default as onTutorialConfig } from './onTutorialConfig' +export { default as onTutorialContinueConfig } from './onTutorialContinueConfig' export { default as onErrorPage } from './onErrorPage' export { default as onTestPass } from './onTestPass' diff --git a/src/actions/onTutorialContinueConfig.ts b/src/actions/onTutorialContinueConfig.ts new file mode 100644 index 00000000..2610b14b --- /dev/null +++ b/src/actions/onTutorialContinueConfig.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode' +import * as T from 'typings' +import * as TT from 'typings/tutorial' +import Context from '../services/context/context' +import tutorialConfig from './utils/tutorialConfig' +import { COMMANDS } from '../commands' + +const onTutorialContinueConfig = async (action: T.Action, context: Context, send: any) => { + try { + const tutorialContinue: TT.Tutorial | null = context.tutorial.get() + if (!tutorialContinue) { + throw new Error('Invalid tutorial to continue') + } + await tutorialConfig({ + data: tutorialContinue, + alreadyConfigured: true, + }) + // update the current stepId on startup + vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position) + } catch (e) { + const error = { + type: 'UnknownError', + message: `Location: Editor tutorial continue config.\n\n ${e.message}`, + } + send({ type: 'CONTINUE_FAILED', payload: { error } }) + } +} + +export default onTutorialContinueConfig diff --git a/src/channel.ts b/src/channel.ts index 16c11978..d339df83 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -3,7 +3,6 @@ import * as TT from 'typings/tutorial' import * as E from 'typings/error' import * as vscode from 'vscode' import { setupActions, solutionActions } from './actions/setupActions' -import tutorialConfig from './actions/utils/tutorialConfig' import { COMMANDS } from './commands' import Context from './services/context/context' import logger from './services/logger' @@ -60,24 +59,7 @@ class Channel implements Channel { actions.onTutorialConfig(action, this.context, this.workspaceState, this.send) return case 'EDITOR_TUTORIAL_CONTINUE_CONFIG': - try { - const tutorialContinue: TT.Tutorial | null = this.context.tutorial.get() - if (!tutorialContinue) { - throw new Error('Invalid tutorial to continue') - } - await tutorialConfig({ - data: tutorialContinue, - alreadyConfigured: true, - }) - // update the current stepId on startup - vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position) - } catch (e) { - const error = { - type: 'UnknownError', - message: `Location: Editor tutorial continue config.\n\n ${e.message}`, - } - this.send({ type: 'CONTINUE_FAILED', payload: { error } }) - } + actions.onTutorialContinueConfig(action, this.context, this.send) return case 'EDITOR_VALIDATE_SETUP': try { From 72ef62ca979a8f387a8358ff81eacb68666ce49e Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 21:19:05 -0700 Subject: [PATCH 6/8] refactor onValidateSetup Signed-off-by: shmck --- src/actions/index.ts | 1 + src/actions/onValidateSetup.ts | 54 ++++++++++++++++++++++++++++++++++ src/channel.ts | 52 ++------------------------------ 3 files changed, 57 insertions(+), 50 deletions(-) create mode 100644 src/actions/onValidateSetup.ts diff --git a/src/actions/index.ts b/src/actions/index.ts index 975765e3..a2315d22 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,5 +1,6 @@ export { default as onStartup } from './onStartup' export { default as onTutorialConfig } from './onTutorialConfig' export { default as onTutorialContinueConfig } from './onTutorialContinueConfig' +export { default as onValidateSetup } from './onValidateSetup' export { default as onErrorPage } from './onErrorPage' export { default as onTestPass } from './onTestPass' diff --git a/src/actions/onValidateSetup.ts b/src/actions/onValidateSetup.ts new file mode 100644 index 00000000..90657e90 --- /dev/null +++ b/src/actions/onValidateSetup.ts @@ -0,0 +1,54 @@ +import * as E from 'typings/error' +import { version } from '../services/dependencies' +import { checkWorkspaceEmpty } from '../services/workspace' + +const onValidateSetup = async (send: any) => { + try { + // check workspace is selected + const isEmptyWorkspace = await checkWorkspaceEmpty() + if (!isEmptyWorkspace) { + const error: E.ErrorMessage = { + type: 'WorkspaceNotEmpty', + message: '', + actions: [ + { + label: 'Open Workspace', + transition: 'REQUEST_WORKSPACE', + }, + { + label: 'Check Again', + transition: 'RETRY', + }, + ], + } + send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) + return + } + // check Git is installed. + // Should wait for workspace before running otherwise requires access to root folder + const isGitInstalled = await version('git') + if (!isGitInstalled) { + const error: E.ErrorMessage = { + type: 'GitNotFound', + message: '', + actions: [ + { + label: 'Check Again', + transition: 'RETRY', + }, + ], + } + send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) + return + } + send({ type: 'SETUP_VALIDATED' }) + } catch (e) { + const error = { + type: 'UknownError', + message: e.message, + } + send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) + } +} + +export default onValidateSetup diff --git a/src/channel.ts b/src/channel.ts index d339df83..9ffe6926 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,13 +1,11 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' -import * as E from 'typings/error' import * as vscode from 'vscode' import { setupActions, solutionActions } from './actions/setupActions' import { COMMANDS } from './commands' import Context from './services/context/context' import logger from './services/logger' -import { version } from './services/dependencies' -import { openWorkspace, checkWorkspaceEmpty } from './services/workspace' +import { openWorkspace } from './services/workspace' import { showOutput } from './services/testRunner/output' import { exec } from './services/node' import reset from './services/reset' @@ -48,7 +46,6 @@ class Channel implements Channel { case 'EDITOR_STARTUP': actions.onStartup(this.context, this.workspaceState, this.send) return - // clear tutorial local storage case 'TUTORIAL_CLEAR': // clear current progress/position/tutorial @@ -62,52 +59,7 @@ class Channel implements Channel { actions.onTutorialContinueConfig(action, this.context, this.send) return case 'EDITOR_VALIDATE_SETUP': - try { - // check workspace is selected - const isEmptyWorkspace = await checkWorkspaceEmpty() - if (!isEmptyWorkspace) { - const error: E.ErrorMessage = { - type: 'WorkspaceNotEmpty', - message: '', - actions: [ - { - label: 'Open Workspace', - transition: 'REQUEST_WORKSPACE', - }, - { - label: 'Check Again', - transition: 'RETRY', - }, - ], - } - this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) - return - } - // check Git is installed. - // Should wait for workspace before running otherwise requires access to root folder - const isGitInstalled = await version('git') - if (!isGitInstalled) { - const error: E.ErrorMessage = { - type: 'GitNotFound', - message: '', - actions: [ - { - label: 'Check Again', - transition: 'RETRY', - }, - ], - } - this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) - return - } - this.send({ type: 'SETUP_VALIDATED' }) - } catch (e) { - const error = { - type: 'UknownError', - message: e.message, - } - this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) - } + actions.onValidateSetup(this.send) return case 'EDITOR_REQUEST_WORKSPACE': openWorkspace() From a9d6561bff8bbdfb206c752e83f983e591db8b4d Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 21:22:19 -0700 Subject: [PATCH 7/8] refactor onRunReset Signed-off-by: shmck --- src/actions/index.ts | 1 + src/actions/onRunReset.ts | 32 ++++++++++++++++++++++++++++++++ src/channel.ts | 27 +-------------------------- 3 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 src/actions/onRunReset.ts diff --git a/src/actions/index.ts b/src/actions/index.ts index a2315d22..a2774cb9 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -2,5 +2,6 @@ export { default as onStartup } from './onStartup' export { default as onTutorialConfig } from './onTutorialConfig' export { default as onTutorialContinueConfig } from './onTutorialContinueConfig' export { default as onValidateSetup } from './onValidateSetup' +export { default as onRunReset } from './onRunReset' export { default as onErrorPage } from './onErrorPage' export { default as onTestPass } from './onTestPass' diff --git a/src/actions/onRunReset.ts b/src/actions/onRunReset.ts new file mode 100644 index 00000000..745c9123 --- /dev/null +++ b/src/actions/onRunReset.ts @@ -0,0 +1,32 @@ +import * as T from 'typings' +import * as TT from 'typings/tutorial' +import Context from '../services/context/context' +import { exec } from '../services/node' +import reset from '../services/reset' +import getLastCommitHash from '../services/reset/lastHash' + +const onRunReset = async (context: Context) => { + // reset to timeline + const tutorial: TT.Tutorial | null = context.tutorial.get() + const position: T.Position = context.position.get() + + // get last pass commit + const hash = getLastCommitHash(position, tutorial?.levels || []) + + const branch = tutorial?.config.repo.branch + + if (!branch) { + console.error('No repo branch found for tutorial') + return + } + + // load timeline until last pass commit + reset({ branch, hash }) + + // if tutorial.config.reset.command, run it + if (tutorial?.config?.reset?.command) { + await exec({ command: tutorial.config.reset.command }) + } +} + +export default onRunReset diff --git a/src/channel.ts b/src/channel.ts index 9ffe6926..ce39af67 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,5 +1,4 @@ import * as T from 'typings' -import * as TT from 'typings/tutorial' import * as vscode from 'vscode' import { setupActions, solutionActions } from './actions/setupActions' import { COMMANDS } from './commands' @@ -7,10 +6,6 @@ import Context from './services/context/context' import logger from './services/logger' import { openWorkspace } from './services/workspace' import { showOutput } from './services/testRunner/output' -import { exec } from './services/node' -import reset from './services/reset' -import getLastCommitHash from './services/reset/lastHash' - import * as actions from './actions' interface Channel { @@ -88,27 +83,7 @@ class Channel implements Channel { vscode.commands.executeCommand(COMMANDS.RUN_TEST, action?.payload) return case 'EDITOR_RUN_RESET': - // reset to timeline - const tutorial: TT.Tutorial | null = this.context.tutorial.get() - const position: T.Position = this.context.position.get() - - // get last pass commit - const hash = getLastCommitHash(position, tutorial?.levels || []) - - const branch = tutorial?.config.repo.branch - - if (!branch) { - console.error('No repo branch found for tutorial') - return - } - - // load timeline until last pass commit - reset({ branch, hash }) - - // if tutorial.config.reset.command, run it - if (tutorial?.config?.reset?.command) { - await exec({ command: tutorial.config.reset.command }) - } + actions.onRunReset(this.context) return default: logger(`No match for action type: ${actionType}`) From 0ec09029d1bf233469802a327abfe4ac46e774e1 Mon Sep 17 00:00:00 2001 From: shmck Date: Sun, 19 Jul 2020 21:27:22 -0700 Subject: [PATCH 8/8] refactor setup/solution actions Signed-off-by: shmck --- src/actions/index.ts | 1 + src/actions/{setupActions.ts => onActions.ts} | 6 +++--- src/channel.ts | 6 +++--- src/commands.ts | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) rename src/actions/{setupActions.ts => onActions.ts} (85%) diff --git a/src/actions/index.ts b/src/actions/index.ts index a2774cb9..a4c88726 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -5,3 +5,4 @@ export { default as onValidateSetup } from './onValidateSetup' export { default as onRunReset } from './onRunReset' export { default as onErrorPage } from './onErrorPage' export { default as onTestPass } from './onTestPass' +export { onSetupActions, onSolutionActions } from './onActions' diff --git a/src/actions/setupActions.ts b/src/actions/onActions.ts similarity index 85% rename from src/actions/setupActions.ts rename to src/actions/onActions.ts index 21e74fb3..1e3d4a05 100644 --- a/src/actions/setupActions.ts +++ b/src/actions/onActions.ts @@ -13,7 +13,7 @@ interface SetupActions { dir?: string } -export const setupActions = async ({ actions, send, dir }: SetupActions): Promise => { +export const onSetupActions = async ({ actions, send, dir }: SetupActions): Promise => { if (!actions) { return } @@ -49,7 +49,7 @@ export const setupActions = async ({ actions, send, dir }: SetupActions): Promis } } -export const solutionActions = async (params: SetupActions): Promise => { +export const onSolutionActions = async (params: SetupActions): Promise => { await git.clear() - return setupActions(params).catch(onError) + return onSetupActions(params).catch(onError) } diff --git a/src/channel.ts b/src/channel.ts index ce39af67..1a7b116d 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,6 +1,6 @@ import * as T from 'typings' import * as vscode from 'vscode' -import { setupActions, solutionActions } from './actions/setupActions' +import { setupActions, solutionActions } from './actions/onActions' import { COMMANDS } from './commands' import Context from './services/context/context' import logger from './services/logger' @@ -62,12 +62,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) - setupActions({ actions: action.payload.actions, send: this.send }) + actions.onSetupActions({ actions: action.payload.actions, send: this.send }) 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 solutionActions({ actions: action.payload.actions, send: this.send }) + await actions.onSolutionActions({ actions: action.payload.actions, send: this.send }) // run test following solution to update position vscode.commands.executeCommand(COMMANDS.RUN_TEST) return diff --git a/src/commands.ts b/src/commands.ts index 3f9989d4..6b36ec62 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,7 +2,7 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' import createTestRunner from './services/testRunner' -import { setupActions } from './actions/setupActions' +import { onSetupActions } from './actions/onActions' import createWebView from './services/webview' import logger from './services/logger' @@ -57,7 +57,7 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP if (setup) { // setup tutorial test runner commits // assumes git already exists - await setupActions({ + await onSetupActions({ actions: setup, send: webview.send, dir: testRunnerConfig.directory || testRunnerConfig.path,