From aa2cfa2c17ebc91e87f2a0df442cecff7fe55e6a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Oct 2020 23:17:28 -0400 Subject: [PATCH 01/26] typings/plugin.d.ts: Create --- typings/plugin.d.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 typings/plugin.d.ts diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts new file mode 100644 index 000000000..92c3acada --- /dev/null +++ b/typings/plugin.d.ts @@ -0,0 +1,110 @@ +/** + * This file describes the code-server plugin API for adding new applications. + */ +import { Logger } from "@coder/logger" +import * as express from "express" + +/** + * Overlay + * + * The homepage of code-server will launch into VS Code. However, there will be an overlay + * button that when clicked, will show all available applications with their names, + * icons and provider plugins. When one clicks on an app's icon, they will be directed + * to // to access the application. + */ + +/** + * Plugins + * + * Plugins are just node modules. + * + * code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * $CS_PLUGIN_PATH with a package.json where the engine is code-server is + * a valid plugin. + * + * e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search + * /tmp/nhooyr and then /tmp/ash for plugins. + * + * CS_PLUGIN_PATH defaults to + * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins + * if unset. + * + * code-server also uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * Built in plugins are also loaded from __dirname/../plugins + * + * Priority is $CS_PLUGIN, $CS_PLUGIN_PATH and then the builtin plugins. + * After the search is complete, plugins will be required in first found order and + * initialized. See the Plugin interface for details. + * + * There is also a /api/applications endpoint to allow programmatic access to all + * available applications. It could be used to create a custom application dashboard + * for example. + */ + +/** + * Your plugin module must implement this interface. + * + * The plugin's name, description and version are fetched from its module's package.json + * + * The plugin's router will be mounted at / + * + * If two plugins are found with the exact same name, then code-server will + * use the last one and emit a warning. + */ +export interface Plugin { + /** + * init is called so that the plugin may initialize itself with the config. + */ + init(config: PluginConfig): void + + /** + * Returns the plugin's router. + */ + router(): express.Router + + /** + * code-server uses this to collect the list of applications that + * the plugin can currently provide. + * It is called when /api/applications is hit or the overlay needs to + * refresh the list of applications + * + * Ensure this is as fast as possible. + */ + applications(): Application[] | Promise +} + +/** + * PluginConfig contains the configuration required for initializing + * a plugin. + */ +export interface PluginConfig { + /** + * All plugin logs should be logged via this logger. + */ + readonly logger: Logger +} + +/** + * Application represents a user accessible application. + * + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If the app's name is the same as the plugin's name then + * / will be used instead. + */ +export interface Application { + readonly name: string + readonly version: string + + /** + * The path at which the icon for this application can be accessed. + * /// + */ + readonly iconPath: string +} From 481df70622e260b8628af76c70933db419ac4f33 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Oct 2020 23:18:07 -0400 Subject: [PATCH 02/26] ci/dev/test.sh: Pass through args --- ci/dev/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 031bacf99..983b2f292 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,7 +4,7 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - mocha -r ts-node/register ./test/*.test.ts + mocha -r ts-node/register ./test/*.test.ts "$@" } main "$@" From e08a55d44a1067c54cc845efd54cc31271c3ae08 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:18:45 -0400 Subject: [PATCH 03/26] src/node/plugin.ts: Implement new plugin API --- src/node/plugin.ts | 239 ++++++++++++++++++++++++++------------- src/node/routes/index.ts | 6 +- 2 files changed, 166 insertions(+), 79 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 20c19d3e7..2ae29b967 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,92 +1,177 @@ -import { field, logger } from "@coder/logger" -import { Express } from "express" -import * as fs from "fs" import * as path from "path" -import * as util from "util" -import { Args } from "./cli" -import { paths } from "./util" +import * as util from "./util" +import * as pluginapi from "../../typings/plugin" +import * as fs from "fs" +import * as semver from "semver" +import { version } from "./constants" +const fsp = fs.promises +import { Logger, field } from "@coder/logger" +import * as express from "express" -/* eslint-disable @typescript-eslint/no-var-requires */ +// These fields are populated from the plugin's package.json. +interface Plugin extends pluginapi.Plugin { + name: string + version: string + description: string +} -export type Activate = (app: Express, args: Args) => void - -/** - * Plugins must implement this interface. - */ -export interface Plugin { - activate: Activate +interface Application extends pluginapi.Application { + plugin: Plugin } /** - * Intercept imports so we can inject code-server when the plugin tries to - * import it. + * PluginAPI implements the plugin API described in typings/plugin.d.ts + * Please see that file for details. */ -const originalLoad = require("module")._load -// eslint-disable-next-line @typescript-eslint/no-explicit-any -require("module")._load = function (request: string, parent: object, isMain: boolean): any { - return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) -} +export class PluginAPI { + private readonly plugins = new Array() + private readonly logger: Logger -/** - * Load a plugin and run its activation function. - */ -const loadPlugin = async (pluginPath: string, app: Express, args: Args): Promise => { - try { - const plugin: Plugin = require(pluginPath) - plugin.activate(app, args) - - const packageJson = require(path.join(pluginPath, "package.json")) - logger.debug( - "Loaded plugin", - field("name", packageJson.name || path.basename(pluginPath)), - field("path", pluginPath), - field("version", packageJson.version || "n/a"), - ) - } catch (error) { - logger.error(error.message) + public constructor( + logger: Logger, + /** + * These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively. + */ + private readonly csPlugin = "", + private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, + ){ + this.logger = logger.named("pluginapi") } -} -/** - * Load all plugins in the specified directory. - */ -const _loadPlugins = async (pluginDir: string, app: Express, args: Args): Promise => { - try { - const files = await util.promisify(fs.readdir)(pluginDir, { - withFileTypes: true, - }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), app, args))) - } catch (error) { - if (error.code !== "ENOENT") { - logger.warn(error.message) + /** + * applications grabs the full list of applications from + * all loaded plugins. + */ + public async applications(): Promise { + const apps = new Array() + for (let p of this.plugins) { + const pluginApps = await p.applications() + + // TODO prevent duplicates + // Add plugin key to each app. + apps.push( + ...pluginApps.map((app) => { + return { ...app, plugin: p } + }), + ) + } + return apps + } + + /** + * mount mounts all plugin routers onto r. + */ + public mount(r: express.Router): void { + for (let p of this.plugins) { + r.use(`/${p.name}`, p.router()) } } + + /** + * loadPlugins loads all plugins based on this.csPluginPath + * and this.csPlugin. + */ + public async loadPlugins(): Promise { + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) + + for (let dir of this.csPluginPath.split(":")) { + if (!dir) { + continue + } + await this._loadPlugins(dir) + } + + for (let dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } + } + + private async _loadPlugins(dir: string): Promise { + try { + const entries = await fsp.readdir(dir, { withFileTypes: true }) + for (let ent of entries) { + if (!ent.isDirectory()) { + continue + } + await this.loadPlugin(path.join(dir, ent.name)) + } + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`) + } + } + } + + private async loadPlugin(dir: string): Promise { + try { + const str = await fsp.readFile(path.join(dir, "package.json"), { + encoding: "utf8", + }) + const packageJSON: PackageJSON = JSON.parse(str) + const p = this._loadPlugin(dir, packageJSON) + // TODO prevent duplicates + this.plugins.push(p) + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugin: ${err.message}`) + } + } + } + + private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + const logger = this.logger.named(packageJSON.name) + logger.debug("loading plugin", + field("plugin_dir", dir), + field("package_json", packageJSON), + ) + + if (!semver.satisfies(version, packageJSON.engines["code-server"])) { + throw new Error(`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + + ` with code-server version ${version}`) + } + if (!packageJSON.name) { + throw new Error("plugin missing name") + } + if (!packageJSON.version) { + throw new Error("plugin missing version") + } + if (!packageJSON.description) { + throw new Error("plugin missing description") + } + + const p = { + name: packageJSON.name, + version: packageJSON.version, + description: packageJSON.description, + ...require(dir), + } as Plugin + + p.init({ + logger: logger, + }) + + logger.debug("loaded") + + return p + } } -/** - * Load all plugins from the `plugins` directory, directories specified by - * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by - * `CS_PLUGIN` (also colon-separated). - */ -export const loadPlugins = async (app: Express, args: Args): Promise => { - const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` - const plugin = process.env.CS_PLUGIN || "" - await Promise.all([ - // Built-in plugins. - _loadPlugins(path.resolve(__dirname, "../../plugins"), app, args), - // User-added plugins. - ...pluginPath - .split(":") - .filter((p) => !!p) - .map((dir) => _loadPlugins(path.resolve(dir), app, args)), - // Individual plugins so you don't have to symlink or move them into a - // directory specifically for plugins. This lets you load plugins that are - // on the same level as other directories that are not plugins (if you tried - // to use CS_PLUGIN_PATH code-server would try to load those other - // directories as plugins). Intended for development. - ...plugin - .split(":") - .filter((p) => !!p) - .map((dir) => loadPlugin(path.resolve(dir), app, args)), - ]) +interface PackageJSON { + name: string + version: string + description: string + engines: { + "code-server": string + } +} + +function q(s: string): string { + if (s === undefined) { + s = "undefined" + } + return JSON.stringify(s) } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index afb24f156..5824475d9 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -12,7 +12,7 @@ import { AuthType, DefaultedArgs } from "../cli" import { rootPath } from "../constants" import { Heart } from "../heart" import { replaceTemplates } from "../http" -import { loadPlugins } from "../plugin" +import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" @@ -115,7 +115,9 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - await loadPlugins(app, args) + const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) + await papi.loadPlugins() + papi.mount(app) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From bea185b8b2cf3487e4dc63c95d822bca76d3a030 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:18:59 -0400 Subject: [PATCH 04/26] plugin: Add basic loading test Will work on testing overlay next. --- test/plugin.test.ts | 31 +++ test/test-plugin/.gitignore | 1 + test/test-plugin/package.json | 20 ++ test/test-plugin/public/icon.svg | 1 + test/test-plugin/src/index.ts | 23 ++ test/test-plugin/tsconfig.json | 69 +++++ test/test-plugin/yarn.lock | 435 +++++++++++++++++++++++++++++++ 7 files changed, 580 insertions(+) create mode 100644 test/plugin.test.ts create mode 100644 test/test-plugin/.gitignore create mode 100644 test/test-plugin/package.json create mode 100644 test/test-plugin/public/icon.svg create mode 100644 test/test-plugin/src/index.ts create mode 100644 test/test-plugin/tsconfig.json create mode 100644 test/test-plugin/yarn.lock diff --git a/test/plugin.test.ts b/test/plugin.test.ts new file mode 100644 index 000000000..05a72028a --- /dev/null +++ b/test/plugin.test.ts @@ -0,0 +1,31 @@ +import { describe } from "mocha" +import { PluginAPI } from "../src/node/plugin" +import { logger } from "@coder/logger" +import * as path from "path" +import * as assert from "assert" + +/** + * Use $LOG_LEVEL=debug to see debug logs. + */ +describe("plugin", () => { + it("loads", async () => { + const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + await papi.loadPlugins() + + // We remove the function fields from the application's plugins. + const apps = JSON.parse(JSON.stringify(await papi.applications())) + + assert.deepEqual([ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + plugin: { + name: "test-plugin", + version: "1.0.0", + description: "Fake plugin for testing code-server's plugin API", + }, + }, + ], apps) + }) +}) diff --git a/test/test-plugin/.gitignore b/test/test-plugin/.gitignore new file mode 100644 index 000000000..1fcb1529f --- /dev/null +++ b/test/test-plugin/.gitignore @@ -0,0 +1 @@ +out diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json new file mode 100644 index 000000000..ccdeabb56 --- /dev/null +++ b/test/test-plugin/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "name": "test-plugin", + "version": "1.0.0", + "description": "Fake plugin for testing code-server's plugin API", + "engines": { + "code-server": "^3.6.0" + }, + "main": "out/index.js", + "devDependencies": { + "@types/express": "^4.17.8", + "typescript": "^4.0.5" + }, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/test/test-plugin/public/icon.svg b/test/test-plugin/public/icon.svg new file mode 100644 index 000000000..25b9cf047 --- /dev/null +++ b/test/test-plugin/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts new file mode 100644 index 000000000..b601cb3c0 --- /dev/null +++ b/test/test-plugin/src/index.ts @@ -0,0 +1,23 @@ +import * as pluginapi from "../../../typings/plugin" +import * as express from "express" +import * as path from "path"; + +export function init(config: pluginapi.PluginConfig) { + config.logger.debug("test-plugin loaded!") +} + +export function router(): express.Router { + const r = express.Router() + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(path.resolve(__dirname, "../public/icon.svg")) + }) + return r +} + +export function applications(): pluginapi.Application[] { + return [{ + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + }] +} diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json new file mode 100644 index 000000000..86e4897bb --- /dev/null +++ b/test/test-plugin/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./out", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/test/test-plugin/yarn.lock b/test/test-plugin/yarn.lock new file mode 100644 index 000000000..c77db2f7e --- /dev/null +++ b/test/test-plugin/yarn.lock @@ -0,0 +1,435 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/node@*": + version "14.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f" + integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw== + +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.6" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1" + integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= From 82e8a00a0d9dff6bcb4ca754ded9dbe1f42407cd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:26:30 -0400 Subject: [PATCH 05/26] Fix CI --- src/node/plugin.ts | 36 ++++++++++++++++------------------ test/plugin.test.ts | 31 ++++++++++++++++------------- test/test-plugin/src/index.ts | 16 ++++++++------- test/test-plugin/tsconfig.json | 14 ++++++------- 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 2ae29b967..ab2af5d66 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,12 +1,12 @@ -import * as path from "path" -import * as util from "./util" -import * as pluginapi from "../../typings/plugin" -import * as fs from "fs" -import * as semver from "semver" -import { version } from "./constants" -const fsp = fs.promises import { Logger, field } from "@coder/logger" import * as express from "express" +import * as fs from "fs" +import * as path from "path" +import * as semver from "semver" +import * as pluginapi from "../../typings/plugin" +import { version } from "./constants" +import * as util from "./util" +const fsp = fs.promises // These fields are populated from the plugin's package.json. interface Plugin extends pluginapi.Plugin { @@ -34,7 +34,7 @@ export class PluginAPI { */ private readonly csPlugin = "", private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, - ){ + ) { this.logger = logger.named("pluginapi") } @@ -44,7 +44,7 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (let p of this.plugins) { + for (const p of this.plugins) { const pluginApps = await p.applications() // TODO prevent duplicates @@ -62,7 +62,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (let p of this.plugins) { + for (const p of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -75,14 +75,14 @@ export class PluginAPI { // Built-in plugins. await this._loadPlugins(path.join(__dirname, "../../plugins")) - for (let dir of this.csPluginPath.split(":")) { + for (const dir of this.csPluginPath.split(":")) { if (!dir) { continue } await this._loadPlugins(dir) } - for (let dir of this.csPlugin.split(":")) { + for (const dir of this.csPlugin.split(":")) { if (!dir) { continue } @@ -93,7 +93,7 @@ export class PluginAPI { private async _loadPlugins(dir: string): Promise { try { const entries = await fsp.readdir(dir, { withFileTypes: true }) - for (let ent of entries) { + for (const ent of entries) { if (!ent.isDirectory()) { continue } @@ -124,14 +124,12 @@ export class PluginAPI { private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { const logger = this.logger.named(packageJSON.name) - logger.debug("loading plugin", - field("plugin_dir", dir), - field("package_json", packageJSON), - ) + logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) if (!semver.satisfies(version, packageJSON.engines["code-server"])) { - throw new Error(`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + - ` with code-server version ${version}`) + throw new Error( + `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, + ) } if (!packageJSON.name) { throw new Error("plugin missing name") diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 05a72028a..a77b1cf43 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,8 +1,8 @@ -import { describe } from "mocha" -import { PluginAPI } from "../src/node/plugin" import { logger } from "@coder/logger" -import * as path from "path" import * as assert from "assert" +import { describe } from "mocha" +import * as path from "path" +import { PluginAPI } from "../src/node/plugin" /** * Use $LOG_LEVEL=debug to see debug logs. @@ -15,17 +15,20 @@ describe("plugin", () => { // We remove the function fields from the application's plugins. const apps = JSON.parse(JSON.stringify(await papi.applications())) - assert.deepEqual([ - { - name: "goland", - version: "4.0.0", - iconPath: "icon.svg", - plugin: { - name: "test-plugin", - version: "1.0.0", - description: "Fake plugin for testing code-server's plugin API", + assert.deepEqual( + [ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + plugin: { + name: "test-plugin", + version: "1.0.0", + description: "Fake plugin for testing code-server's plugin API", + }, }, - }, - ], apps) + ], + apps, + ) }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index b601cb3c0..83575533b 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,6 +1,6 @@ -import * as pluginapi from "../../../typings/plugin" import * as express from "express" -import * as path from "path"; +import * as path from "path" +import * as pluginapi from "../../../typings/plugin" export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") @@ -15,9 +15,11 @@ export function router(): express.Router { } export function applications(): pluginapi.Application[] { - return [{ - name: "goland", - version: "4.0.0", - iconPath: "icon.svg", - }] + return [ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + }, + ] } diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json index 86e4897bb..0956ead88 100644 --- a/test/test-plugin/tsconfig.json +++ b/test/test-plugin/tsconfig.json @@ -4,8 +4,8 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ @@ -14,7 +14,7 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./out", /* Redirect output structure to the directory. */ + "outDir": "./out" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ @@ -25,7 +25,7 @@ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ @@ -48,7 +48,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -63,7 +63,7 @@ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ - "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } From 30d2962e21468825e545235c3e272c6a7ffefe97 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:37:42 -0400 Subject: [PATCH 06/26] src/node/plugin.ts: Warn on duplicate plugin and only load first --- src/node/plugin.ts | 20 +++++++++++++++++--- test/plugin.test.ts | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ab2af5d66..ddfef7f97 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -8,11 +8,18 @@ import { version } from "./constants" import * as util from "./util" const fsp = fs.promises -// These fields are populated from the plugin's package.json. interface Plugin extends pluginapi.Plugin { + /** + * These fields are populated from the plugin's package.json. + */ name: string version: string description: string + + /** + * path to the node module on the disk. + */ + modulePath: string } interface Application extends pluginapi.Application { @@ -47,7 +54,6 @@ export class PluginAPI { for (const p of this.plugins) { const pluginApps = await p.applications() - // TODO prevent duplicates // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { @@ -112,8 +118,15 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) + for (const p of this.plugins) { + if (p.name === packageJSON.name) { + this.logger.warn( + `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, + ) + return + } + } const p = this._loadPlugin(dir, packageJSON) - // TODO prevent duplicates this.plugins.push(p) } catch (err) { if (err.code !== "ENOENT") { @@ -145,6 +158,7 @@ export class PluginAPI { name: packageJSON.name, version: packageJSON.version, description: packageJSON.description, + modulePath: dir, ...require(dir), } as Plugin diff --git a/test/plugin.test.ts b/test/plugin.test.ts index a77b1cf43..014e07f5d 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -25,6 +25,7 @@ describe("plugin", () => { name: "test-plugin", version: "1.0.0", description: "Fake plugin for testing code-server's plugin API", + modulePath: path.join(__dirname, "test-plugin"), }, }, ], From ef971009d9632e333e2428fc3214dd2fa6f9ac02 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:39:14 -0400 Subject: [PATCH 07/26] plugin.test.ts: Make it clear iconPath is a path --- test/plugin.test.ts | 2 +- test/test-plugin/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 014e07f5d..69c4572ee 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -20,7 +20,7 @@ describe("plugin", () => { { name: "goland", version: "4.0.0", - iconPath: "icon.svg", + iconPath: "/icon.svg", plugin: { name: "test-plugin", version: "1.0.0", diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 83575533b..94bf73b80 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -19,7 +19,7 @@ export function applications(): pluginapi.Application[] { { name: "goland", version: "4.0.0", - iconPath: "icon.svg", + iconPath: "/icon.svg", }, ] } From f4d7f000331fe8152bfb95b7552c031e41fb3cd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:21:18 -0500 Subject: [PATCH 08/26] plugin.ts: Fixes for @wbobeirne --- src/node/plugin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ddfef7f97..cdd9c3d9a 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -150,9 +150,6 @@ export class PluginAPI { if (!packageJSON.version) { throw new Error("plugin missing version") } - if (!packageJSON.description) { - throw new Error("plugin missing description") - } const p = { name: packageJSON.name, @@ -181,7 +178,7 @@ interface PackageJSON { } } -function q(s: string): string { +function q(s: string | undefined): string { if (s === undefined) { s = "undefined" } From 75e52a37742833679711fca2b48fdf8a04fcb521 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:24:06 -0500 Subject: [PATCH 09/26] plugin.ts: Fixes for @code-asher --- ci/dev/test.sh | 3 +++ src/node/plugin.ts | 19 +++++++++++++++---- test/plugin.test.ts | 3 +-- test/test-plugin/Makefile | 5 +++++ 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 test/test-plugin/Makefile diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 983b2f292..6eaa3878d 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,6 +4,9 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." + cd test/test-plugin + make -s out/index.js + cd $OLDPWD mocha -r ts-node/register ./test/*.test.ts "$@" } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index cdd9c3d9a..f0dca2754 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -23,7 +23,10 @@ interface Plugin extends pluginapi.Plugin { } interface Application extends pluginapi.Application { - plugin: Plugin + /* + * Clone of the above without functions. + */ + plugin: Omit } /** @@ -57,7 +60,15 @@ export class PluginAPI { // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { - return { ...app, plugin: p } + return { + ...app, + plugin: { + name: p.name, + version: p.version, + description: p.description, + modulePath: p.modulePath, + }, + } }), ) } @@ -74,8 +85,8 @@ export class PluginAPI { } /** - * loadPlugins loads all plugins based on this.csPluginPath - * and this.csPlugin. + * loadPlugins loads all plugins based on this.csPlugin, + * this.csPluginPath and the built in plugins. */ public async loadPlugins(): Promise { // Built-in plugins. diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 69c4572ee..5836deada 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -12,8 +12,7 @@ describe("plugin", () => { const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") await papi.loadPlugins() - // We remove the function fields from the application's plugins. - const apps = JSON.parse(JSON.stringify(await papi.applications())) + const apps = await papi.applications() assert.deepEqual( [ diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile new file mode 100644 index 000000000..fb66dc81a --- /dev/null +++ b/test/test-plugin/Makefile @@ -0,0 +1,5 @@ +out/index.js: src/index.ts + yarn build + +node_modules: package.json yarn.lock + yarn From 8d3a7721feaa7e319ec6182fbe4806a301b2671a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:42:18 -0500 Subject: [PATCH 10/26] plugin.d.ts: Document plugin priority correctly --- src/node/plugin.ts | 16 ++++++++-------- typings/plugin.d.ts | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index f0dca2754..a34c2027f 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -89,8 +89,12 @@ export class PluginAPI { * this.csPluginPath and the built in plugins. */ public async loadPlugins(): Promise { - // Built-in plugins. - await this._loadPlugins(path.join(__dirname, "../../plugins")) + for (const dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } for (const dir of this.csPluginPath.split(":")) { if (!dir) { @@ -99,12 +103,8 @@ export class PluginAPI { await this._loadPlugins(dir) } - for (const dir of this.csPlugin.split(":")) { - if (!dir) { - continue - } - await this.loadPlugin(dir) - } + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) } private async _loadPlugins(dir: string): Promise { diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts index 92c3acada..549c15f11 100644 --- a/typings/plugin.d.ts +++ b/typings/plugin.d.ts @@ -18,7 +18,12 @@ import * as express from "express" * * Plugins are just node modules. * - * code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * 1. code-server uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in * $CS_PLUGIN_PATH with a package.json where the engine is code-server is * a valid plugin. * @@ -29,16 +34,14 @@ import * as express from "express" * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins * if unset. * - * code-server also uses $CS_PLUGIN to find plugins. * - * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load - * /tmp/will and /tmp/teffen as plugins. + * 3. Built in plugins are loaded from __dirname/../plugins * - * Built in plugins are also loaded from __dirname/../plugins + * Plugins are required as soon as they are found and then initialized. + * See the Plugin interface for details. * - * Priority is $CS_PLUGIN, $CS_PLUGIN_PATH and then the builtin plugins. - * After the search is complete, plugins will be required in first found order and - * initialized. See the Plugin interface for details. + * If two plugins are found with the exact same name, then code-server will + * use the first one and emit a warning. * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard @@ -51,9 +54,6 @@ import * as express from "express" * The plugin's name, description and version are fetched from its module's package.json * * The plugin's router will be mounted at / - * - * If two plugins are found with the exact same name, then code-server will - * use the last one and emit a warning. */ export interface Plugin { /** From 6638daf6f05a21bf82dab86690635045443a055d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 17:09:28 -0500 Subject: [PATCH 11/26] plugin.d.ts: Add explicit path field and adjust types to reflect See my discussion with Will in the PR. --- typings/plugin.d.ts | 54 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts index 549c15f11..bc8d2ef58 100644 --- a/typings/plugin.d.ts +++ b/typings/plugin.d.ts @@ -10,7 +10,7 @@ import * as express from "express" * The homepage of code-server will launch into VS Code. However, there will be an overlay * button that when clicked, will show all available applications with their names, * icons and provider plugins. When one clicks on an app's icon, they will be directed - * to // to access the application. + * to // to access the application. */ /** @@ -51,11 +51,35 @@ import * as express from "express" /** * Your plugin module must implement this interface. * - * The plugin's name, description and version are fetched from its module's package.json - * - * The plugin's router will be mounted at / + * The plugin's router will be mounted at / */ export interface Plugin { + /** + * name is used as the plugin's unique identifier. + * No two plugins may share the same name. + * + * Fetched from package.json. + */ + name?: string + + /** + * The version for the plugin in the overlay. + * + * Fetched from package.json. + */ + version?: string + + /** + * These two are used in the overlay. + */ + displayName: string + description: string + + /** + * The path at which the plugin router is to be registered. + */ + path: string + /** * init is called so that the plugin may initialize itself with the config. */ @@ -63,6 +87,8 @@ export interface Plugin { /** * Returns the plugin's router. + * + * Mounted at / */ router(): express.Router @@ -90,21 +116,25 @@ export interface PluginConfig { /** * Application represents a user accessible application. - * - * When the user clicks on the icon in the overlay, they will be - * redirected to // - * where the application should be accessible. - * - * If the app's name is the same as the plugin's name then - * / will be used instead. */ export interface Application { readonly name: string readonly version: string + /** + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If undefined, then / is used. + */ + readonly path?: string + + readonly description?: string + /** * The path at which the icon for this application can be accessed. - * /// + * /// */ readonly iconPath: string } From fed545e67d77e5792a0a4ee00aa4a0485ff925eb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 17:13:21 -0500 Subject: [PATCH 12/26] plugin.d.ts -> pluginapi.d.ts More clear. --- src/node/plugin.ts | 4 ++-- test/test-plugin/src/index.ts | 2 +- typings/{plugin.d.ts => pluginapi.d.ts} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename typings/{plugin.d.ts => pluginapi.d.ts} (100%) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index a34c2027f..061523a0c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -3,7 +3,7 @@ import * as express from "express" import * as fs from "fs" import * as path from "path" import * as semver from "semver" -import * as pluginapi from "../../typings/plugin" +import * as pluginapi from "../../typings/pluginapi" import { version } from "./constants" import * as util from "./util" const fsp = fs.promises @@ -30,7 +30,7 @@ interface Application extends pluginapi.Application { } /** - * PluginAPI implements the plugin API described in typings/plugin.d.ts + * PluginAPI implements the plugin API described in typings/pluginapi.d.ts * Please see that file for details. */ export class PluginAPI { diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 94bf73b80..bc37d7c05 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,6 +1,6 @@ import * as express from "express" import * as path from "path" -import * as pluginapi from "../../../typings/plugin" +import * as pluginapi from "../../../typings/pluginapi" export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") diff --git a/typings/plugin.d.ts b/typings/pluginapi.d.ts similarity index 100% rename from typings/plugin.d.ts rename to typings/pluginapi.d.ts From afff86ae9cd68f5e8d87e28dc9e78002489874df Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:11:14 -0500 Subject: [PATCH 13/26] plugin.ts: Adjust to implement pluginapi.d.ts correctly --- src/node/plugin.ts | 33 +++++++++++++++++++++++---------- test/plugin.test.ts | 10 ++++++++-- test/test-plugin/package.json | 1 - test/test-plugin/src/index.ts | 12 +++++++++--- tsconfig.json | 3 ++- typings/pluginapi.d.ts | 7 ++++++- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 061523a0c..8d5e552b5 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -10,11 +10,11 @@ const fsp = fs.promises interface Plugin extends pluginapi.Plugin { /** - * These fields are populated from the plugin's package.json. + * These fields are populated from the plugin's package.json + * and now guaranteed to exist. */ name: string version: string - description: string /** * path to the node module on the disk. @@ -34,7 +34,7 @@ interface Application extends pluginapi.Application { * Please see that file for details. */ export class PluginAPI { - private readonly plugins = new Array() + private readonly plugins = new Map() private readonly logger: Logger public constructor( @@ -54,7 +54,7 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { const pluginApps = await p.applications() // Add plugin key to each app. @@ -65,8 +65,11 @@ export class PluginAPI { plugin: { name: p.name, version: p.version, - description: p.description, modulePath: p.modulePath, + + displayName: p.displayName, + description: p.description, + path: p.path, }, } }), @@ -79,7 +82,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -129,7 +132,7 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { if (p.name === packageJSON.name) { this.logger.warn( `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, @@ -138,7 +141,7 @@ export class PluginAPI { } } const p = this._loadPlugin(dir, packageJSON) - this.plugins.push(p) + this.plugins.set(p.name, p) } catch (err) { if (err.code !== "ENOENT") { this.logger.warn(`failed to load plugin: ${err.message}`) @@ -147,6 +150,8 @@ export class PluginAPI { } private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + dir = path.resolve(dir) + const logger = this.logger.named(packageJSON.name) logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) @@ -165,11 +170,20 @@ export class PluginAPI { const p = { name: packageJSON.name, version: packageJSON.version, - description: packageJSON.description, modulePath: dir, ...require(dir), } as Plugin + if (!p.displayName) { + throw new Error("plugin missing displayName") + } + if (!p.description) { + throw new Error("plugin missing description") + } + if (!p.path) { + throw new Error("plugin missing path") + } + p.init({ logger: logger, }) @@ -183,7 +197,6 @@ export class PluginAPI { interface PackageJSON { name: string version: string - description: string engines: { "code-server": string } diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 5836deada..1e63fa8d8 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -17,14 +17,20 @@ describe("plugin", () => { assert.deepEqual( [ { - name: "goland", + name: "test app", version: "4.0.0", + + description: "my description", iconPath: "/icon.svg", + plugin: { name: "test-plugin", version: "1.0.0", - description: "Fake plugin for testing code-server's plugin API", modulePath: path.join(__dirname, "test-plugin"), + + description: "Plugin used in code-server tests.", + displayName: "Test Plugin", + path: "/test-plugin", }, }, ], diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json index ccdeabb56..c1f2e6980 100644 --- a/test/test-plugin/package.json +++ b/test/test-plugin/package.json @@ -2,7 +2,6 @@ "private": true, "name": "test-plugin", "version": "1.0.0", - "description": "Fake plugin for testing code-server's plugin API", "engines": { "code-server": "^3.6.0" }, diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index bc37d7c05..6435592b5 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,7 +1,11 @@ import * as express from "express" -import * as path from "path" +import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" +export const displayName = "Test Plugin" +export const path = "/test-plugin" +export const description = "Plugin used in code-server tests." + export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") } @@ -9,7 +13,7 @@ export function init(config: pluginapi.PluginConfig) { export function router(): express.Router { const r = express.Router() r.get("/goland/icon.svg", (req, res) => { - res.sendFile(path.resolve(__dirname, "../public/icon.svg")) + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r } @@ -17,9 +21,11 @@ export function router(): express.Router { export function applications(): pluginapi.Application[] { return [ { - name: "goland", + name: "test app", version: "4.0.0", iconPath: "/icon.svg", + + description: "my description", }, ] } diff --git a/tsconfig.json b/tsconfig.json index ac3a1df52..0db0b1908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "tsBuildInfoFile": "./.cache/tsbuildinfo", "incremental": true, "rootDir": "./src", - "typeRoots": ["./node_modules/@types", "./typings"] + "typeRoots": ["./node_modules/@types", "./typings"], + "downlevelIteration": true }, "include": ["./src/**/*.ts"] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index bc8d2ef58..7b61c4cef 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -70,9 +70,14 @@ export interface Plugin { version?: string /** - * These two are used in the overlay. + * Name used in the overlay. */ displayName: string + + /** + * Used in overlay. + * Should be a full sentence describing the plugin. + */ description: string /** From e03bbe31497b5581e578e03ebc5abc7a11b43580 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:14:14 -0500 Subject: [PATCH 14/26] routes/apps.ts: Implement /api/applications endpoint --- src/node/routes/apps.ts | 12 ++++++++++++ src/node/routes/index.ts | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 src/node/routes/apps.ts diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts new file mode 100644 index 000000000..970bd3cb1 --- /dev/null +++ b/src/node/routes/apps.ts @@ -0,0 +1,12 @@ +import * as express from "express" +import { PluginAPI } from "../plugin" + +export function router(papi: PluginAPI): express.Router { + const router = express.Router() + + router.get("/", async (req, res) => { + res.json(await papi.applications()) + }) + + return router +} diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 5824475d9..a39b2a6a1 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -23,6 +23,7 @@ import * as proxy from "./pathProxy" import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" +import * as apps from "./apps" declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -118,6 +119,7 @@ export const register = async ( const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) await papi.loadPlugins() papi.mount(app) + app.use("/api/applications", apps.router(papi)) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From 139a28e0ea063120a6adc4880f93f9c4d14bbdd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:14:19 -0500 Subject: [PATCH 15/26] plugin.ts: Describe private counterpart functions Addresses Will's comments. --- src/node/plugin.ts | 12 ++++++++++++ src/node/routes/apps.ts | 3 +++ test/plugin.test.ts | 6 +++--- test/test-plugin/src/index.ts | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 8d5e552b5..ce424770b 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -110,6 +110,13 @@ export class PluginAPI { await this._loadPlugins(path.join(__dirname, "../../plugins")) } + /** + * _loadPlugins is the counterpart to loadPlugins. + * + * It differs in that it loads all plugins in a single + * directory whereas loadPlugins uses all available directories + * as documented. + */ private async _loadPlugins(dir: string): Promise { try { const entries = await fsp.readdir(dir, { withFileTypes: true }) @@ -149,6 +156,11 @@ export class PluginAPI { } } + /** + * _loadPlugin is the counterpart to loadPlugin and actually + * loads the plugin now that we know there is no duplicate + * and that the package.json has been read. + */ private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { dir = path.resolve(dir) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index 970bd3cb1..c678f2fee 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -1,6 +1,9 @@ import * as express from "express" import { PluginAPI } from "../plugin" +/** + * Implements the /api/applications endpoint + */ export function router(papi: PluginAPI): express.Router { const router = express.Router() diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 1e63fa8d8..8c419139a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -17,10 +17,10 @@ describe("plugin", () => { assert.deepEqual( [ { - name: "test app", + name: "Test App", version: "4.0.0", - description: "my description", + description: "This app does XYZ.", iconPath: "/icon.svg", plugin: { @@ -28,8 +28,8 @@ describe("plugin", () => { version: "1.0.0", modulePath: path.join(__dirname, "test-plugin"), - description: "Plugin used in code-server tests.", displayName: "Test Plugin", + description: "Plugin used in code-server tests.", path: "/test-plugin", }, }, diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 6435592b5..f9f316b4e 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -21,11 +21,11 @@ export function router(): express.Router { export function applications(): pluginapi.Application[] { return [ { - name: "test app", + name: "Test App", version: "4.0.0", iconPath: "/icon.svg", - description: "my description", + description: "This app does XYZ.", }, ] } From 687094802ec4bb88ed4b07ecdf072e3249dfc7ca Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:42:21 -0500 Subject: [PATCH 16/26] plugin.ts: Make application endpoint paths absolute --- src/node/plugin.ts | 8 +++++--- test/plugin.test.ts | 5 +++-- test/test-plugin/Makefile | 3 ++- test/test-plugin/src/index.ts | 3 ++- typings/pluginapi.d.ts | 14 ++++++++------ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ce424770b..9523782bc 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -60,6 +60,8 @@ export class PluginAPI { // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { + app = {...app, path: path.join(p.routerPath, app.path || "")} + app = {...app, iconPath: path.join(app.path || "", app.iconPath)} return { ...app, plugin: { @@ -69,7 +71,7 @@ export class PluginAPI { displayName: p.displayName, description: p.description, - path: p.path, + routerPath: p.routerPath, }, } }), @@ -192,8 +194,8 @@ export class PluginAPI { if (!p.description) { throw new Error("plugin missing description") } - if (!p.path) { - throw new Error("plugin missing path") + if (!p.routerPath) { + throw new Error("plugin missing router path") } p.init({ diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 8c419139a..bc13fc803 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -21,7 +21,8 @@ describe("plugin", () => { version: "4.0.0", description: "This app does XYZ.", - iconPath: "/icon.svg", + iconPath: "/test-plugin/test-app/icon.svg", + path: "/test-plugin/test-app", plugin: { name: "test-plugin", @@ -30,7 +31,7 @@ describe("plugin", () => { displayName: "Test Plugin", description: "Plugin used in code-server tests.", - path: "/test-plugin", + routerPath: "/test-plugin", }, }, ], diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile index fb66dc81a..d01aa80a8 100644 --- a/test/test-plugin/Makefile +++ b/test/test-plugin/Makefile @@ -1,5 +1,6 @@ out/index.js: src/index.ts - yarn build + # Typescript always emits, even on errors. + yarn build || rm out/index.js node_modules: package.json yarn.lock yarn diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index f9f316b4e..161203319 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -3,7 +3,7 @@ import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" export const displayName = "Test Plugin" -export const path = "/test-plugin" +export const routerPath = "/test-plugin" export const description = "Plugin used in code-server tests." export function init(config: pluginapi.PluginConfig) { @@ -24,6 +24,7 @@ export function applications(): pluginapi.Application[] { name: "Test App", version: "4.0.0", iconPath: "/icon.svg", + path: "/test-app", description: "This app does XYZ.", }, diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 7b61c4cef..dbb985a58 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -45,7 +45,9 @@ import * as express from "express" * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard - * for example. + * for example. An important difference with the API is that all application paths + * will be absolute (i.e have the plugin path prepended) so that they may be used + * directly. */ /** @@ -60,30 +62,30 @@ export interface Plugin { * * Fetched from package.json. */ - name?: string + readonly name?: string /** * The version for the plugin in the overlay. * * Fetched from package.json. */ - version?: string + readonly version?: string /** * Name used in the overlay. */ - displayName: string + readonly displayName: string /** * Used in overlay. * Should be a full sentence describing the plugin. */ - description: string + readonly description: string /** * The path at which the plugin router is to be registered. */ - path: string + readonly routerPath: string /** * init is called so that the plugin may initialize itself with the config. From 2a13d003d37d97f8650bd15f29baa45cd4fb1d5a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:45:25 -0500 Subject: [PATCH 17/26] plugin.ts: Add homepageURL to plugin and application --- src/node/plugin.ts | 4 ++++ test/plugin.test.ts | 2 ++ test/test-plugin/src/index.ts | 2 ++ typings/pluginapi.d.ts | 10 ++++++++++ 4 files changed, 18 insertions(+) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 9523782bc..368d6f7ff 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -72,6 +72,7 @@ export class PluginAPI { displayName: p.displayName, description: p.description, routerPath: p.routerPath, + homepageURL: p.homepageURL, }, } }), @@ -197,6 +198,9 @@ export class PluginAPI { if (!p.routerPath) { throw new Error("plugin missing router path") } + if (!p.homepageURL) { + throw new Error("plugin missing homepage") + } p.init({ logger: logger, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index bc13fc803..ed040dc21 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -22,6 +22,7 @@ describe("plugin", () => { description: "This app does XYZ.", iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", path: "/test-plugin/test-app", plugin: { @@ -32,6 +33,7 @@ describe("plugin", () => { displayName: "Test Plugin", description: "Plugin used in code-server tests.", routerPath: "/test-plugin", + homepageURL: "https://example.com", }, }, ], diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 161203319..2fc1ddab0 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -4,6 +4,7 @@ import * as pluginapi from "../../../typings/pluginapi" export const displayName = "Test Plugin" export const routerPath = "/test-plugin" +export const homepageURL = "https://example.com" export const description = "Plugin used in code-server tests." export function init(config: pluginapi.PluginConfig) { @@ -27,6 +28,7 @@ export function applications(): pluginapi.Application[] { path: "/test-app", description: "This app does XYZ.", + homepageURL: "https://example.com", }, ] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index dbb985a58..3ce5e5d0c 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -87,6 +87,11 @@ export interface Plugin { */ readonly routerPath: string + /** + * Link to plugin homepage. + */ + readonly homepageURL: string + /** * init is called so that the plugin may initialize itself with the config. */ @@ -144,4 +149,9 @@ export interface Application { * /// */ readonly iconPath: string + + /** + * Link to application homepage. + */ + readonly homepageURL: string } From af73b96313c4d31da917ec46cbee390c7bbb32e9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:49:10 -0500 Subject: [PATCH 18/26] routes/apps.ts: Add example output --- src/node/routes/apps.ts | 2 ++ typings/pluginapi.d.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index c678f2fee..4298fb392 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -3,6 +3,8 @@ import { PluginAPI } from "../plugin" /** * Implements the /api/applications endpoint + * + * See typings/pluginapi.d.ts for details. */ export function router(papi: PluginAPI): express.Router { const router = express.Router() diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 3ce5e5d0c..94819bd3c 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -42,12 +42,38 @@ import * as express from "express" * * If two plugins are found with the exact same name, then code-server will * use the first one and emit a warning. + * + */ + +/* Programmability * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard * for example. An important difference with the API is that all application paths * will be absolute (i.e have the plugin path prepended) so that they may be used * directly. + * + * Example output: + * + * [ + * { + * "name": "Test App", + * "version": "4.0.0", + * "iconPath": "/test-plugin/test-app/icon.svg", + * "path": "/test-plugin/test-app", + * "description": "This app does XYZ.", + * "homepageURL": "https://example.com", + * "plugin": { + * "name": "test-plugin", + * "version": "1.0.0", + * "modulePath": "/Users/nhooyr/src/cdr/code-server/test/test-plugin", + * "displayName": "Test Plugin", + * "description": "Plugin used in code-server tests.", + * "routerPath": "/test-plugin", + * "homepageURL": "https://example.com" + * } + * } + * ] */ /** From 706bc23f0489bf0fe9a4b844cc36077f52600041 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:53:16 -0500 Subject: [PATCH 19/26] plugin: Fixes for CI --- ci/dev/test.sh | 2 +- src/node/plugin.ts | 10 +++++----- src/node/routes/apps.ts | 2 +- src/node/routes/index.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 6eaa3878d..9922a9c84 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -6,7 +6,7 @@ main() { cd test/test-plugin make -s out/index.js - cd $OLDPWD + cd "$OLDPWD" mocha -r ts-node/register ./test/*.test.ts "$@" } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 368d6f7ff..fea85710c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -54,14 +54,14 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { const pluginApps = await p.applications() // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { - app = {...app, path: path.join(p.routerPath, app.path || "")} - app = {...app, iconPath: path.join(app.path || "", app.iconPath)} + app = { ...app, path: path.join(p.routerPath, app.path || "") } + app = { ...app, iconPath: path.join(app.path || "", app.iconPath) } return { ...app, plugin: { @@ -85,7 +85,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -142,7 +142,7 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { if (p.name === packageJSON.name) { this.logger.warn( `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index 4298fb392..5c8541fc9 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -12,6 +12,6 @@ export function router(papi: PluginAPI): express.Router { router.get("/", async (req, res) => { res.json(await papi.applications()) }) - + return router } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index a39b2a6a1..da714eea5 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -16,6 +16,7 @@ import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" +import * as apps from "./apps" import * as health from "./health" import * as login from "./login" import * as proxy from "./pathProxy" @@ -23,7 +24,6 @@ import * as proxy from "./pathProxy" import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" -import * as apps from "./apps" declare global { // eslint-disable-next-line @typescript-eslint/no-namespace From 8a8159c683c07c8f8e854b7c176077e1b8360c39 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Nov 2020 22:59:43 -0500 Subject: [PATCH 20/26] plugin: More review fixes Next commit will address Will's comments about the typings being weird. --- src/node/plugin.ts | 26 ++++++++++++++++++-------- typings/pluginapi.d.ts | 3 ++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index fea85710c..0ac3abfc8 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -86,7 +86,7 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { - r.use(`/${p.name}`, p.router()) + r.use(`/${p.routerPath}`, p.router()) } } @@ -154,7 +154,7 @@ export class PluginAPI { this.plugins.set(p.name, p) } catch (err) { if (err.code !== "ENOENT") { - this.logger.warn(`failed to load plugin: ${err.message}`) + this.logger.warn(`failed to load plugin: ${err.stack}`) } } } @@ -170,17 +170,24 @@ export class PluginAPI { const logger = this.logger.named(packageJSON.name) logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) + if (!packageJSON.name) { + throw new Error("plugin package.json missing name") + } + if (!packageJSON.version) { + throw new Error("plugin package.json missing version") + } + if (!packageJSON.engines || !packageJSON.engines["code-server"]) { + throw new Error(`plugin package.json missing code-server range like: + "engines": { + "code-server": "^3.6.0" + } +`) + } if (!semver.satisfies(version, packageJSON.engines["code-server"])) { throw new Error( `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, ) } - if (!packageJSON.name) { - throw new Error("plugin missing name") - } - if (!packageJSON.version) { - throw new Error("plugin missing version") - } const p = { name: packageJSON.name, @@ -198,6 +205,9 @@ export class PluginAPI { if (!p.routerPath) { throw new Error("plugin missing router path") } + if (!p.routerPath.startsWith("/") || p.routerPath.length < 2) { + throw new Error(`plugin router path ${q(p.routerPath)}: invalid`) + } if (!p.homepageURL) { throw new Error("plugin missing homepage") } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 94819bd3c..4e3971eeb 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -45,7 +45,8 @@ import * as express from "express" * */ -/* Programmability +/** + * Programmability * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard From 14f408a837cbdd301545d4241da4e6ecd90857cb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Nov 2020 23:10:41 -0500 Subject: [PATCH 21/26] plugin: Plugin modules now export a single top level identifier Makes typing much easier. Addresse's Will's last comment. --- src/node/plugin.ts | 7 ++++- test/test-plugin/src/index.ts | 58 ++++++++++++++++++----------------- typings/pluginapi.d.ts | 5 +-- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 0ac3abfc8..71831f504 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -189,11 +189,16 @@ export class PluginAPI { ) } + const pluginModule = require(dir) + if (!pluginModule.plugin) { + throw new Error("plugin module does not export a plugin") + } + const p = { name: packageJSON.name, version: packageJSON.version, modulePath: dir, - ...require(dir), + ...pluginModule.plugin, } as Plugin if (!p.displayName) { diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 2fc1ddab0..2b00c2ec5 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -2,33 +2,35 @@ import * as express from "express" import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" -export const displayName = "Test Plugin" -export const routerPath = "/test-plugin" -export const homepageURL = "https://example.com" -export const description = "Plugin used in code-server tests." +export const plugin: pluginapi.Plugin = { + displayName: "Test Plugin", + routerPath: "/test-plugin", + homepageURL: "https://example.com", + description: "Plugin used in code-server tests.", -export function init(config: pluginapi.PluginConfig) { - config.logger.debug("test-plugin loaded!") -} - -export function router(): express.Router { - const r = express.Router() - r.get("/goland/icon.svg", (req, res) => { - res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) - }) - return r -} - -export function applications(): pluginapi.Application[] { - return [ - { - name: "Test App", - version: "4.0.0", - iconPath: "/icon.svg", - path: "/test-app", - - description: "This app does XYZ.", - homepageURL: "https://example.com", - }, - ] + init: (config) => { + config.logger.debug("test-plugin loaded!") + }, + + router: () => { + const r = express.Router() + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) + }) + return r + }, + + applications: () => { + return [ + { + name: "Test App", + version: "4.0.0", + iconPath: "/icon.svg", + path: "/test-app", + + description: "This app does XYZ.", + homepageURL: "https://example.com", + }, + ] + }, } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 4e3971eeb..d0846a288 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -16,7 +16,8 @@ import * as express from "express" /** * Plugins * - * Plugins are just node modules. + * Plugins are just node modules that contain a top level export "plugin" that implements + * the Plugin interface. * * 1. code-server uses $CS_PLUGIN to find plugins. * @@ -78,7 +79,7 @@ import * as express from "express" */ /** - * Your plugin module must implement this interface. + * Your plugin module must have a top level export "plugin" that implements this interface. * * The plugin's router will be mounted at / */ From 9453f891df6283747f0b53b7dfb52524bdf346ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 5 Nov 2020 14:17:13 -0500 Subject: [PATCH 22/26] plugin.ts: Fix usage of routerPath in mount --- src/node/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 71831f504..77a4a8277 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -86,7 +86,7 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { - r.use(`/${p.routerPath}`, p.router()) + r.use(`${p.routerPath}`, p.router()) } } From 197a09f0c1227a47ea709042ea6c46bb6ded5227 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 09:51:46 -0500 Subject: [PATCH 23/26] plugin: Test endpoints via supertest Unfortunately we can't use node-mocks-http to test a express.Router that has async routes. See https://github.com/howardabrams/node-mocks-http/issues/225 router will just return undefined if the executing handler is async and so the test will have no way to wait for it to complete. Thus, we have to use supertest which starts an actual HTTP server in the background and uses a HTTP client to send requests. --- package.json | 2 + test/plugin.test.ts | 66 ++++++++++++++++----------- test/test-plugin/src/index.ts | 11 +++-- yarn.lock | 85 +++++++++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 6459e6a0f..c41b8b41c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", "@types/split2": "^2.1.6", + "@types/supertest": "^2.0.10", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", @@ -59,6 +60,7 @@ "prettier": "^2.0.5", "stylelint": "^13.0.0", "stylelint-config-recommended": "^3.0.0", + "supertest": "^6.0.1", "ts-node": "^9.0.0", "typescript": "4.0.2" }, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index ed040dc21..aaf8c14dc 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,43 +1,57 @@ import { logger } from "@coder/logger" -import * as assert from "assert" import { describe } from "mocha" import * as path from "path" import { PluginAPI } from "../src/node/plugin" +import * as supertest from "supertest" +import * as express from "express" +import * as apps from "../src/node/routes/apps" /** * Use $LOG_LEVEL=debug to see debug logs. */ describe("plugin", () => { - it("loads", async () => { - const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + let papi: PluginAPI + let app: express.Application + let agent: supertest.SuperAgentTest + + before(async () => { + papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") await papi.loadPlugins() - const apps = await papi.applications() + app = express.default() + papi.mount(app) - assert.deepEqual( - [ - { - name: "Test App", - version: "4.0.0", + app.use("/api/applications", apps.router(papi)) - description: "This app does XYZ.", - iconPath: "/test-plugin/test-app/icon.svg", + agent = supertest.agent(app) + }) + + it("/api/applications", async () => { + await agent.get("/api/applications").expect(200, [ + { + name: "Test App", + version: "4.0.0", + + description: "This app does XYZ.", + iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", + path: "/test-plugin/test-app", + + plugin: { + name: "test-plugin", + version: "1.0.0", + modulePath: path.join(__dirname, "test-plugin"), + + displayName: "Test Plugin", + description: "Plugin used in code-server tests.", + routerPath: "/test-plugin", homepageURL: "https://example.com", - path: "/test-plugin/test-app", - - plugin: { - name: "test-plugin", - version: "1.0.0", - modulePath: path.join(__dirname, "test-plugin"), - - displayName: "Test Plugin", - description: "Plugin used in code-server tests.", - routerPath: "/test-plugin", - homepageURL: "https://example.com", - }, }, - ], - apps, - ) + }, + ]) + }) + + it("/test-plugin/test-app", async () => { + await agent.get("/test-plugin/test-app").expect(200, { date: "2000-02-05T05:00:00.000Z" }) }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 2b00c2ec5..9e95ffca7 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -8,19 +8,24 @@ export const plugin: pluginapi.Plugin = { homepageURL: "https://example.com", description: "Plugin used in code-server tests.", - init: (config) => { + init(config) { config.logger.debug("test-plugin loaded!") }, - router: () => { + router() { const r = express.Router() + r.get("/test-app", (req, res) => { + res.json({ + date: new Date("2000/02/05"), + }) + }) r.get("/goland/icon.svg", (req, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r }, - applications: () => { + applications() { return [ { name: "Test App", diff --git a/yarn.lock b/yarn.lock index d96b2e6ee..034efecce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1035,6 +1035,11 @@ dependencies: "@types/express" "*" +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1172,6 +1177,21 @@ dependencies: "@types/node" "*" +"@types/superagent@*": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86" + integrity sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7" + integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ== + dependencies: + "@types/superagent" "*" + "@types/tar-fs@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1" @@ -2182,7 +2202,7 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2204,7 +2224,7 @@ commander@^5.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2276,6 +2296,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookiejar@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -3351,6 +3376,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -3493,6 +3523,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3507,6 +3546,11 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -4807,7 +4851,7 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@1.1.2, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -4864,6 +4908,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -6181,6 +6230,11 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -7246,6 +7300,31 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" +superagent@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supertest@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.0.1.tgz#f6b54370de85c45d6557192c8d7df604ca2c9e18" + integrity sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g== + dependencies: + methods "1.1.2" + superagent "6.1.0" + supports-color@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" From 9d39c53c99eb5398ecd0df5a3ef0c0f8b3ec80c7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 10:09:35 -0500 Subject: [PATCH 24/26] plugin: Give test-plugin some html to test overlay --- test/plugin.test.ts | 7 ++++++- test/test-plugin/public/index.html | 10 ++++++++++ test/test-plugin/src/index.ts | 4 +--- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 test/test-plugin/public/index.html diff --git a/test/plugin.test.ts b/test/plugin.test.ts index aaf8c14dc..a0885916b 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -5,6 +5,8 @@ import { PluginAPI } from "../src/node/plugin" import * as supertest from "supertest" import * as express from "express" import * as apps from "../src/node/routes/apps" +import * as fs from "fs" +const fsp = fs.promises /** * Use $LOG_LEVEL=debug to see debug logs. @@ -52,6 +54,9 @@ describe("plugin", () => { }) it("/test-plugin/test-app", async () => { - await agent.get("/test-plugin/test-app").expect(200, { date: "2000-02-05T05:00:00.000Z" }) + const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), { + encoding: "utf8", + }) + await agent.get("/test-plugin/test-app").expect(200, indexHTML) }) }) diff --git a/test/test-plugin/public/index.html b/test/test-plugin/public/index.html new file mode 100644 index 000000000..3485f18e5 --- /dev/null +++ b/test/test-plugin/public/index.html @@ -0,0 +1,10 @@ + + + + + Test Plugin + + +

