From 5f1fab7d27609b0de080c1691538e3c923d6811b Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 13 Jan 2021 16:25:39 -0600 Subject: [PATCH 01/27] Re-export logger field for plugins --- .eslintrc.yaml | 2 +- src/node/plugin.ts | 15 +++++++++++++++ test/test-plugin/src/index.ts | 4 ++-- test/test-plugin/tsconfig.json | 6 ++++-- typings/pluginapi.d.ts | 9 ++++++++- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index edb20bfe1..2036b6e0e 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -35,7 +35,7 @@ rules: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] no-async-promise-executor: off # This isn't a real module, just types, which apparently doesn't resolve. - import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }] + import/no-unresolved: [error, { ignore: ["express-serve-static-core", "code-server"] }] settings: # Does not work with CommonJS unfortunately. diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 2c0519ac1..33047e826 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -8,6 +8,21 @@ import { version } from "./constants" import * as util from "./util" const fsp = fs.promises +/** + * Inject code-server when `require`d. This is required because the API provides + * more than just types so these need to be provided at run-time. + */ +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 { + if (request === "code-server") { + return { + field, + } + } + return originalLoad.apply(this, [request, parent, isMain]) +} + interface Plugin extends pluginapi.Plugin { /** * These fields are populated from the plugin's package.json diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index fb1869447..7d14a7bc6 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,8 +1,8 @@ +import * as cs from "code-server" import * as express from "express" import * as fspath from "path" -import * as pluginapi from "../../../typings/pluginapi" -export const plugin: pluginapi.Plugin = { +export const plugin: cs.Plugin = { displayName: "Test Plugin", routerPath: "/test-plugin", homepageURL: "https://example.com", diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json index 0956ead88..5afea81bf 100644 --- a/test/test-plugin/tsconfig.json +++ b/test/test-plugin/tsconfig.json @@ -42,8 +42,10 @@ /* 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'. */ + "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, + "paths": { + "code-server": ["../../typings/pluginapi"] + } /* 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. */ diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 06ce35fb4..f9b07602b 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -1,7 +1,7 @@ /** * This file describes the code-server plugin API for adding new applications. */ -import { Logger } from "@coder/logger" +import { field, Logger } from "@coder/logger" import * as express from "express" /** @@ -78,6 +78,13 @@ import * as express from "express" * ] */ +/** + * Use to add a field to a log. + * + * Re-exported so plugins don't have to import duplicate copies of the logger. + */ +export { field } + /** * Your plugin module must have a top level export "plugin" that implements this interface. * From a8e928798bcba72b85a19aa86a4302c484f172b4 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 13 Jan 2021 16:26:11 -0600 Subject: [PATCH 02/27] Re-export express for plugins --- src/node/plugin.ts | 1 + test/test-plugin/package.json | 3 - test/test-plugin/src/index.ts | 7 +- test/test-plugin/yarn.lock | 365 ---------------------------------- typings/pluginapi.d.ts | 9 + 5 files changed, 13 insertions(+), 372 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 33047e826..cdc5919f3 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -17,6 +17,7 @@ const originalLoad = require("module")._load require("module")._load = function (request: string, parent: object, isMain: boolean): any { if (request === "code-server") { return { + express, field, } } diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json index 55c474e3d..2fe723780 100644 --- a/test/test-plugin/package.json +++ b/test/test-plugin/package.json @@ -12,8 +12,5 @@ }, "scripts": { "build": "tsc" - }, - "dependencies": { - "express": "^4.17.1" } } diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 7d14a7bc6..b7c288ebf 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,5 +1,4 @@ import * as cs from "code-server" -import * as express from "express" import * as fspath from "path" export const plugin: cs.Plugin = { @@ -13,11 +12,11 @@ export const plugin: cs.Plugin = { }, router() { - const r = express.Router() - r.get("/test-app", (req, res) => { + const r = cs.express.Router() + r.get("/test-app", (_, res) => { res.sendFile(fspath.resolve(__dirname, "../public/index.html")) }) - r.get("/goland/icon.svg", (req, res) => { + r.get("/goland/icon.svg", (_, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r diff --git a/test/test-plugin/yarn.lock b/test/test-plugin/yarn.lock index c77db2f7e..f295de1ea 100644 --- a/test/test-plugin/yarn.lock +++ b/test/test-plugin/yarn.lock @@ -64,372 +64,7 @@ "@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= diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index f9b07602b..5dd2a066a 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -78,6 +78,15 @@ import * as express from "express" * ] */ +/** + * The Express import used by code-server. + * + * Re-exported so plugins don't have to import duplicate copies of Express and + * to avoid potential version differences or issues caused by running separate + * instances. + */ +export { express } + /** * Use to add a field to a log. * From f6b04c7c29b77d8051158ca98122a7d531f230a5 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 19 Jan 2021 16:43:36 -0600 Subject: [PATCH 03/27] Expose proxy server to plugins --- src/node/plugin.ts | 4 +++- typings/pluginapi.d.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index cdc5919f3..c2e431738 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,10 +1,11 @@ -import { Logger, field } from "@coder/logger" +import { field, Logger } 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/pluginapi" import { version } from "./constants" +import { proxy } from "./proxy" import * as util from "./util" const fsp = fs.promises @@ -19,6 +20,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo return { express, field, + proxy, } } return originalLoad.apply(this, [request, parent, isMain]) diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 5dd2a066a..14d6cb487 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -94,6 +94,8 @@ export { express } */ export { field } +export const proxy: ProxyServer + /** * Your plugin module must have a top level export "plugin" that implements this interface. * From fb37473e72bc5dc6a3e51dc3a9b84a5b93865698 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 19 Jan 2021 16:44:42 -0600 Subject: [PATCH 04/27] Load only test plugin during tests The other plugins in my path were causing the tests to fail. --- src/node/plugin.ts | 7 ++++--- test/plugin.test.ts | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index c2e431738..651271277 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -118,7 +118,7 @@ export class PluginAPI { * loadPlugins loads all plugins based on this.csPlugin, * this.csPluginPath and the built in plugins. */ - public async loadPlugins(): Promise { + public async loadPlugins(loadBuiltin = true): Promise { for (const dir of this.csPlugin.split(":")) { if (!dir) { continue @@ -133,8 +133,9 @@ export class PluginAPI { await this._loadPlugins(dir) } - // Built-in plugins. - await this._loadPlugins(path.join(__dirname, "../../plugins")) + if (loadBuiltin) { + await this._loadPlugins(path.join(__dirname, "../../plugins")) + } } /** diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 0c4acb9e8..01780eb8f 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -15,8 +15,10 @@ describe("plugin", () => { let s: httpserver.HttpServer beforeAll(async () => { - papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`) - await papi.loadPlugins() + // Only include the test plugin to avoid contaminating results with other + // plugins that might be on the filesystem. + papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`, "") + await papi.loadPlugins(false) const app = express.default() papi.mount(app) From 055e0ef9ecc73ca5f3d476ce48538df645301771 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 20 Jan 2021 14:11:08 -0600 Subject: [PATCH 05/27] Provide WsRouter to plugins --- src/node/plugin.ts | 14 +++++++++----- src/node/routes/index.ts | 22 +++++++++++----------- src/node/routes/pathProxy.ts | 4 ++-- src/node/wsRouter.ts | 19 ++++--------------- test/httpserver.ts | 17 +++++++++++++++++ test/plugin.test.ts | 13 ++++++++++++- test/test-plugin/src/index.ts | 13 +++++++++++++ typings/pluginapi.d.ts | 33 +++++++++++++++++++++++++++++++++ 8 files changed, 101 insertions(+), 34 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 651271277..49d924b80 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -7,6 +7,7 @@ import * as pluginapi from "../../typings/pluginapi" import { version } from "./constants" import { proxy } from "./proxy" import * as util from "./util" +import { Router as WsRouter, WebsocketRouter } from "./wsRouter" const fsp = fs.promises /** @@ -21,6 +22,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo express, field, proxy, + WsRouter, } } return originalLoad.apply(this, [request, parent, isMain]) @@ -103,14 +105,16 @@ export class PluginAPI { } /** - * mount mounts all plugin routers onto r. + * mount mounts all plugin routers onto r and websocket routers onto wr. */ - public mount(r: express.Router): void { + public mount(r: express.Router, wr: express.Router): void { for (const [, p] of this.plugins) { - if (!p.router) { - continue + if (p.router) { + r.use(`${p.routerPath}`, p.router()) + } + if (p.wsRouter) { + wr.use(`${p.routerPath}`, (p.wsRouter() as WebsocketRouter).router) } - r.use(`${p.routerPath}`, p.router()) } } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index dd4cc126a..73116bfb2 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -6,20 +6,20 @@ import { promises as fs } from "fs" import http from "http" import * as path from "path" import * as tls from "tls" +import * as pluginapi from "../../../typings/pluginapi" import { HttpCode, HttpError } from "../../common/http" import { plural } from "../../common/util" import { AuthType, DefaultedArgs } from "../cli" import { rootPath } from "../constants" import { Heart } from "../heart" -import { replaceTemplates, redirect } from "../http" +import { redirect, replaceTemplates } from "../http" import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" -import { WebsocketRequest } from "../wsRouter" 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" +import * as pathProxy from "./pathProxy" // static is a reserved keyword. import * as _static from "./static" import * as update from "./update" @@ -104,21 +104,21 @@ export const register = async ( wsApp.use("/", domainProxy.wsRouter.router) app.all("/proxy/(:port)(/*)?", (req, res) => { - proxy.proxy(req, res) + pathProxy.proxy(req, res) }) - wsApp.get("/proxy/(:port)(/*)?", (req, res) => { - proxy.wsProxy(req as WebsocketRequest) + wsApp.get("/proxy/(:port)(/*)?", (req) => { + pathProxy.wsProxy(req as pluginapi.WebsocketRequest) }) // These two routes pass through the path directly. // So the proxied app must be aware it is running // under /absproxy// app.all("/absproxy/(:port)(/*)?", (req, res) => { - proxy.proxy(req, res, { + pathProxy.proxy(req, res, { passthroughPath: true, }) }) - wsApp.get("/absproxy/(:port)(/*)?", (req, res) => { - proxy.wsProxy(req as WebsocketRequest, { + wsApp.get("/absproxy/(:port)(/*)?", (req) => { + pathProxy.wsProxy(req as pluginapi.WebsocketRequest, { passthroughPath: true, }) }) @@ -146,7 +146,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) + papi.mount(app, wsApp) app.use("/api/applications", apps.router(papi)) app.use(() => { @@ -187,7 +187,7 @@ export const register = async ( const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { logger.error(`${err.message} ${err.stack}`) - ;(req as WebsocketRequest).ws.end() + ;(req as pluginapi.WebsocketRequest).ws.end() } wsApp.use(wsErrorHandler) diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index 31fc53366..789fa5c18 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,11 +1,11 @@ import { Request, Response } from "express" import * as path from "path" import qs from "qs" +import * as pluginapi from "../../../typings/pluginapi" import { HttpCode, HttpError } from "../../common/http" import { normalize } from "../../common/util" import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy as _proxy } from "../proxy" -import { WebsocketRequest } from "../wsRouter" const getProxyTarget = (req: Request, passthroughPath?: boolean): string => { if (passthroughPath) { @@ -46,7 +46,7 @@ export function proxy( } export function wsProxy( - req: WebsocketRequest, + req: pluginapi.WebsocketRequest, opts?: { passthroughPath?: boolean }, diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts index 8787d6f4f..e1502c4fb 100644 --- a/src/node/wsRouter.ts +++ b/src/node/wsRouter.ts @@ -1,7 +1,7 @@ import * as express from "express" import * as expressCore from "express-serve-static-core" import * as http from "http" -import * as net from "net" +import * as pluginapi from "../../typings/pluginapi" export const handleUpgrade = (app: express.Express, server: http.Server): void => { server.on("upgrade", (req, socket, head) => { @@ -20,31 +20,20 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void = }) } -export interface WebsocketRequest extends express.Request { - ws: net.Socket - head: Buffer -} - -interface InternalWebsocketRequest extends WebsocketRequest { +interface InternalWebsocketRequest extends pluginapi.WebsocketRequest { _ws_handled: boolean } -export type WebSocketHandler = ( - req: WebsocketRequest, - res: express.Response, - next: express.NextFunction, -) => void | Promise - export class WebsocketRouter { public readonly router = express.Router() - public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void { + public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void { this.router.get( route, ...handlers.map((handler) => { const wrapped: express.Handler = (req, res, next) => { ;(req as InternalWebsocketRequest)._ws_handled = true - return handler(req as WebsocketRequest, res, next) + return handler(req as pluginapi.WebsocketRequest, res, next) } return wrapped }), diff --git a/test/httpserver.ts b/test/httpserver.ts index 50f887863..4fe54f880 100644 --- a/test/httpserver.ts +++ b/test/httpserver.ts @@ -1,7 +1,10 @@ +import * as express from "express" import * as http from "http" import * as nodeFetch from "node-fetch" +import Websocket from "ws" import * as util from "../src/common/util" import { ensureAddress } from "../src/node/app" +import { handleUpgrade } from "../src/node/wsRouter" // Perhaps an abstraction similar to this should be used in app.ts as well. export class HttpServer { @@ -39,6 +42,13 @@ export class HttpServer { }) } + /** + * Send upgrade requests to an Express app. + */ + public listenUpgrade(app: express.Express): void { + handleUpgrade(app, this.hs) + } + /** * close cleans up the server. */ @@ -62,6 +72,13 @@ export class HttpServer { return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts) } + /** + * Open a websocket against the requset path. + */ + public ws(requestPath: string): Websocket { + return new Websocket(`${ensureAddress(this.hs).replace("http:", "ws:")}${requestPath}`) + } + public port(): number { const addr = this.hs.address() if (addr && typeof addr === "object") { diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 01780eb8f..139885dab 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -21,11 +21,13 @@ describe("plugin", () => { await papi.loadPlugins(false) const app = express.default() - papi.mount(app) + const wsApp = express.default() + papi.mount(app, wsApp) app.use("/api/applications", apps.router(papi)) s = new httpserver.HttpServer() await s.listen(app) + s.listenUpgrade(wsApp) }) afterAll(async () => { @@ -70,4 +72,13 @@ describe("plugin", () => { const body = await resp.text() expect(body).toBe(indexHTML) }) + + it("/test-plugin/test-app (websocket)", async () => { + const ws = s.ws("/test-plugin/test-app") + const message = await new Promise((resolve) => { + ws.once("message", (message) => resolve(message)) + }) + ws.terminate() + expect(message).toBe("hello") + }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index b7c288ebf..c7d03bdd2 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,5 +1,8 @@ import * as cs from "code-server" import * as fspath from "path" +import Websocket from "ws" + +const wss = new Websocket.Server({ noServer: true }) export const plugin: cs.Plugin = { displayName: "Test Plugin", @@ -22,6 +25,16 @@ export const plugin: cs.Plugin = { return r }, + wsRouter() { + const wr = cs.WsRouter() + wr.ws("/test-app", (req) => { + wss.handleUpgrade(req, req.socket, req.head, (ws) => { + ws.send("hello") + }) + }) + return wr + }, + applications() { return [ { diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 14d6cb487..5c56303e3 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -3,6 +3,9 @@ */ import { field, Logger } from "@coder/logger" import * as express from "express" +import * as expressCore from "express-serve-static-core" +import ProxyServer from "http-proxy" +import * as net from "net" /** * Overlay @@ -78,6 +81,27 @@ import * as express from "express" * ] */ +export interface WebsocketRequest extends express.Request { + ws: net.Socket + head: Buffer +} + +export type WebSocketHandler = ( + req: WebsocketRequest, + res: express.Response, + next: express.NextFunction, +) => void | Promise + +export interface WebsocketRouter { + readonly router: express.Router + ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void +} + +/** + * Create a router for websocket routes. + */ +export function WsRouter(): WebsocketRouter + /** * The Express import used by code-server. * @@ -152,6 +176,15 @@ export interface Plugin { */ router?(): express.Router + /** + * Returns the plugin's websocket router. + * + * Mounted at / + * + * If not present, the plugin provides no websockets. + */ + wsRouter?(): WebsocketRouter + /** * code-server uses this to collect the list of applications that * the plugin can currently provide. From 3c6fac9ce45d6ea284bfee6855368533cbf60d55 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 20 Jan 2021 15:29:45 -0600 Subject: [PATCH 06/27] Wait for inner process to exit --- src/node/wrapper.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index f6f84e2bd..28803fe9c 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -234,9 +234,7 @@ export class ParentProcess extends Process { this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts) this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts) - this.onDispose(() => { - this.disposeChild() - }) + this.onDispose(() => this.disposeChild()) this.onChildMessage((message) => { switch (message.type) { @@ -252,11 +250,15 @@ export class ParentProcess extends Process { }) } - private disposeChild(): void { + private async disposeChild(): Promise { this.started = undefined if (this.child) { - this.child.removeAllListeners() - this.child.kill() + const child = this.child + child.removeAllListeners() + child.kill() + // Wait for the child to exit otherwise its output will be lost which can + // be especially problematic if you're trying to debug why cleanup failed. + await new Promise((r) => child!.on("exit", r)) } } From 017b1cc6339c1c1876e3833159c3d01d721f133a Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 20 Jan 2021 15:48:35 -0600 Subject: [PATCH 07/27] Add deinit for plugins --- src/node/plugin.ts | 17 ++++++++++++++++- src/node/routes/index.ts | 2 ++ typings/pluginapi.d.ts | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 49d924b80..ae339cf9f 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -46,7 +46,7 @@ interface Application extends pluginapi.Application { /* * Clone of the above without functions. */ - plugin: Omit + plugin: Omit } /** @@ -254,6 +254,21 @@ export class PluginAPI { return p } + + public async dispose(): Promise { + await Promise.all( + Array.from(this.plugins.values()).map(async (p) => { + if (!p.deinit) { + return + } + try { + await p.deinit() + } catch (error) { + this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message)) + } + }), + ) + } } interface PackageJSON { diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 73116bfb2..0070c6957 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -15,6 +15,7 @@ import { Heart } from "../heart" import { redirect, replaceTemplates } from "../http" import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" +import { wrapper } from "../wrapper" import * as apps from "./apps" import * as domainProxy from "./domainProxy" import * as health from "./health" @@ -148,6 +149,7 @@ export const register = async ( await papi.loadPlugins() papi.mount(app, wsApp) app.use("/api/applications", apps.router(papi)) + wrapper.onDispose(() => papi.dispose()) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 5c56303e3..e2ba32395 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -167,6 +167,11 @@ export interface Plugin { */ init(config: PluginConfig): void + /** + * Called when the plugin should dispose/shutdown everything. + */ + deinit?(): Promise + /** * Returns the plugin's router. * From 3211eb1ce596d9d016c537bfacbb48f475e16571 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 20 Jan 2021 15:53:11 -0600 Subject: [PATCH 08/27] Expose log level to plugins In case they need to map it to something else. --- src/node/plugin.ts | 3 ++- typings/pluginapi.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ae339cf9f..72f4d0e6c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,4 +1,4 @@ -import { field, Logger } from "@coder/logger" +import { field, Level, Logger } from "@coder/logger" import * as express from "express" import * as fs from "fs" import * as path from "path" @@ -21,6 +21,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo return { express, field, + Level, proxy, WsRouter, } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index e2ba32395..65d5f9afd 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -1,7 +1,7 @@ /** * This file describes the code-server plugin API for adding new applications. */ -import { field, Logger } from "@coder/logger" +import { field, Level, Logger } from "@coder/logger" import * as express from "express" import * as expressCore from "express-serve-static-core" import ProxyServer from "http-proxy" @@ -116,7 +116,7 @@ export { express } * * Re-exported so plugins don't have to import duplicate copies of the logger. */ -export { field } +export { field, Level } export const proxy: ProxyServer From 00cfd9bdf1f69acb4391dfa8e751d21ed48b8ba9 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 21 Jan 2021 13:49:45 -0600 Subject: [PATCH 09/27] Add working directory to plugin config --- src/node/plugin.ts | 2 ++ src/node/routes/index.ts | 3 ++- typings/pluginapi.d.ts | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 72f4d0e6c..1e9e901c7 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -65,6 +65,7 @@ export class PluginAPI { */ private readonly csPlugin = "", private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, + private readonly workingDirectory: string | undefined = undefined, ) { this.logger = logger.named("pluginapi") } @@ -249,6 +250,7 @@ export class PluginAPI { p.init({ logger: logger, + workingDirectory: this.workingDirectory, }) logger.debug("loaded") diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 0070c6957..1c4483ffe 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -145,7 +145,8 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) + const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined + const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) await papi.loadPlugins() papi.mount(app, wsApp) app.use("/api/applications", apps.router(papi)) diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 65d5f9afd..1802f089f 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -212,6 +212,11 @@ export interface PluginConfig { * All plugin logs should be logged via this logger. */ readonly logger: Logger + + /** + * Plugins should default to this directory when applicable. + */ + readonly workingDirectory?: string } /** From f136a600930c9149cae451a5d3ad654fbcdcb0a0 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 22 Jan 2021 11:33:16 -0600 Subject: [PATCH 10/27] Note that we immediately pause websockets --- src/node/wsRouter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts index e1502c4fb..1ff13aafd 100644 --- a/src/node/wsRouter.ts +++ b/src/node/wsRouter.ts @@ -27,6 +27,10 @@ interface InternalWebsocketRequest extends pluginapi.WebsocketRequest { export class WebsocketRouter { public readonly router = express.Router() + /** + * Handle a websocket at this route. Note that websockets are immediately + * paused when they come in. + */ public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void { this.router.get( route, From b13db3124bfd53c3b44a90e1af4175e77e64aa10 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 28 Jan 2021 12:47:50 -0600 Subject: [PATCH 11/27] Add health websocket This is used by some of our services. --- src/node/routes/health.ts | 17 +++++++++++++++++ src/node/routes/index.ts | 1 + src/node/wsRouter.ts | 3 +++ 3 files changed, 21 insertions(+) diff --git a/src/node/routes/health.ts b/src/node/routes/health.ts index 20dab71a5..f38bb0abd 100644 --- a/src/node/routes/health.ts +++ b/src/node/routes/health.ts @@ -1,4 +1,5 @@ import { Router } from "express" +import { wss, Router as WsRouter } from "../wsRouter" export const router = Router() @@ -8,3 +9,19 @@ router.get("/", (req, res) => { lastHeartbeat: req.heart.lastHeartbeat, }) }) + +export const wsRouter = WsRouter() + +wsRouter.ws("/", async (req) => { + wss.handleUpgrade(req, req.socket, req.head, (ws) => { + ws.on("message", () => { + ws.send( + JSON.stringify({ + event: "health", + status: req.heart.alive() ? "alive" : "expired", + lastHeartbeat: req.heart.lastHeartbeat, + }), + ) + }) + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 1c4483ffe..5a84ff6ec 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -133,6 +133,7 @@ export const register = async ( wsApp.use("/vscode", vscode.wsRouter.router) app.use("/healthz", health.router) + wsApp.use("/healthz", health.wsRouter.router) if (args.auth === AuthType.Password) { app.use("/login", login.router) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts index 1ff13aafd..d829d0821 100644 --- a/src/node/wsRouter.ts +++ b/src/node/wsRouter.ts @@ -1,6 +1,7 @@ import * as express from "express" import * as expressCore from "express-serve-static-core" import * as http from "http" +import Websocket from "ws" import * as pluginapi from "../../typings/pluginapi" export const handleUpgrade = (app: express.Express, server: http.Server): void => { @@ -48,3 +49,5 @@ export class WebsocketRouter { export function Router(): WebsocketRouter { return new WebsocketRouter() } + +export const wss = new Websocket.Server({ noServer: true }) From 5505959f7e7e05d2f439f189d18270ec2192f03c Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 28 Jan 2021 12:48:47 -0600 Subject: [PATCH 12/27] Expose websocket server to plugins Same reasoning used when exposing Express. --- src/node/plugin.ts | 3 ++- test/test-plugin/src/index.ts | 5 +---- typings/pluginapi.d.ts | 7 ++++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 1e9e901c7..cf06d3d70 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -7,7 +7,7 @@ import * as pluginapi from "../../typings/pluginapi" import { version } from "./constants" import { proxy } from "./proxy" import * as util from "./util" -import { Router as WsRouter, WebsocketRouter } from "./wsRouter" +import { Router as WsRouter, WebsocketRouter, wss } from "./wsRouter" const fsp = fs.promises /** @@ -24,6 +24,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo Level, proxy, WsRouter, + wss, } } return originalLoad.apply(this, [request, parent, isMain]) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index c7d03bdd2..211ddf0d8 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,8 +1,5 @@ import * as cs from "code-server" import * as fspath from "path" -import Websocket from "ws" - -const wss = new Websocket.Server({ noServer: true }) export const plugin: cs.Plugin = { displayName: "Test Plugin", @@ -28,7 +25,7 @@ export const plugin: cs.Plugin = { wsRouter() { const wr = cs.WsRouter() wr.ws("/test-app", (req) => { - wss.handleUpgrade(req, req.socket, req.head, (ws) => { + cs.wss.handleUpgrade(req, req.socket, req.head, (ws) => { ws.send("hello") }) }) diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 1802f089f..aa9ce0ff3 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -6,6 +6,7 @@ import * as express from "express" import * as expressCore from "express-serve-static-core" import ProxyServer from "http-proxy" import * as net from "net" +import Websocket from "ws" /** * Overlay @@ -102,6 +103,11 @@ export interface WebsocketRouter { */ export function WsRouter(): WebsocketRouter +/** + * The websocket server used by code-server. + */ +export const wss: Websocket.Server + /** * The Express import used by code-server. * @@ -110,7 +116,6 @@ export function WsRouter(): WebsocketRouter * instances. */ export { express } - /** * Use to add a field to a log. * From 150513fbc4399c4f4c1f2505ae5dfaff24fca70b Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 28 Jan 2021 14:11:53 -0600 Subject: [PATCH 13/27] Export Logger type So plugins can pass the logger around. --- typings/pluginapi.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index aa9ce0ff3..b0f6bd02d 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -121,7 +121,7 @@ export { express } * * Re-exported so plugins don't have to import duplicate copies of the logger. */ -export { field, Level } +export { field, Level, Logger } export const proxy: ProxyServer From 36aad9bdab3961b48d83f5811240a6580854d370 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 29 Jan 2021 18:03:21 -0600 Subject: [PATCH 14/27] Move global express args definition This way tests that import the http utilities but not the routes won't error due to missing types. --- src/node/http.ts | 13 ++++++++++++- src/node/routes/index.ts | 10 ---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index 18fee9f84..eb8c91f94 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -5,10 +5,21 @@ import qs from "qs" import safeCompare from "safe-compare" import { HttpCode, HttpError } from "../common/http" import { normalize, Options } from "../common/util" -import { AuthType } from "./cli" +import { AuthType, DefaultedArgs } from "./cli" import { commit, rootPath } from "./constants" +import { Heart } from "./heart" import { hash } from "./util" +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + export interface Request { + args: DefaultedArgs + heart: Heart + } + } +} + /** * Replace common variable strings in HTML templates. */ diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 5a84ff6ec..41c07a09a 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -26,16 +26,6 @@ import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Express { - export interface Request { - args: DefaultedArgs - heart: Heart - } - } -} - /** * Register all routes and middleware. */ From 22d194515a22297a393ad320093e87bcceb41049 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 28 Jan 2021 14:24:07 -0600 Subject: [PATCH 15/27] Expose replaceTemplates to plugins This is mainly so they can get relative paths in their HTML, in particular code-server's static base so they can use the favicon and service worker. --- src/node/plugin.ts | 2 ++ typings/pluginapi.d.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index cf06d3d70..702201039 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -5,6 +5,7 @@ import * as path from "path" import * as semver from "semver" import * as pluginapi from "../../typings/pluginapi" import { version } from "./constants" +import { replaceTemplates } from "./http" import { proxy } from "./proxy" import * as util from "./util" import { Router as WsRouter, WebsocketRouter, wss } from "./wsRouter" @@ -23,6 +24,7 @@ require("module")._load = function (request: string, parent: object, isMain: boo field, Level, proxy, + replaceTemplates, WsRouter, wss, } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index b0f6bd02d..fbac3af9b 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -123,8 +123,20 @@ export { express } */ export { field, Level, Logger } +/** + * code-server's proxy server. + */ export const proxy: ProxyServer +/** + * Replace variables in HTML: TO, BASE, CS_STATIC_BASE, and OPTIONS. + */ +export function replaceTemplates( + req: express.Request, + content: string, + extraOpts?: Omit, +): string + /** * Your plugin module must have a top level export "plugin" that implements this interface. * From c78f56b3342d4416477a3b1b8810a6bb4230cb23 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 29 Jan 2021 17:42:50 -0600 Subject: [PATCH 16/27] Expose HttpError to plugins This will let them throw and show nice errors more easily. --- src/node/plugin.ts | 3 +++ test/plugin.test.ts | 6 ++++++ test/test-plugin/src/index.ts | 3 +++ typings/pluginapi.d.ts | 14 ++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 702201039..079981004 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -4,6 +4,7 @@ import * as fs from "fs" import * as path from "path" import * as semver from "semver" import * as pluginapi from "../../typings/pluginapi" +import { HttpCode, HttpError } from "../common/http" import { version } from "./constants" import { replaceTemplates } from "./http" import { proxy } from "./proxy" @@ -22,6 +23,8 @@ require("module")._load = function (request: string, parent: object, isMain: boo return { express, field, + HttpCode, + HttpError, Level, proxy, replaceTemplates, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 139885dab..b5bfb7633 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -2,6 +2,7 @@ import { logger } from "@coder/logger" import * as express from "express" import * as fs from "fs" import * as path from "path" +import { HttpCode } from "../src/common/http" import { PluginAPI } from "../src/node/plugin" import * as apps from "../src/node/routes/apps" import * as httpserver from "./httpserver" @@ -81,4 +82,9 @@ describe("plugin", () => { ws.terminate() expect(message).toBe("hello") }) + + it("/test-plugin/error", async () => { + const resp = await s.fetch("/test-plugin/error") + expect(resp.status).toBe(HttpCode.LargePayload) + }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 211ddf0d8..592ad3723 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -19,6 +19,9 @@ export const plugin: cs.Plugin = { r.get("/goland/icon.svg", (_, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) + r.get("/error", () => { + throw new cs.HttpError("error", cs.HttpCode.LargePayload) + }) return r }, diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index fbac3af9b..383bf962d 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -82,6 +82,20 @@ import Websocket from "ws" * ] */ +export enum HttpCode { + Ok = 200, + Redirect = 302, + NotFound = 404, + BadRequest = 400, + Unauthorized = 401, + LargePayload = 413, + ServerError = 500, +} + +export declare class HttpError extends Error { + constructor(message: string, status: HttpCode, details?: object) +} + export interface WebsocketRequest extends express.Request { ws: net.Socket head: Buffer From 2fe3d57df36b3009b03d099e707e0cbb2baba143 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 12:26:00 -0600 Subject: [PATCH 17/27] Mount plugins before bodyParser Otherwise it consumes the body and plugins won't be able to do things like proxy POST requests. --- src/node/routes/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 41c07a09a..21918b38a 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -114,6 +114,11 @@ export const register = async ( }) }) + const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined + const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) + await papi.loadPlugins() + papi.mount(app, wsApp) + app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) @@ -136,10 +141,6 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined - const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) - await papi.loadPlugins() - papi.mount(app, wsApp) app.use("/api/applications", apps.router(papi)) wrapper.onDispose(() => papi.dispose()) From 3226d50747045b7e864479c91d03bd15eb2ead86 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 12:40:14 -0600 Subject: [PATCH 18/27] Rename papi to pluginApi --- src/node/routes/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 21918b38a..f91c75766 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -115,9 +115,9 @@ export const register = async ( }) const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined - const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) - await papi.loadPlugins() - papi.mount(app, wsApp) + const pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) + await pluginApi.loadPlugins() + pluginApi.mount(app, wsApp) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) @@ -141,8 +141,8 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - app.use("/api/applications", apps.router(papi)) - wrapper.onDispose(() => papi.dispose()) + app.use("/api/applications", apps.router(pluginApi)) + wrapper.onDispose(() => pluginApi.dispose()) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From 2879bd4c228d0fd7b9342cba678e89a14e9fc803 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 12:55:32 -0600 Subject: [PATCH 19/27] Add type alias for required modules --- src/node/plugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 079981004..d0531610b 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -12,13 +12,15 @@ import * as util from "./util" import { Router as WsRouter, WebsocketRouter, wss } from "./wsRouter" const fsp = fs.promises +// Represents a required module which could be anything. +type Module = any + /** * Inject code-server when `require`d. This is required because the API provides * more than just types so these need to be provided at run-time. */ 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 { +require("module")._load = function (request: string, parent: object, isMain: boolean): Module { if (request === "code-server") { return { express, From 9647d65e522f78f861d618557083faf84c7c4133 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 13:32:01 -0600 Subject: [PATCH 20/27] Add code-server alias to eslint --- .eslintrc.yaml | 2 +- test/test-plugin/.eslintrc.yaml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 test/test-plugin/.eslintrc.yaml diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 2036b6e0e..edb20bfe1 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -35,7 +35,7 @@ rules: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] no-async-promise-executor: off # This isn't a real module, just types, which apparently doesn't resolve. - import/no-unresolved: [error, { ignore: ["express-serve-static-core", "code-server"] }] + import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }] settings: # Does not work with CommonJS unfortunately. diff --git a/test/test-plugin/.eslintrc.yaml b/test/test-plugin/.eslintrc.yaml new file mode 100644 index 000000000..12279afb5 --- /dev/null +++ b/test/test-plugin/.eslintrc.yaml @@ -0,0 +1,5 @@ +settings: + import/resolver: + alias: + map: + - [code-server, ../../typings/pluginapi.d.ts] From b881117762fd253f3b7dd13f898d01709b3a1550 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 13:35:06 -0600 Subject: [PATCH 21/27] Expand working directory comment --- typings/pluginapi.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 383bf962d..b93a82b5d 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -245,7 +245,9 @@ export interface PluginConfig { readonly logger: Logger /** - * Plugins should default to this directory when applicable. + * This can be specified by the user on the command line. Plugins should + * default to this directory when applicable. For example, the Jupyter plugin + * uses this to launch in this directory. */ readonly workingDirectory?: string } From e098df076666248f48b1da5a7c5a79505cb45357 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 15:23:08 -0600 Subject: [PATCH 22/27] Fix code-server module not being provided in Jest --- src/node/plugin.ts | 30 ++++++++++++++++-------------- test/plugin.test.ts | 5 ++++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index d0531610b..2ba1bf1eb 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -21,20 +21,22 @@ type Module = any */ const originalLoad = require("module")._load require("module")._load = function (request: string, parent: object, isMain: boolean): Module { - if (request === "code-server") { - return { - express, - field, - HttpCode, - HttpError, - Level, - proxy, - replaceTemplates, - WsRouter, - wss, - } - } - return originalLoad.apply(this, [request, parent, isMain]) + return request === "code-server" ? codeServer : originalLoad.apply(this, [request, parent, isMain]) +} + +/** + * The module you get when importing "code-server". + */ +export const codeServer = { + express, + field, + HttpCode, + HttpError, + Level, + proxy, + replaceTemplates, + WsRouter, + wss, } interface Plugin extends pluginapi.Plugin { diff --git a/test/plugin.test.ts b/test/plugin.test.ts index b5bfb7633..e9248eed7 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -3,11 +3,14 @@ import * as express from "express" import * as fs from "fs" import * as path from "path" import { HttpCode } from "../src/common/http" -import { PluginAPI } from "../src/node/plugin" +import { codeServer, PluginAPI } from "../src/node/plugin" import * as apps from "../src/node/routes/apps" import * as httpserver from "./httpserver" const fsp = fs.promises +// Jest overrides `require` so our usual override doesn't work. +jest.mock("code-server", () => codeServer) + /** * Use $LOG_LEVEL=debug to see debug logs. */ From e4e0ac43b045bf8e627e57fc9c7f0342007dfff9 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 15:36:05 -0600 Subject: [PATCH 23/27] Don't load plugins in tests This can affect the test behavior and results. --- ci/dev/test.sh | 2 +- src/node/routes/index.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 6b0acd02a..851aa0d3b 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -9,7 +9,7 @@ main() { # information. We must also run it from the root otherwise coverage will not # include our source files. cd "$OLDPWD" - ./test/node_modules/.bin/jest "$@" + CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" } main "$@" diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index f91c75766..d04eac349 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -114,10 +114,14 @@ export const register = async ( }) }) - const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined - const pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) - await pluginApi.loadPlugins() - pluginApi.mount(app, wsApp) + if (!process.env.CS_DISABLE_PLUGINS) { + const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined + const pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) + await pluginApi.loadPlugins() + pluginApi.mount(app, wsApp) + app.use("/api/applications", apps.router(pluginApi)) + wrapper.onDispose(() => pluginApi.dispose()) + } app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) @@ -141,9 +145,6 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - app.use("/api/applications", apps.router(pluginApi)) - wrapper.onDispose(() => pluginApi.dispose()) - app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) }) From 2b1b3e6dc09e506dfdb320883a5bdd99237b23b1 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 16:20:28 -0600 Subject: [PATCH 24/27] Add eslint import alias resolver Somehow I managed not to commit this earlier. --- package.json | 5 +++-- yarn.lock | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 51479b901..888f9dfed 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "doctoc": "^1.4.0", "eslint": "^7.7.0", "eslint-config-prettier": "^6.0.0", + "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^3.1.0", "istanbul-badges-readme": "^1.2.0", @@ -62,8 +63,8 @@ "stylelint": "^13.0.0", "stylelint-config-recommended": "^3.0.0", "ts-node": "^9.0.0", - "wtfnode": "^0.8.4", - "typescript": "^4.1.3" + "typescript": "^4.1.3", + "wtfnode": "^0.8.4" }, "resolutions": { "@types/node": "^12.12.7", diff --git a/yarn.lock b/yarn.lock index cb51b9912..9371a7a3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2985,6 +2985,11 @@ eslint-config-prettier@^6.0.0: dependencies: get-stdin "^6.0.0" +eslint-import-resolver-alias@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97" + integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w== + eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" From 4f16087a94aac1ea602b37c838009651d74eec42 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 9 Feb 2021 16:36:03 -0600 Subject: [PATCH 25/27] Resolve code-server from the root This fixes the lint script but unfortunately breaks my editor. --- test/test-plugin/.eslintrc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-plugin/.eslintrc.yaml b/test/test-plugin/.eslintrc.yaml index 12279afb5..67a20fa64 100644 --- a/test/test-plugin/.eslintrc.yaml +++ b/test/test-plugin/.eslintrc.yaml @@ -2,4 +2,4 @@ settings: import/resolver: alias: map: - - [code-server, ../../typings/pluginapi.d.ts] + - [code-server, ./typings/pluginapi.d.ts] From 3f837d30361841085ff6e4e10316913ec0c36c0d Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 10 Feb 2021 10:32:17 -0600 Subject: [PATCH 26/27] Fix tests failing due to collisions in release --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 888f9dfed..65b26ef2c 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,9 @@ "global": { "lines": 40 } - } + }, + "modulePathIgnorePatterns": [ + "/release" + ] } } From de9491d5a625b3a366fc33f77d2178de47782e1a Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 10 Feb 2021 13:13:23 -0600 Subject: [PATCH 27/27] Mark code-server as a virtual module --- test/plugin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugin.test.ts b/test/plugin.test.ts index e9248eed7..dfd7fac00 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -9,7 +9,7 @@ import * as httpserver from "./httpserver" const fsp = fs.promises // Jest overrides `require` so our usual override doesn't work. -jest.mock("code-server", () => codeServer) +jest.mock("code-server", () => codeServer, { virtual: true }) /** * Use $LOG_LEVEL=debug to see debug logs.