diff --git a/.eslintrc.yaml b/.eslintrc.yaml index b579a9a24671..962251cd0ea1 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -36,8 +36,8 @@ rules: import/order: [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"] }] + # These aren't real modules, just types, which apparently don't resolve. + import/no-unresolved: [error, { ignore: ["express-serve-static-core", "vscode"] }] settings: # Does not work with CommonJS unfortunately. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7ba2851c6f..a9b81b91d5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ VS Code v0.00.0 ### New Features -- item +- Add `VSCODE_PROXY_URI` env var for use in the terminal and extensions (#1510) ### Bug Fixes diff --git a/ci/dev/postinstall.sh b/ci/dev/postinstall.sh index 026ef5f3a225..005ec22b6589 100755 --- a/ci/dev/postinstall.sh +++ b/ci/dev/postinstall.sh @@ -3,15 +3,20 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." + source ./ci/lib.sh - echo "Installing code-server test dependencies..." + pushd test + echo "Installing dependencies for $PWD" + yarn install + popd - cd test + pushd test/e2e/extensions/test-extension + echo "Installing dependencies for $PWD" yarn install - cd .. + popd - cd vendor - echo "Installing vendor dependencies..." + pushd vendor + echo "Installing dependencies for $PWD" # * We install in 'modules' instead of 'node_modules' because VS Code's extensions # use a webpack config which cannot differentiate between its own node_modules @@ -32,6 +37,8 @@ main() { # Finally, run the vendor `postinstall` yarn run postinstall + + popd } main "$@" diff --git a/ci/dev/test-e2e.sh b/ci/dev/test-e2e.sh index f42deb837552..af738af5151d 100755 --- a/ci/dev/test-e2e.sh +++ b/ci/dev/test-e2e.sh @@ -6,6 +6,11 @@ main() { source ./ci/lib.sh + pushd test/e2e/extensions/test-extension + echo "Building test extension" + yarn build + popd + local dir="$PWD" if [[ ! ${CODE_SERVER_TEST_ENTRY-} ]]; then echo "Set CODE_SERVER_TEST_ENTRY to test another build of code-server" diff --git a/ci/dev/test-unit.sh b/ci/dev/test-unit.sh index 65fa94001e39..632db727022b 100755 --- a/ci/dev/test-unit.sh +++ b/ci/dev/test-unit.sh @@ -3,12 +3,16 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - cd test/unit/node/test-plugin + source ./ci/lib.sh + + echo "Building test plugin" + pushd test/unit/node/test-plugin make -s out/index.js + popd + # We must keep jest in a sub-directory. See ../../test/package.json for more # information. We must also run it from the root otherwise coverage will not # include our source files. - cd "$OLDPWD" CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" } diff --git a/src/common/util.ts b/src/common/util.ts index 4e4f23cfd818..7fe2c1ff0ac3 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -66,33 +66,42 @@ export const resolveBase = (base?: string): string => { return normalize(url.pathname) } +let options: Options + /** * Get options embedded in the HTML or query params. */ export const getOptions = (): T => { - let options: T - try { - options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!) - } catch (error) { - options = {} as T - } + if (!options) { + try { + options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!) + } catch (error) { + console.error(error) + options = {} as T + } - // You can also pass options in stringified form to the options query - // variable. Options provided here will override the ones in the options - // element. - const params = new URLSearchParams(location.search) - const queryOpts = params.get("options") - if (queryOpts) { - options = { - ...options, - ...JSON.parse(queryOpts), + // You can also pass options in stringified form to the options query + // variable. Options provided here will override the ones in the options + // element. + const params = new URLSearchParams(location.search) + const queryOpts = params.get("options") + if (queryOpts) { + try { + options = { + ...options, + ...JSON.parse(queryOpts), + } + } catch (error) { + // Don't fail if the query parameters are malformed. + console.error(error) + } } - } - options.base = resolveBase(options.base) - options.csStaticBase = resolveBase(options.csStaticBase) + options.base = resolveBase(options.base) + options.csStaticBase = resolveBase(options.csStaticBase) + } - return options + return options as T } /** diff --git a/test/e2e/extensions.test.ts b/test/e2e/extensions.test.ts new file mode 100644 index 000000000000..f83e8e031692 --- /dev/null +++ b/test/e2e/extensions.test.ts @@ -0,0 +1,12 @@ +import { describe, test } from "./baseFixture" + +describe("Extensions", true, () => { + // This will only work if the test extension is loaded into code-server. + test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => { + const address = await codeServerPage.address() + + await codeServerPage.executeCommandViaMenus("code-server: Get proxy URI") + + await codeServerPage.page.waitForSelector(`text=${address}/proxy/{port}`) + }) +}) diff --git a/test/e2e/extensions/test-extension/.gitignore b/test/e2e/extensions/test-extension/.gitignore new file mode 100644 index 000000000000..e7b307d8c4f7 --- /dev/null +++ b/test/e2e/extensions/test-extension/.gitignore @@ -0,0 +1 @@ +/extension.js diff --git a/test/e2e/extensions/test-extension/extension.ts b/test/e2e/extensions/test-extension/extension.ts new file mode 100644 index 000000000000..dcbd6dde7bc0 --- /dev/null +++ b/test/e2e/extensions/test-extension/extension.ts @@ -0,0 +1,13 @@ +import * as vscode from "vscode" + +export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand("codeServerTest.proxyUri", () => { + if (process.env.VSCODE_PROXY_URI) { + vscode.window.showInformationMessage(process.env.VSCODE_PROXY_URI) + } else { + vscode.window.showErrorMessage("No proxy URI was set") + } + }), + ) +} diff --git a/test/e2e/extensions/test-extension/package.json b/test/e2e/extensions/test-extension/package.json new file mode 100644 index 000000000000..82be6fe52ced --- /dev/null +++ b/test/e2e/extensions/test-extension/package.json @@ -0,0 +1,29 @@ +{ + "name": "code-server-extension", + "description": "code-server test extension", + "version": "0.0.1", + "publisher": "cdr", + "activationEvents": [ + "onCommand:codeServerTest.proxyUri" + ], + "engines": { + "vscode": "^1.56.0" + }, + "main": "./extension.js", + "contributes": { + "commands": [ + { + "command": "codeServerTest.proxyUri", + "title": "Get proxy URI", + "category": "code-server" + } + ] + }, + "devDependencies": { + "@types/vscode": "^1.56.0", + "typescript": "^4.0.5" + }, + "scripts": { + "build": "tsc extension.ts" + } +} diff --git a/test/e2e/extensions/test-extension/tsconfig.json b/test/e2e/extensions/test-extension/tsconfig.json new file mode 100644 index 000000000000..9840655c5d4b --- /dev/null +++ b/test/e2e/extensions/test-extension/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": ".", + "strict": true, + "baseUrl": "./" + }, + "include": ["./extension.ts"] +} diff --git a/test/e2e/extensions/test-extension/yarn.lock b/test/e2e/extensions/test-extension/yarn.lock new file mode 100644 index 000000000000..363247117ecf --- /dev/null +++ b/test/e2e/extensions/test-extension/yarn.lock @@ -0,0 +1,13 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/vscode@^1.56.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.57.0.tgz#cc648e0573b92f725cd1baf2621f8da9f8bc689f" + integrity sha512-FeznBFtIDCWRluojTsi9c3LLcCHOXP5etQfBK42+ixo1CoEAchkw39tuui9zomjZuKfUVL33KZUDIwHZ/xvOkQ== + +typescript@^4.0.5: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index 3acb0cd7b192..22d2bc1d034d 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -87,6 +87,8 @@ export class CodeServer { path.join(dir, "config.yaml"), "--user-data-dir", dir, + "--extensions-dir", + path.join(__dirname, "../extensions"), // The last argument is the workspace to open. dir, ], diff --git a/test/playwright.config.ts b/test/playwright.config.ts index 679dd33f9399..f256c0a9239e 100644 --- a/test/playwright.config.ts +++ b/test/playwright.config.ts @@ -20,12 +20,10 @@ const config: PlaywrightTestConfig = { name: "Chromium", use: { browserName: "chromium" }, }, - { name: "Firefox", use: { browserName: "firefox" }, }, - { name: "WebKit", use: { browserName: "webkit" }, diff --git a/test/unit/common/util.test.ts b/test/unit/common/util.test.ts index 85422aa84629..b691cce1f994 100644 --- a/test/unit/common/util.test.ts +++ b/test/unit/common/util.test.ts @@ -1,6 +1,6 @@ import { JSDOM } from "jsdom" import * as util from "../../../src/common/util" -import { createLoggerMock } from "../../utils/helpers" +import * as helpers from "../../utils/helpers" const dom = new JSDOM() global.document = dom.window.document @@ -111,6 +111,8 @@ describe("util", () => { }) describe("getOptions", () => { + let getOptions: typeof import("../../../src/common/util").getOptions + beforeEach(() => { const location: LocationLike = { pathname: "/healthz", @@ -124,6 +126,12 @@ describe("util", () => { // and tell TS that our location should be looked at // as Location (even though it's missing some properties) global.location = location as Location + + // Reset and re-import since the options are cached. + jest.resetModules() + getOptions = require("../../../src/common/util").getOptions + + helpers.spyOnConsole() }) afterEach(() => { @@ -131,47 +139,67 @@ describe("util", () => { }) it("should return options with base and cssStaticBase even if it doesn't exist", () => { - expect(util.getOptions()).toStrictEqual({ + expect(getOptions()).toStrictEqual({ base: "", csStaticBase: "", }) + expect(console.error).toBeCalledTimes(1) }) it("should return options when they do exist", () => { + const expected = { + base: ".", + csStaticBase: "./static/development/Users/jp/Dev/code-server", + logLevel: 2, + disableTelemetry: false, + disableUpdateCheck: false, + } + // Mock getElementById const spy = jest.spyOn(document, "getElementById") - // Create a fake element and set the attribute + // Create a fake element and set the attribute. Options are expected to be + // stringified JSON. const mockElement = document.createElement("div") - mockElement.setAttribute( - "data-settings", - '{"base":".","csStaticBase":"./static/development/Users/jp/Dev/code-server","logLevel":2,"disableUpdateCheck":false}', - ) + mockElement.setAttribute("data-settings", JSON.stringify(expected)) // Return mockElement from the spy // this way, when we call "getElementById" // it returns the element spy.mockImplementation(() => mockElement) - expect(util.getOptions()).toStrictEqual({ + expect(getOptions()).toStrictEqual({ + ...expected, + // The two bases should get resolved. The rest should be unchanged. base: "", csStaticBase: "/static/development/Users/jp/Dev/code-server", - disableUpdateCheck: false, + }) + expect(console.error).toBeCalledTimes(0) + }) + + it("should merge options in the query", () => { + // Options provided in the query will override any options provided in the + // HTML. Options are expected to be stringified JSON (same as the + // element). + const expected = { logLevel: 2, + } + location.search = `?options=${JSON.stringify(expected)}` + expect(getOptions()).toStrictEqual({ + ...expected, + base: "", + csStaticBase: "", }) + // Once for the element. + expect(console.error).toBeCalledTimes(1) }) - it("should include queryOpts", () => { - // Trying to understand how the implementation works - // 1. It grabs the search params from location.search (i.e. ?) - // 2. it then grabs the "options" param if it exists - // 3. then it creates a new options object - // spreads the original options - // then parses the queryOpts - location.search = '?options={"logLevel":2}' - expect(util.getOptions()).toStrictEqual({ + it("should skip bad query options", () => { + location.search = "?options=invalidJson" + expect(getOptions()).toStrictEqual({ base: "", csStaticBase: "", - logLevel: 2, }) + // Once for the element, once for the query. + expect(console.error).toBeCalledTimes(2) }) }) @@ -217,7 +245,7 @@ describe("util", () => { jest.restoreAllMocks() }) - const loggerModule = createLoggerMock() + const loggerModule = helpers.createLoggerMock() it("should log an error with the message and stack trace", () => { const message = "You don't have access to that folder." diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 2e55c322c080..36b5f620474f 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -29,8 +29,23 @@ export async function clean(testName: string): Promise { await fs.rmdir(dir, { recursive: true }) } +/** + * Spy on the console and surpress its output. + * TODO: Replace `createLoggerMock` usage with `spyOnConsole`. + */ +export function spyOnConsole() { + const noop = () => undefined + jest.spyOn(global.console, "debug").mockImplementation(noop) + jest.spyOn(global.console, "error").mockImplementation(noop) + jest.spyOn(global.console, "info").mockImplementation(noop) + jest.spyOn(global.console, "trace").mockImplementation(noop) + jest.spyOn(global.console, "warn").mockImplementation(noop) +} + /** * Create a uniquely named temporary directory for a test. + * + * These directories are placed under a single temporary code-server directory. */ export async function tmpdir(testName: string): Promise { const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`) diff --git a/vendor/package.json b/vendor/package.json index 22fe1709b706..f814e0d2fb94 100644 --- a/vendor/package.json +++ b/vendor/package.json @@ -7,6 +7,6 @@ "postinstall": "./postinstall.sh" }, "devDependencies": { - "code-oss-dev": "cdr/vscode#9cb5fb3759f46b10bc66e676fa7f44c51e84824b" + "code-oss-dev": "code-asher/vscode#7185f35737bd0b943119a0eee4c981677d13e23e" } } diff --git a/vendor/yarn.lock b/vendor/yarn.lock index d505c31d46d2..71b855bd9188 100644 --- a/vendor/yarn.lock +++ b/vendor/yarn.lock @@ -122,9 +122,9 @@ integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== "@types/node@^14.6.2": - version "14.17.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.17.tgz#4ec7b71bbcb01a4e55455b60b18b1b6a783fe31d" - integrity sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ== + version "14.17.19" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.19.tgz#7341e9ac1b5d748d7a3ddc04336ed536a6f91c31" + integrity sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA== "@vscode/sqlite3@4.0.12": version "4.0.12" @@ -313,9 +313,9 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" -code-oss-dev@cdr/vscode#9cb5fb3759f46b10bc66e676fa7f44c51e84824b: +code-oss-dev@code-asher/vscode#7185f35737bd0b943119a0eee4c981677d13e23e: version "1.60.0" - resolved "https://codeload.github.com/cdr/vscode/tar.gz/9cb5fb3759f46b10bc66e676fa7f44c51e84824b" + resolved "https://codeload.github.com/code-asher/vscode/tar.gz/7185f35737bd0b943119a0eee4c981677d13e23e" dependencies: "@coder/logger" "^1.1.16" "@microsoft/applicationinsights-web" "^2.6.4" @@ -396,9 +396,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= core-js@^3.6.5: - version "3.17.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.17.3.tgz#8e8bd20e91df9951e903cabe91f9af4a0895bc1e" - integrity sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw== + version "3.18.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.1.tgz#289d4be2ce0085d40fc1244c0b1a54c00454622f" + integrity sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA== core-util-is@~1.0.0: version "1.0.3"