Welcome to the test plugin!

+ + diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 9e95ffca7..fb1869447 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -15,9 +15,7 @@ export const plugin: pluginapi.Plugin = { router() { const r = express.Router() r.get("/test-app", (req, res) => { - res.json({ - date: new Date("2000/02/05"), - }) + res.sendFile(fspath.resolve(__dirname, "../public/index.html")) }) r.get("/goland/icon.svg", (req, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) From 277211c4ce0ca66af19c0ffb80a4d64dec4099b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 14:44:19 -0500 Subject: [PATCH 25/26] plugin: Make init and applications callbacks optional --- src/node/plugin.ts | 6 ++++++ typings/pluginapi.d.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 77a4a8277..899aa1111 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -55,6 +55,9 @@ export class PluginAPI { public async applications(): Promise { const apps = new Array() for (const [, p] of this.plugins) { + if (!p.applications) { + continue + } const pluginApps = await p.applications() // Add plugin key to each app. @@ -86,6 +89,9 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { + if (!p.router) { + continue + } r.use(`${p.routerPath}`, p.router()) } } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index d0846a288..06ce35fb4 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -129,8 +129,10 @@ export interface Plugin { * Returns the plugin's router. * * Mounted at / + * + * If not present, the plugin provides no routes. */ - router(): express.Router + router?(): express.Router /** * code-server uses this to collect the list of applications that @@ -139,8 +141,10 @@ export interface Plugin { * refresh the list of applications * * Ensure this is as fast as possible. + * + * If not present, the plugin provides no applications. */ - applications(): Application[] | Promise + applications?(): Application[] | Promise } /** From fe399ff0fe875aed3327cbe6708572367a135f33 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 14:46:49 -0500 Subject: [PATCH 26/26] Fix formatting --- src/node/routes/index.ts | 2 +- test/plugin.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index da714eea5..4d92c365f 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -15,8 +15,8 @@ import { replaceTemplates } from "../http" import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" -import * as domainProxy from "./domainProxy" import * as apps from "./apps" +import * as domainProxy from "./domainProxy" import * as health from "./health" import * as login from "./login" import * as proxy from "./pathProxy" diff --git a/test/plugin.test.ts b/test/plugin.test.ts index a0885916b..305cf041a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,11 +1,11 @@ import { logger } from "@coder/logger" +import * as express from "express" +import * as fs from "fs" import { describe } from "mocha" import * as path from "path" -import { PluginAPI } from "../src/node/plugin" import * as supertest from "supertest" -import * as express from "express" +import { PluginAPI } from "../src/node/plugin" import * as apps from "../src/node/routes/apps" -import * as fs from "fs" const fsp = fs.promises /**