From ca3b34228982fc286718b706f03b0e0fb83fe653 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Sat, 18 May 2024 01:10:48 -0700 Subject: [PATCH] fix and test loading ESM providers in VS Code (#54) - Use a hacky pure-JS converter from ESM to CommonJS instead of the esbuild approach, which was very heavyweight due to its use of wasm and was hard for consumers of @openctx/vscode-lib to use. - Refactor how clients can override imports. --- .github/workflows/ci.yml | 3 + bin/package.json | 2 +- .../browser/src/background/background.main.ts | 2 +- client/vscode-lib/src/controller.ts | 11 +- client/vscode-lib/src/testing.ts | 22 +- .../vscode-lib/src/util/esmToCommonJS.test.ts | 88 ++++++ client/vscode-lib/src/util/esmToCommonJS.ts | 38 +++ .../vscode-lib/src/util/importHelpers.test.ts | 14 + client/vscode-lib/src/util/importHelpers.ts | 67 +++++ client/vscode-lib/tsconfig.json | 1 + client/vscode/.vscodeignore | 3 +- client/vscode/dev/build.mts | 6 +- client/vscode/dev/release.mts | 27 -- client/vscode/package.json | 5 +- client/vscode/src/dynamicImport.test.ts | 20 -- client/vscode/src/dynamicImport.ts | 70 ----- client/vscode/src/extension.ts | 2 - client/vscode/test/e2e.sh | 85 ++++++ .../fixtures/workspace/.vscode/settings.json | 2 +- client/vscode/test/integration/api.test.cts | 81 +++++- client/vscode/test/integration/main.cts | 38 ++- client/vscode/tsconfig.json | 3 - lib/client/src/client/client.ts | 24 +- lib/client/src/index.ts | 1 + .../providerClient/createProviderClient.ts | 2 +- .../transport/createTransport.test.ts | 105 ------- .../transport/createTransport.ts | 45 +-- .../providerClient/transport/module.test.ts | 34 +++ .../src/providerClient/transport/module.ts | 103 ++----- pnpm-lock.yaml | 264 ++++++++++++++++-- provider/url-fetcher/package.json | 9 +- 31 files changed, 738 insertions(+), 439 deletions(-) create mode 100644 client/vscode-lib/src/util/esmToCommonJS.test.ts create mode 100644 client/vscode-lib/src/util/esmToCommonJS.ts create mode 100644 client/vscode-lib/src/util/importHelpers.test.ts create mode 100644 client/vscode-lib/src/util/importHelpers.ts delete mode 100644 client/vscode/src/dynamicImport.test.ts delete mode 100644 client/vscode/src/dynamicImport.ts create mode 100644 client/vscode/test/e2e.sh create mode 100644 lib/client/src/providerClient/transport/module.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42196bb0..c96ae71a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,3 +94,6 @@ jobs: if: matrix.runner == 'ubuntu' - run: pnpm -C client/vscode run test:integration if: github.ref == 'refs/heads/main' && (matrix.runner == 'windows' || matrix.runner == 'macos') + - run: xvfb-run -a pnpm -C client/vscode run test:e2e + # if: matrix.runner == 'ubuntu' # only run e2e tests on Linux + if: false # no openctx logs printed so this fails for some reason on CI \ No newline at end of file diff --git a/bin/package.json b/bin/package.json index ddd81782..fc5bc220 100644 --- a/bin/package.json +++ b/bin/package.json @@ -10,6 +10,6 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "esbuild": "^0.19.11" + "esbuild": "^0.21.3" } } diff --git a/client/browser/src/background/background.main.ts b/client/browser/src/background/background.main.ts index 936f145b..25f91020 100644 --- a/client/browser/src/background/background.main.ts +++ b/client/browser/src/background/background.main.ts @@ -37,7 +37,7 @@ function main(): void { configuration: () => configurationChanges, logger: console.error, makeRange: r => r, - dynamicImportFromUri: uri => Promise.resolve({ default: getBuiltinProvider(uri) }), + importProvider: uri => Promise.resolve({ default: getBuiltinProvider(uri) }), }) subscriptions.add(() => client.dispose()) diff --git a/client/vscode-lib/src/controller.ts b/client/vscode-lib/src/controller.ts index 38ac7dee..f37be473 100644 --- a/client/vscode-lib/src/controller.ts +++ b/client/vscode-lib/src/controller.ts @@ -2,7 +2,6 @@ import { type AuthInfo, type CapabilitiesParams, type Client, - type ClientEnv, type ItemsParams, type MentionsParams, type Range, @@ -28,6 +27,7 @@ import { createHoverProvider } from './ui/editor/hover.js' import { createShowFileItemsList } from './ui/fileItemsList.js' import { createStatusBarItem } from './ui/statusBarItem.js' import { createErrorWaiter } from './util/errorWaiter.js' +import { importProvider } from './util/importHelpers.js' import { observeWorkspaceConfigurationChanges, toEventEmitter } from './util/observable.js' export type VSCodeClient = Client @@ -57,13 +57,11 @@ export function createController({ secrets: secretsInput, outputChannel, getAuthInfo, - dynamicImportFromSource, features, }: { secrets: Observable | vscode.SecretStorage outputChannel: vscode.OutputChannel getAuthInfo?: (secrets: vscode.SecretStorage, providerUri: string) => Promise - dynamicImportFromSource?: ClientEnv['dynamicImportFromSource'] features: { annotations?: boolean; statusBar?: boolean } }): { controller: Controller @@ -113,12 +111,7 @@ export function createController({ : undefined, makeRange, logger: message => outputChannel.appendLine(message), - - // On VS Code desktop, use a workaround for dynamic imports. - dynamicImportFromSource: - vscode.env.uiKind === vscode.UIKind.Desktop && process.env.DESKTOP_BUILD - ? dynamicImportFromSource - : undefined, + importProvider, }) const errorTapObserver: Partial> = { diff --git a/client/vscode-lib/src/testing.ts b/client/vscode-lib/src/testing.ts index df333a20..134ce848 100644 --- a/client/vscode-lib/src/testing.ts +++ b/client/vscode-lib/src/testing.ts @@ -1,27 +1,11 @@ -import type { Annotation, Item, ItemsParams } from '@openctx/client' -import type * as vscode from 'vscode' import type { Controller } from './controller.js' /** * The API exposed for this VS Code extension's tests. */ -export interface ExtensionApiForTesting { - /** - * Get OpenCtx items for the document. - */ - getItems(params: ItemsParams): Promise - - /** - * Get OpenCtx annotations for the document. - */ - getAnnotations( - doc: Pick - ): Promise[] | null> -} +export interface ExtensionApiForTesting + extends Pick {} export function createApiForTesting(controller: Controller): ExtensionApiForTesting { - return { - getItems: params => controller.items(params), - getAnnotations: doc => controller.annotations(doc), - } + return controller } diff --git a/client/vscode-lib/src/util/esmToCommonJS.test.ts b/client/vscode-lib/src/util/esmToCommonJS.test.ts new file mode 100644 index 00000000..ac9d05cb --- /dev/null +++ b/client/vscode-lib/src/util/esmToCommonJS.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from 'vitest' +import { esmToCommonJS } from './esmToCommonJS.js' + +describe('esmToCommonJS', () => { + test('default import statements', () => { + const esm = "import foo from './foo.js'" + const expected = "const foo = require('./foo.js');" + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('named import statements', () => { + const esm = "import { foo, bar } from './foobar.js'" + const expected = "const { foo, bar } = require('./foobar.js');" + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('namespace import statements', () => { + const esm = "import * as foobar from './foobar.js'" + const expected = "const foobar = require('./foobar.js');" + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('default export identifiers', () => { + const esm = 'export default foo' + const expected = 'module.exports = foo' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('default export literals', () => { + const esm = 'export default { a: 123 }' + const expected = 'module.exports = { a: 123 }' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('default export multi-line literals', () => { + const esm = 'export default { a: 123\nb: 456 }' + const expected = 'module.exports = { a: 123\nb: 456 }' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('named export statements', () => { + const esm = 'export { foo, bar }' + const expected = 'module.exports = { foo, bar };' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('named export statements with "as" keyword', () => { + const esm = 'export { foo as default }' + const expected = 'module.exports = foo;' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('export const statements', () => { + const esm = 'export const foo = 123;' + const expected = 'exports.foo = 123;' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('export function statements', () => { + const esm = 'export function foo(bar) {' + const expected = 'exports.foo = function foo(bar) {' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('export class statements', () => { + const esm = 'export class Foo {' + const expected = 'exports.Foo = class Foo {' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('minified code 1', () => { + const esm = 'var s=1;export{s as default}' + const expected = 'var s=1;module.exports = s;' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('minified code 2', () => { + const esm = 'function(){}export{s as default}' + const expected = 'function(){}module.exports = s;' + expect(esmToCommonJS(esm)).toBe(expected) + }) + + test('minified code 3', () => { + const esm = 'export{x as y,s as default}' + const expected = 'module.exports = s;' + expect(esmToCommonJS(esm)).toBe(expected) + }) +}) diff --git a/client/vscode-lib/src/util/esmToCommonJS.ts b/client/vscode-lib/src/util/esmToCommonJS.ts new file mode 100644 index 00000000..cfbdc3f2 --- /dev/null +++ b/client/vscode-lib/src/util/esmToCommonJS.ts @@ -0,0 +1,38 @@ +/** + * Convert from ESM JavaScript to CommonJS. + * + * NOTE(sqs): This is hacky! I hope VS Code starts supporting ESM import() in VS Code extensions + * soon so we can get rid of this! See https://github.com/microsoft/vscode/issues/130367. + * + * @param esm An ESM JavaScript string, assumed to have no import() calls. + */ +export function esmToCommonJS(esm: string): string { + // Convert import statements. + let cjs = esm.replace(/(?<=^|\b)import\s+(\w+)\s+from\s+['"](.+)['"]/gm, "const $1 = require('$2');") + cjs = cjs.replace( + /(?<=^|\b)import\s*\{\s*([\w\s,]+)\s*\}\s+from\s+['"](.+)['"]/gm, + "const { $1} = require('$2');" + ) + cjs = cjs.replace( + /(?<=^|\b)import\s+\*\s+as\s+(\w+)\s+from\s+['"](.+)['"]/gm, + "const $1 = require('$2');" + ) + + // Convert export default statements. + cjs = cjs.replace(/(?<=^|\b)export\s+default\s+/gm, 'module.exports = ') + + // Convert named export statements. + cjs = cjs.replace( + /(?<=^|\b)export\s*\{\s*(?:[^},]*,\s*)*([\w\s,]+) as default\s*\}/gm, + 'module.exports = $1;' + ) + cjs = cjs.replace(/(?<=^|\b)export\s*\{\s*([\w\s,]+)\s*\}/gm, 'module.exports = { $1};') + cjs = cjs.replace(/(?<=^|\b)export\s+const\s+(\w+)\s*=\s*(.+);/gm, 'exports.$1 = $2;') + cjs = cjs.replace( + /(?<=^|\b)export\s+function\s+(\w+)\s*\((.*)\)\s*\{/gm, + 'exports.$1 = function $1($2) {' + ) + cjs = cjs.replace(/(?<=^|\b)export\s+class\s+(\w+)\s*\{/gm, 'exports.$1 = class $1 {') + + return cjs +} diff --git a/client/vscode-lib/src/util/importHelpers.test.ts b/client/vscode-lib/src/util/importHelpers.test.ts new file mode 100644 index 00000000..fa3223bb --- /dev/null +++ b/client/vscode-lib/src/util/importHelpers.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test, vi } from 'vitest' +import { importESMFromString } from './importHelpers.js' + +vi.mock('vscode', () => ({ + env: { uiKind: 1 }, + UIKind: { Desktop: 1 }, +})) + +describe('importESMFromString', () => { + test('works', async () => + expect({ ...((await importESMFromString('export default 123')) as any) }).toEqual({ + default: 123, + })) +}) diff --git a/client/vscode-lib/src/util/importHelpers.ts b/client/vscode-lib/src/util/importHelpers.ts new file mode 100644 index 00000000..af9bb7b3 --- /dev/null +++ b/client/vscode-lib/src/util/importHelpers.ts @@ -0,0 +1,67 @@ +import { readFile } from 'node:fs/promises' +import { type Provider, fetchProviderSource } from '@openctx/client' +import * as vscode from 'vscode' +import { esmToCommonJS } from './esmToCommonJS.js' + +export async function importProvider(providerUri: string): Promise<{ default: Provider }> { + const url = new URL(providerUri) + + let source: string + if (url.protocol === 'file:') { + source = await readFile(url.pathname, 'utf-8') + } else { + source = await fetchProviderSource(providerUri) + } + + if (vscode.env.uiKind === vscode.UIKind.Desktop) { + // VS Code Desktop only supports require()ing of CommonJS modules. + return importCommonJSFromESM(source) as { default: Provider } + } + + // VS Code Web supports import()ing, but not cross-origin. + return (await importESMFromString(source)) as { default: Provider } +} + +/** + * Convert an ESM bundle to CommonJS. + * + * VS Code does not support dynamically import()ing ES modules (see + * https://github.com/microsoft/vscode/issues/130367). But we always want OpenCtx providers to be ES + * modules for consistency. So, we need to rewrite the ESM bundle to CommonJS to import it here. + * + * Note that it's deceiving because VS Code using extensionDevelopmentPath *does* support dynamic + * import()s, but they fail when installing the extension from a `.vsix` (including from the + * extension marketplace). When VS Code supports dynamic import()s for extensions, we can remove + * this. + */ +function importCommonJSFromESM(esmSource: string): unknown { + return { + default: requireCommonJSFromString('', esmToCommonJS(esmSource)), + } +} + +export function requireCommonJSFromString(filename: string, cjsSource: string): unknown { + const Module: any = module.constructor + const m = new Module() + m._compile(cjsSource, filename) + return m.exports +} + +/** + * VS Code Web requires this because it blocks import()s of remote URLs, so we need to fetch() the + * source first and then import it by string. + */ +export function importESMFromString(esmSource: string): Promise { + const url = `data:text/javascript;charset=utf-8;base64,${base64Encode(esmSource)}` + return import(/* @vite-ignore */ url) +} + +/** + * See https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem for why we need + * something other than just `btoa` for base64 encoding. + */ +function base64Encode(text: string): string { + const bytes = new TextEncoder().encode(text) + const binString = String.fromCodePoint(...bytes) + return btoa(binString) +} diff --git a/client/vscode-lib/tsconfig.json b/client/vscode-lib/tsconfig.json index d212b677..da982cf3 100644 --- a/client/vscode-lib/tsconfig.json +++ b/client/vscode-lib/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist", + "lib": ["ESNext", "DOM"], }, "include": ["src"], "exclude": ["dist"], diff --git a/client/vscode/.vscodeignore b/client/vscode/.vscodeignore index 93302a2d..a663415e 100644 --- a/client/vscode/.vscodeignore +++ b/client/vscode/.vscodeignore @@ -9,5 +9,4 @@ node_modules/ tsconfig.json test/ *.tsbuildinfo -dev/** -!node_modules/esbuild-wasm/esbuild.wasm \ No newline at end of file +dev/** \ No newline at end of file diff --git a/client/vscode/dev/build.mts b/client/vscode/dev/build.mts index d6673bdc..24324b07 100644 --- a/client/vscode/dev/build.mts +++ b/client/vscode/dev/build.mts @@ -13,9 +13,10 @@ const buildTarget = getBuildTarget() const commonBuildOptions: esbuild.BuildOptions = { entryPoints: ['src/extension.ts'], bundle: true, - external: ['vscode', 'esbuild-wasm/esbuild.wasm', 'fs/promises'], + external: ['vscode'], format: 'cjs', sourcemap: true, + treeShaking: true, minify: false, } @@ -25,13 +26,12 @@ const buildOptions: Record = { platform: 'node', outfile: 'dist/extension.node.cjs', outExtension: { '.js': '.cjs' }, - define: { ...commonBuildOptions.define, 'process.env.DESKTOP_BUILD': 'true' }, }, web: { ...commonBuildOptions, + external: [...commonBuildOptions.external!, 'node:fs/promises'], platform: 'browser', outfile: 'dist/extension.web.js', - define: { ...commonBuildOptions.define, 'process.env.DESKTOP_BUILD': 'false' }, alias: { ...commonBuildOptions.alias, path: 'path-browserify' }, }, } diff --git a/client/vscode/dev/release.mts b/client/vscode/dev/release.mts index 84f594f5..ce3d4232 100644 --- a/client/vscode/dev/release.mts +++ b/client/vscode/dev/release.mts @@ -95,33 +95,6 @@ execFileSync( } ) -// Add the esbuild wasm file. -execFileSync('mkdir', ['-p', 'extension/node_modules/esbuild-wasm'], { stdio: 'inherit' }) -execFileSync( - 'cp', - [ - 'node_modules/esbuild-wasm/esbuild.wasm', - 'node_modules/esbuild-wasm/package.json', - 'extension/node_modules/esbuild-wasm/', - ], - { - stdio: 'inherit', - } -) -execFileSync( - 'zip', - [ - '-ur', - 'dist/openctx.vsix', - 'extension/node_modules/esbuild-wasm/esbuild.wasm', - 'extension/node_modules/esbuild-wasm/package.json', - ], - { - stdio: 'inherit', - } -) -execFileSync('rm', ['-rf', 'extension/node_modules/esbuild-wasm/'], { stdio: 'inherit' }) - // Publish the extension. console.error(`Publishing ${releaseType} release at version ${version}...`) if (dryRun) { diff --git a/client/vscode/package.json b/client/vscode/package.json index 781df63b..bcf2147c 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -32,6 +32,7 @@ "test": "pnpm run -s test:unit && pnpm run -s test:integration", "test:integration": "tsc --build ./test/integration && pnpm run -s build:dev:desktop && node --inspect dist/tsc/test/integration/main.cjs", "test:unit": "vitest", + "test:e2e": "bash test/e2e.sh", "vscode:prepublish": "pnpm -s run build" }, "categories": ["Programming Languages", "Linters", "Snippets", "Education"], @@ -173,9 +174,7 @@ }, "dependencies": { "@openctx/client": "workspace:*", - "@openctx/ui-common": "workspace:*", "@openctx/vscode-lib": "workspace:*", - "esbuild-wasm": "^0.19.11", "path-browserify": "^1.0.1", "rxjs": "^7.8.1" }, @@ -188,7 +187,7 @@ "@vscode/test-web": "^0.0.47", "@vscode/vsce": "^2.22.0", "concurrently": "^8.2.2", - "esbuild": "^0.19.11", + "esbuild": "^0.21.3", "glob": "^10.3.10", "mocha": "^10.2.0", "ovsx": "^0.8.3", diff --git a/client/vscode/src/dynamicImport.test.ts b/client/vscode/src/dynamicImport.test.ts deleted file mode 100644 index 1894214a..00000000 --- a/client/vscode/src/dynamicImport.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { esmToCommonJS } from './dynamicImport.js' - -vi.mock('vscode', () => ({ - env: { uiKind: 1 }, - UIKind: { Desktop: 1 }, -})) - -describe('esmToCommonJS', () => { - beforeEach(() => { - process.env.DESKTOP_BUILD = 'true' - }) - afterEach(() => { - process.env.DESKTOP_BUILD = undefined - }) - - test('works', async () => { - expect(await esmToCommonJS('export default 123')).toContain('var stdin_default = 123') - }) -}) diff --git a/client/vscode/src/dynamicImport.ts b/client/vscode/src/dynamicImport.ts deleted file mode 100644 index 7d92f497..00000000 --- a/client/vscode/src/dynamicImport.ts +++ /dev/null @@ -1,70 +0,0 @@ -// VS Code does not support dynamically import()ing ES modules (see -// https://github.com/microsoft/vscode/issues/130367). But we always want OpenCtx providers to -// be ES modules for consistency. So, we need to rewrite the ESM bundle to CommonJS to import it -// here. -// -// Note that it's deceiving because VS Code using extensionDevelopmentPath *does* support dynamic -// import()s, but they fail when installing the extension from a `.vsix` (including from the -// extension marketplace). -// -// When VS Code supports dynamic import()s for extensions, we can remove this. - -import type { Provider } from '@openctx/client' -import * as esbuild from 'esbuild-wasm/esm/browser.js' -import { readFile } from 'fs/promises' -import * as vscode from 'vscode' - -function requireFromString(cjsSource: string, filename: string): { default: Provider } { - const Module: any = module.constructor - const m = new Module() - m._compile(cjsSource, filename) - return m.exports -} - -export async function dynamicImportFromSource( - _uri: string, - esmSource?: string -): Promise<{ exports: { default: Provider } }> { - if (esmSource === undefined) { - throw new Error('vscode dynamicImport requires esmSource') - } - const cjsSource = await esmToCommonJS(esmSource) - const cjsModule = requireFromString(cjsSource, '') - return Promise.resolve({ exports: cjsModule }) -} - -export async function esmToCommonJS(esmSource: string): Promise { - await initializeEsbuildOnce() - const cjsSource = await esbuild.transform(esmSource, { format: 'cjs', target: 'esnext' }) - return cjsSource.code -} - -let esbuildInitialization: Promise | undefined -async function initializeEsbuildOnce(): Promise { - if (esbuildInitialization) { - return esbuildInitialization - } - - async function run(): Promise { - if (typeof self === 'undefined') { - // Required for tests to pass in Node. - ;(globalThis as any).self = globalThis - } - - const initOptions: esbuild.InitializeOptions = { worker: false } - - if (vscode.env.uiKind === vscode.UIKind.Desktop && process.env.DESKTOP_BUILD) { - if (!globalThis.crypto) { - globalThis.crypto ??= require('crypto') - } - const wasmModule = await readFile(require.resolve('esbuild-wasm/esbuild.wasm')) - initOptions.wasmModule = new WebAssembly.Module(wasmModule) - } else { - throw new Error('not implemented - VS Code Web uses dynamic import()') - } - return esbuild.initialize(initOptions) - } - - esbuildInitialization = run() - return esbuildInitialization -} diff --git a/client/vscode/src/extension.ts b/client/vscode/src/extension.ts index 92b571be..5e8e57f4 100644 --- a/client/vscode/src/extension.ts +++ b/client/vscode/src/extension.ts @@ -1,7 +1,6 @@ import { type ExtensionApiForTesting, createController } from '@openctx/vscode-lib' import * as vscode from 'vscode' import { getAuthInfo, secretsChanges } from './authInfo.js' -import { dynamicImportFromSource } from './dynamicImport.js' /** * Start the extension, watching all relevant configuration and secrets for changes. @@ -23,7 +22,6 @@ export function activate( secrets, outputChannel, getAuthInfo, - dynamicImportFromSource, features: { annotations: true, statusBar: true, diff --git a/client/vscode/test/e2e.sh b/client/vscode/test/e2e.sh new file mode 100644 index 00000000..a05f725d --- /dev/null +++ b/client/vscode/test/e2e.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# This is an end-to-end test that checks for common failures from using JavaScript module providers +# in VS Code. This is tricky because VS Code's extension host does not support import()ing, so we +# need to rewrite to CommonJS and require. See https://github.com/microsoft/vscode/issues/130367. + +set -eu + +MY_DIR=$(cd $(dirname $0) > /dev/null; pwd) +VSCODE_EXT_DIR=$(realpath "$MY_DIR"/..) +cd "$VSCODE_EXT_DIR" > /dev/null + +VSCODE_VERSION=$(grep -Eo 'version: (.+),' "$VSCODE_EXT_DIR"/test/integration/main.cts | sed "s/version: '//" | sed "s/',//") +VSCODE_BIN=$(realpath .vscode-test/vscode-*-$VSCODE_VERSION/bin/code) + +RELEASE_TYPE=pre pnpm run release:dry-run +VSIX_FILE="$VSCODE_EXT_DIR"/dist/openctx.vsix + +TMPDIR=$(mktemp -d) +function cleanup() { + sleep 0.25 + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +USER_DATA_DIR="$TMPDIR"/vscode-user-data-dir +WORKSPACE_DIR="$TMPDIR"/vscode-workspace +mkdir -p "$WORKSPACE_DIR" + +export OPENCTX_VSCODE_INTEGRATION_TEST_TMP_SCRATCH_DIR="$TMPDIR"/scratch +mkdir -p "$OPENCTX_VSCODE_INTEGRATION_TEST_TMP_SCRATCH_DIR" + +VSCODE_ARGS="--user-data-dir $USER_DATA_DIR --profile openctx-integration-test --no-sandbox --disable-gpu-sandbox --skip-release-notes --skip-welcome --disable-telemetry --disable-updates --disable-workspace-trust --sync off" + +HELLO_PATH="$WORKSPACE_DIR"/hello.txt +echo "Hello, world" > "$HELLO_PATH" + +# Open VS Code. +"$VSCODE_BIN" $VSCODE_ARGS --new-window --extensionTestsPath="$VSCODE_EXT_DIR"/dist/tsc/test/integration/index.cjs "$WORKSPACE_DIR" +"$VSCODE_BIN" $VSCODE_ARGS --reuse-window "$HELLO_PATH" + + +# Set the workspace settings. +mkdir -p "$WORKSPACE_DIR"/.vscode +PROVIDER_URI_PREFIX="file://"$(realpath "$VSCODE_EXT_DIR"/../../lib/client/src/providerClient/transport/testdata/) +echo '{"openctx.enable":true,"openctx.debug":true,"openctx.providers":{' '"'$PROVIDER_URI_PREFIX/{commonjsExtProvider.cjs,commonjsProvider.js,esmExtProvider.mjs,esmProvider.js}'"':true, '}}' > "$WORKSPACE_DIR"/.vscode/settings.json + +# Install the extension (after polling to see when the new VS Code profile is ready). +while true; do + "$VSCODE_BIN" $VSCODE_ARGS --list-extensions > /dev/null 2>&1 && break + sleep 0.25 +done +"$VSCODE_BIN" $VSCODE_ARGS --install-extension "$VSIX_FILE" + + +# Tail the "OpenCtx" output channel. +while true; do + OUTPUT_CHANNEL_FILE=$(find "$USER_DATA_DIR"/logs -name '*-OpenCtx.log') + [ -n "$OUTPUT_CHANNEL_FILE" ] && break + sleep 0.5 +done +echo "# OpenCtx output channel: $OUTPUT_CHANNEL_FILE" +tail --quiet -f "$OUTPUT_CHANNEL_FILE" & + +# Other logs: +#tail --quiet -f $(find "$USER_DATA_DIR"/logs -name renderer.log) & +#tail --quiet -f $(find "$USER_DATA_DIR"/logs -name exthost.log) & + +if [ -n "${CI-}" ]; then + sleep 20 +else + sleep 5 +fi + +# Tests +if grep -q 'failed to get provider capabilities' "$OUTPUT_CHANNEL_FILE"; then + echo 'FAIL - saw failures in the output' + exit 1 +fi +if ! grep -q 'received capabilities' "$OUTPUT_CHANNEL_FILE"; then + echo 'FAIL - did not see any "received capabilities" messages in the output' + exit 1 +fi +echo PASS +exit 0 \ No newline at end of file diff --git a/client/vscode/test/fixtures/workspace/.vscode/settings.json b/client/vscode/test/fixtures/workspace/.vscode/settings.json index 4c6f331b..501bb015 100644 --- a/client/vscode/test/fixtures/workspace/.vscode/settings.json +++ b/client/vscode/test/fixtures/workspace/.vscode/settings.json @@ -5,7 +5,7 @@ // "https://sourcegraph.test:3443/.api/openctx": true, // "https://openctx.org/npm/@openctx/provider-hello-world": true, "../../../../../../provider/hello-world/dist/index.js": true, - "../../../../../../provider/links/dist/index.js": { + "../../../../../../provider/links/dist/index.js": { "links": [ { "title": "Telemetry", diff --git a/client/vscode/test/integration/api.test.cts b/client/vscode/test/integration/api.test.cts index 7372cec2..9f1da6f5 100644 --- a/client/vscode/test/integration/api.test.cts +++ b/client/vscode/test/integration/api.test.cts @@ -1,18 +1,83 @@ import * as assert from 'assert' +import path from 'path' +import type { ClientConfiguration } from '@openctx/client' import type { ExtensionApiForTesting } from '@openctx/vscode-lib' +import { copyFile } from 'fs/promises' +import { before } from 'mocha' import * as vscode from 'vscode' -suite('API', () => { - test('get exported extension API', async () => { - // Wait for the extension to become ready. - const ext = vscode.extensions.getExtension('sourcegraph.openctx') - assert.ok(ext, 'extension not found') +const openctxDir = path.join(__dirname, '../../../../../..') +const scratchDir = process.env.OPENCTX_VSCODE_INTEGRATION_TEST_TMP_SCRATCH_DIR! +if (!scratchDir) { + throw new Error('scratch dir not set') +} - const api = await ext.activate() +suite('API', () => { + let api: ExtensionApiForTesting + before(async () => { + api = await getExtension() + }) + test('items', async () => { + await updateOpenCtxSettings({ + enable: true, + debug: true, + providers: { + [vscode.Uri.file( + path.join(openctxDir, 'provider/hello-world/dist/index.js') + ).toString()]: true, + }, + }) assert.deepEqual( - (await api.getItems({}))?.map(item => item.title), - ['✨ Hello, world!', 'Bazel at Sourcegraph', 'View all Storybooks'] + (await api.items({})).map(item => item.title), + ['✨ Hello, world!'] ) }) + + /** + * To manually test these in a VS Code non-debug extension host: + */ + testLoadProviderFromFile('commonjsExtProvider.cjs') + testLoadProviderFromFile('commonjsProvider.js') + testLoadProviderFromFile('esmExtProvider.mjs') + testLoadProviderFromFile('esmProvider.js') + function testLoadProviderFromFile(providerFilename: string) { + test(`load provider from ${path.extname(providerFilename)} file`, async () => { + const origProviderPath = path.join( + openctxDir, + 'lib/client/src/providerClient/transport/testdata', + providerFilename + ) + + const providerPath = path.join(scratchDir, providerFilename) + await copyFile(origProviderPath, providerPath) + + await updateOpenCtxSettings({ + enable: true, + debug: true, + providers: { + [vscode.Uri.file(providerPath).toString()]: true, + }, + }) + + assert.deepEqual( + (await api.capabilities({})).map(cap => cap.meta.name), + ['foo'] + ) + }) + } }) + +async function getExtension(): Promise { + const ext = vscode.extensions.getExtension('sourcegraph.openctx') + assert.ok(ext, 'extension not found') + const api = await ext.activate() + return api +} + +async function updateOpenCtxSettings(settings: ClientConfiguration): Promise { + const openctxSection = vscode.workspace.getConfiguration('openctx') + for (const [key, value] of Object.entries(settings)) { + await openctxSection.update(key, value, vscode.ConfigurationTarget.Global) + } +} diff --git a/client/vscode/test/integration/main.cts b/client/vscode/test/integration/main.cts index 4227f2a7..cf79528e 100644 --- a/client/vscode/test/integration/main.cts +++ b/client/vscode/test/integration/main.cts @@ -1,15 +1,13 @@ +import { tmpdir } from 'os' import * as path from 'path' import { runTests } from '@vscode/test-electron' +import { mkdir, mkdtemp, rm } from 'fs/promises' async function main(): Promise { // When run, this script's filename is `client/vscode/dist/tsc/test/integration/main.js`, so // __dirname is derived from that path, not this file's source path. const clientVsCodeRoot = path.resolve(__dirname, '..', '..', '..', '..') - // The test workspace is not copied to out/ during the TypeScript build, so we need to refer to - // it in the src/ dir. - const testWorkspacePath = path.resolve(clientVsCodeRoot, 'test', 'fixtures', 'workspace') - // The directory containing the extension's package.json, passed to --extensionDevelopmentPath. const extensionDevelopmentPath = clientVsCodeRoot @@ -23,18 +21,36 @@ async function main(): Promise { 'index.cjs' ) - // Download VS Code, unzip it, and run the integration test. - process.exit( - await runTests({ - version: '1.88.1', + let exitCode: number + + // Ensure we're running in a clean VS Code user data dir. + const tmpDir = await mkdtemp(path.join(tmpdir(), 'openctx-vscode-integration-test-')) + const tmpUserDataDir = path.join(tmpDir, 'userdata') + const tmpWorkspaceDir = path.join(tmpDir, 'workspace') + const tmpScratchDir = path.join(tmpDir, 'scratch') + await mkdir(tmpUserDataDir) + await mkdir(tmpWorkspaceDir) + await mkdir(tmpScratchDir) + + try { + exitCode = await runTests({ + version: '1.89.1', extensionDevelopmentPath, extensionTestsPath, + extensionTestsEnv: { + OPENCTX_VSCODE_INTEGRATION_TEST_TMP_SCRATCH_DIR: tmpScratchDir, + }, launchArgs: [ - testWorkspacePath, - '--disable-extensions', // disable other extensions + tmpWorkspaceDir, + '--profile-temp', + `--user-data-dir=${tmpUserDataDir}`, + '--disable-extensions', ], }) - ) + } finally { + await rm(tmpUserDataDir, { recursive: true, force: true }) + } + process.exit(exitCode ?? 1) } main().catch(error => { console.error('Failed to run tests:', error) diff --git a/client/vscode/tsconfig.json b/client/vscode/tsconfig.json index 6d410040..785805e9 100644 --- a/client/vscode/tsconfig.json +++ b/client/vscode/tsconfig.json @@ -12,8 +12,5 @@ { "path": "../../lib/client", }, - { - "path": "../../lib/ui-common", - }, ], } diff --git a/lib/client/src/client/client.ts b/lib/client/src/client/client.ts index 2ff4642c..55bb51f0 100644 --- a/lib/client/src/client/client.ts +++ b/lib/client/src/client/client.ts @@ -88,17 +88,7 @@ export interface ClientEnv { * Called (if set) to dynamically import an OpenCtx provider from a URI. This can be used * by runtimes that need to pre-bundle providers. */ - dynamicImportFromUri?: (uri: string) => Promise<{ default: Provider }> - - /** - * Called (if set) to dynamically import an OpenCtx provider from its ES module source - * code. This can be used by runtimes that only support `require()` and CommonJS (such as VS - * Code). - */ - dynamicImportFromSource?: ( - uri: string, - esmSource: string - ) => Promise<{ exports: { default: Provider } }> + importProvider?: (uri: string) => Promise<{ default: Provider }> /** * @internal @@ -270,9 +260,7 @@ export function createClient(env: ClientEnv): Client { { providerBaseUri: env.providerBaseUri, logger, - dynamicImportFromUri: env.dynamicImportFromUri, - dynamicImportFromSource: - env.dynamicImportFromSource, + importProvider: env.importProvider, } ), settings, @@ -411,10 +399,7 @@ interface ProviderCacheKey { function createProviderPool(): { getOrCreate: ( key: ProviderCacheKey, - env: Pick< - ClientEnv, - 'providerBaseUri' | 'logger' | 'dynamicImportFromUri' | 'dynamicImportFromSource' - > + env: Pick, 'providerBaseUri' | 'logger' | 'importProvider'> ) => ProviderClient } { function cacheKey(key: ProviderCacheKey): string { @@ -438,8 +423,7 @@ function createProviderPool(): { providerBaseUri: env.providerBaseUri, authInfo: key.authInfo, logger: env.logger, - dynamicImportFromUri: env.dynamicImportFromUri, - dynamicImportFromSource: env.dynamicImportFromSource, + importProvider: env.importProvider, }) cache.set(cacheKey(key), c) return c diff --git a/lib/client/src/index.ts b/lib/client/src/index.ts index 8a71b2ef..321ffacd 100644 --- a/lib/client/src/index.ts +++ b/lib/client/src/index.ts @@ -5,3 +5,4 @@ export { observeItems, type Annotation, type EachWithProviderUri } from './api.j export { createClient, type AuthInfo, type Client, type ClientEnv } from './client/client.js' export { type ConfigurationUserInput as ClientConfiguration } from './configuration.js' export type { Logger } from './logger.js' +export { fetchProviderSource } from './providerClient/transport/module.js' diff --git a/lib/client/src/providerClient/createProviderClient.ts b/lib/client/src/providerClient/createProviderClient.ts index 01e605b3..87feb44a 100644 --- a/lib/client/src/providerClient/createProviderClient.ts +++ b/lib/client/src/providerClient/createProviderClient.ts @@ -38,7 +38,7 @@ export interface ProviderClient { export interface ProviderClientOptions extends Pick< ProviderTransportOptions, - 'providerBaseUri' | 'authInfo' | 'logger' | 'dynamicImportFromUri' | 'dynamicImportFromSource' + 'providerBaseUri' | 'authInfo' | 'logger' | 'importProvider' > {} /** diff --git a/lib/client/src/providerClient/transport/createTransport.test.ts b/lib/client/src/providerClient/transport/createTransport.test.ts index b3f8c2cd..1643f4cd 100644 --- a/lib/client/src/providerClient/transport/createTransport.test.ts +++ b/lib/client/src/providerClient/transport/createTransport.test.ts @@ -1,4 +1,3 @@ -import { readFile } from 'node:fs/promises' import { afterEach } from 'node:test' import type { CapabilitiesResult, ResponseMessage } from '@openctx/protocol' import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' @@ -49,110 +48,6 @@ describe('createTransport', () => { )) }) - describe('dynamicImport options', () => { - test('dynamicImportFromUri local', async () => { - const provider = createTransport('file:///myProvider.js', { - dynamicImportFromUri(uri) { - expect(uri).toBe('file:///myProvider.js') - return Promise.resolve({ - default: { - capabilities: () => ({ - selector: [{ path: 'asdf' }], - meta: { name: 'foo' }, - }), - items: () => [], - }, - }) - }, - }) - expect(await provider.capabilities({}, {})).toEqual({ - selector: [{ path: 'asdf' }], - meta: { name: 'foo' }, - }) - }) - - test('dynamicImportFromUri remote', async () => { - const provider = createTransport('http://example.com/myProvider.js', { - dynamicImportFromUri(uri) { - expect(uri).toBe('http://example.com/myProvider.js') - return Promise.resolve({ - default: { - capabilities: () => ({ - selector: [{ path: 'asdf' }], - meta: { name: 'foo' }, - }), - items: () => [], - }, - }) - }, - }) - expect(await provider.capabilities({}, {})).toEqual({ - selector: [{ path: 'asdf' }], - meta: { name: 'foo' }, - }) - }) - - test('dynamicImportFromSource', async () => { - const content = await readFile(__dirname + '/testdata/esmProvider.js', 'utf8') - fetchMocker.mockOnce(content, { headers: { 'Content-Type': 'text/javascript' } }) - - const provider = createTransport('https://example.com/myProvider.js', { - dynamicImportFromSource(uri, esmSource) { - expect(uri).toBe('https://example.com/myProvider.js') - expect(esmSource).toBe(content) - return Promise.resolve({ - exports: { - default: { - capabilities: () => ({ - selector: [{ path: 'asdf' }], - meta: { name: 'foo' }, - }), - items: () => [], - }, - }, - }) - }, - }) - expect(await provider.capabilities({}, {})).toEqual({ - selector: [{ path: 'asdf' }], - meta: { name: 'foo' }, - }) - }) - }) - - describe('HTTP file', () => { - test('ESM .js', async () => { - const content = await readFile(__dirname + '/testdata/esmProvider.js', 'utf8') - fetchMocker.mockOnce(content, { headers: { 'Content-Type': 'text/javascript' } }) - const provider = createTransport('https://example.com/myProvider.js', {}) - await expectProviderTransport(provider) - }) - - test('arbitrary unicode', async () => { - const content = await readFile(__dirname + '/testdata/emoji.js', 'utf8') - fetchMocker.mockOnce(content, { headers: { 'Content-Type': 'text/javascript' } }) - const provider = createTransport('https://example.com/emoji.js', {}) - await expectProviderTransport(provider) - }) - - describe('errors', () => { - test('bad Content-Type', async () => { - fetchMocker.mockResponseOnce('', { - status: 200, - headers: { 'Content-Type': 'text/html' }, - }) - const provider = createTransport('https://example.com/a.js', {}) - await expect(provider.capabilities({}, {})).rejects.toThrow(/invalid Content-Type/) - }) - - test('HTTP error 404', async () => { - fetchMocker.mockResponseOnce('', { status: 404 }) - const provider = createTransport('https://example.com/a.js', {}) - await expect(provider.capabilities({}, {})).rejects.toThrow(/404/) - }) - }) - }) - describe('HTTP endpoint (integration)', () => { const fetchMocker = createFetchMock(vi) beforeAll(() => fetchMocker.enableMocks()) diff --git a/lib/client/src/providerClient/transport/createTransport.ts b/lib/client/src/providerClient/transport/createTransport.ts index 3f33283d..3f5ed38e 100644 --- a/lib/client/src/providerClient/transport/createTransport.ts +++ b/lib/client/src/providerClient/transport/createTransport.ts @@ -3,7 +3,7 @@ import type { AuthInfo, ClientEnv } from '../../client/client.js' import type { Logger } from '../../logger.js' import { cachedTransport } from './cache.js' import { createHttpTransport } from './http.js' -import { createLocalModuleFileTransport, createRemoteModuleFileTransport } from './module.js' +import { createModuleTransport } from './module.js' /** * A provider transport is a low-level TypeScript wrapper around the provider protocol. It is a @@ -20,17 +20,14 @@ export type ProviderTransport = { } & { dispose?(): void } export interface ProviderTransportOptions - extends Pick< - ClientEnv, - 'providerBaseUri' | 'dynamicImportFromUri' | 'dynamicImportFromSource' - > { + extends Pick, 'providerBaseUri' | 'importProvider'> { authInfo?: AuthInfo cache?: boolean logger?: Logger } /** - * Create a transport that communicates with a provider URI using the provider API. + * Create a transport that communicates with a provider using the provider API. * * @internal */ @@ -40,25 +37,17 @@ export function createTransport( ): ProviderTransport { function doResolveProvider(providerUri: string): ProviderTransport { let url = new URL(providerUri, options.providerBaseUri) - if ( - url.protocol === 'file:' || - (runtimeSupportsImportFromUrl() && isRemoteJavaScriptFile(url)) - ) { - if (isHttpsPlusJs(url)) { - url = removePlusJs(url) - } - return createLocalModuleFileTransport(url.toString(), options) - } - if (isRemoteJavaScriptFile(url)) { - if (isHttpsPlusJs(url)) { - url = removePlusJs(url) - } - return createRemoteModuleFileTransport(url.toString(), options) - } - if (isHttpOrHttps(url)) { + + if (isHttpOrHttps(url) && !isRemoteJavaScriptFile(url)) { + // Provider is an HTTP endpoint. return createHttpTransport(providerUri, options) } - throw new Error(`Unsupported OpenCtx provider URI: ${providerUri}`) + + // Provider is a JavaScript module. + if (isHttpsPlusJs(url)) { + url = removePlusJs(url) + } + return createModuleTransport(url.toString(), options) } let provider = doResolveProvider(providerUri) @@ -98,13 +87,3 @@ function removePlusJs(url: URL): URL { function isWellKnownNpmUrl(url: URL): boolean { return url.protocol === 'https:' && url.host === 'openctx.org' && url.pathname.startsWith('/npm/') } - -function runtimeSupportsImportFromUrl(): boolean { - // `import('https://...')` is not supported natively in Node.js; see - // https://nodejs.org/api/esm.html#urls. - // - // TODO(sqs): this is hacky and not correct in general - // - // @ts-ignore - return typeof window !== 'undefined' -} diff --git a/lib/client/src/providerClient/transport/module.test.ts b/lib/client/src/providerClient/transport/module.test.ts new file mode 100644 index 00000000..9bcf3860 --- /dev/null +++ b/lib/client/src/providerClient/transport/module.test.ts @@ -0,0 +1,34 @@ +import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' +import { fetchProviderSource } from './module.js' + +describe('fetchProviderSource', () => { + const fetchMocker = createFetchMock(vi) + beforeAll(() => fetchMocker.enableMocks()) + afterEach(() => fetchMocker.resetMocks()) + afterAll(() => fetchMocker.disableMocks()) + + test('ok', async () => { + fetchMocker.mockResponseOnce('abc', { + status: 200, + headers: { 'Content-Type': 'application/javascript' }, + }) + expect(await fetchProviderSource('https://example.com/a.js')).toEqual('abc') + }) + + test('bad Content-Type', async () => { + fetchMocker.mockResponseOnce('', { + status: 200, + headers: { 'Content-Type': 'text/html' }, + }) + + await expect(fetchProviderSource('https://example.com/a.js')).rejects.toThrow( + /invalid Content-Type/ + ) + }) + + test('HTTP error 404', async () => { + fetchMocker.mockResponseOnce('', { status: 404 }) + await expect(fetchProviderSource('https://example.com/a.js')).rejects.toThrow(/404/) + }) +}) diff --git a/lib/client/src/providerClient/transport/module.ts b/lib/client/src/providerClient/transport/module.ts index 33ff46c5..c1040548 100644 --- a/lib/client/src/providerClient/transport/module.ts +++ b/lib/client/src/providerClient/transport/module.ts @@ -1,62 +1,41 @@ import type { Provider } from '@openctx/provider' import type { ProviderTransport, ProviderTransportOptions } from './createTransport.js' -export function createRemoteModuleFileTransport( +export function createModuleTransport( providerUri: string, - { - dynamicImportFromUri, - dynamicImportFromSource, - }: Pick + { importProvider }: Pick ): ProviderTransport { return lazyProvider( - dynamicImportFromUri - ? dynamicImportFromUri(providerUri).then(mod => providerFromModule(mod)) - : fetch(providerUri).then(async resp => { - if (!resp.ok) { - throw new Error( - `OpenCtx remote provider module URL ${providerUri} responded with HTTP error ${resp.status} ${resp.statusText}` - ) - } - const contentType = resp.headers.get('Content-Type')?.trim()?.replace(/;.*$/, '') - if ( - !contentType || - (contentType !== 'text/javascript' && - contentType !== 'application/javascript' && - contentType !== 'text/plain') - ) { - throw new Error( - `OpenCtx remote provider module URL ${providerUri} reported invalid Content-Type ${JSON.stringify( - contentType - )} (expected "text/javascript" or "text/plain")` - ) - } - - const moduleSource = await resp.text() - try { - const mod = await importModuleFromString( - providerUri, - moduleSource, - dynamicImportFromSource - ) - return providerFromModule(mod) - } catch (error) { - console.log(error) - throw error - } - }) + (importProvider ? importProvider(providerUri) : import(/* @vite-ignore */ providerUri)).then( + mod => providerFromModule(mod) + ) ) } -export function createLocalModuleFileTransport( - moduleUrl: string, - { dynamicImportFromUri }: Pick -): ProviderTransport { - return lazyProvider( - (dynamicImportFromUri - ? dynamicImportFromUri(moduleUrl) - : import(/* @vite-ignore */ moduleUrl) - ).then(providerFromModule) - ) +export async function fetchProviderSource(providerUri: string): Promise { + const resp = await fetch(providerUri) + + if (!resp.ok) { + throw new Error( + `OpenCtx remote provider module URL ${providerUri} responded with HTTP error ${resp.status} ${resp.statusText}` + ) + } + const contentType = resp.headers.get('Content-Type')?.trim()?.replace(/;.*$/, '') + if ( + !contentType || + (contentType !== 'text/javascript' && + contentType !== 'application/javascript' && + contentType !== 'text/plain') + ) { + throw new Error( + `OpenCtx remote provider module URL ${providerUri} reported invalid Content-Type ${JSON.stringify( + contentType + )} (expected "text/javascript" or "text/plain")` + ) + } + + const moduleSource = await resp.text() + return moduleSource } interface ProviderModule { @@ -65,30 +44,6 @@ interface ProviderModule { default: Provider | { default: Provider } } -async function importModuleFromString( - uri: string, - source: string, - dynamicImportFromSource: ProviderTransportOptions['dynamicImportFromSource'] -): Promise { - if (dynamicImportFromSource) { - return (await dynamicImportFromSource(uri, source)).exports - } - - // Note: Used by VS Code Web. - const url = `data:text/javascript;charset=utf-8;base64,${base64Encode(source)}` - return import(/* @vite-ignore */ url) -} - -/** - * See https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem for why we need - * something other than just `btoa` for base64 encoding. - */ -function base64Encode(text: string): string { - const bytes = new TextEncoder().encode(text) - const binString = String.fromCodePoint(...bytes) - return btoa(binString) -} - function providerFromModule(providerModule: ProviderModule): Provider { let impl = providerModule.default if ('default' in impl) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63940228..ad703e2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: version: 7.8.1 devDependencies: esbuild: - specifier: ^0.19.11 - version: 0.19.12 + specifier: ^0.21.3 + version: 0.21.3 client/browser: dependencies: @@ -252,15 +252,9 @@ importers: '@openctx/client': specifier: workspace:* version: link:../../lib/client - '@openctx/ui-common': - specifier: workspace:* - version: link:../../lib/ui-common '@openctx/vscode-lib': specifier: workspace:* version: link:../vscode-lib - esbuild-wasm: - specifier: ^0.19.11 - version: 0.19.12 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -293,8 +287,8 @@ importers: specifier: ^8.2.2 version: 8.2.2 esbuild: - specifier: ^0.19.11 - version: 0.19.12 + specifier: ^0.21.3 + version: 0.21.3 glob: specifier: ^10.3.10 version: 10.3.10 @@ -555,8 +549,8 @@ importers: version: link:../../lib/provider devDependencies: esbuild: - specifier: ^0.19.11 - version: 0.19.12 + specifier: ^0.21.3 + version: 0.21.3 web: dependencies: @@ -2622,6 +2616,15 @@ packages: dev: true optional: true + /@esbuild/aix-ppc64@0.21.3: + resolution: {integrity: sha512-yTgnwQpFVYfvvo4SvRFB0SwrW8YjOxEoT7wfMT7Ol5v7v5LDNvSGo67aExmxOb87nQNeWPVvaGBNfQ7BXcrZ9w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.18.20: resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -2648,6 +2651,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.21.3: + resolution: {integrity: sha512-c+ty9necz3zB1Y+d/N+mC6KVVkGUUOcm4ZmT5i/Fk5arOaY3i6CA3P5wo/7+XzV8cb4GrI/Zjp8NuOQ9Lfsosw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.18.20: resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -2674,6 +2686,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.21.3: + resolution: {integrity: sha512-bviJOLMgurLJtF1/mAoJLxDZDL6oU5/ztMHnJQRejbJrSc9FFu0QoUoFhvi6qSKJEw9y5oGyvr9fuDtzJ30rNQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.18.20: resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -2700,6 +2721,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.21.3: + resolution: {integrity: sha512-JReHfYCRK3FVX4Ra+y5EBH1b9e16TV2OxrPAvzMsGeES0X2Ndm9ImQRI4Ket757vhc5XBOuGperw63upesclRw==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.18.20: resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -2726,6 +2756,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.21.3: + resolution: {integrity: sha512-U3fuQ0xNiAkXOmQ6w5dKpEvXQRSpHOnbw7gEfHCRXPeTKW9sBzVck6C5Yneb8LfJm0l6le4NQfkNPnWMSlTFUQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.18.20: resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -2752,6 +2791,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.21.3: + resolution: {integrity: sha512-3m1CEB7F07s19wmaMNI2KANLcnaqryJxO1fXHUV5j1rWn+wMxdUYoPyO2TnAbfRZdi7ADRwJClmOwgT13qlP3Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.18.20: resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -2778,6 +2826,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.21.3: + resolution: {integrity: sha512-fsNAAl5pU6wmKHq91cHWQT0Fz0vtyE1JauMzKotrwqIKAswwP5cpHUCxZNSTuA/JlqtScq20/5KZ+TxQdovU/g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.18.20: resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -2804,6 +2861,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.21.3: + resolution: {integrity: sha512-tci+UJ4zP5EGF4rp8XlZIdq1q1a/1h9XuronfxTMCNBslpCtmk97Q/5qqy1Mu4zIc0yswN/yP/BLX+NTUC1bXA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.18.20: resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -2830,6 +2896,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.21.3: + resolution: {integrity: sha512-vvG6R5g5ieB4eCJBQevyDMb31LMHthLpXTc2IGkFnPWS/GzIFDnaYFp558O+XybTmYrVjxnryru7QRleJvmZ6Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.18.20: resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -2856,6 +2931,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.21.3: + resolution: {integrity: sha512-f6kz2QpSuyHHg01cDawj0vkyMwuIvN62UAguQfnNVzbge2uWLhA7TCXOn83DT0ZvyJmBI943MItgTovUob36SQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.18.20: resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -2882,6 +2966,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.21.3: + resolution: {integrity: sha512-HjCWhH7K96Na+66TacDLJmOI9R8iDWDDiqe17C7znGvvE4sW1ECt9ly0AJ3dJH62jHyVqW9xpxZEU1jKdt+29A==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.18.20: resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -2908,6 +3001,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.21.3: + resolution: {integrity: sha512-BGpimEccmHBZRcAhdlRIxMp7x9PyJxUtj7apL2IuoG9VxvU/l/v1z015nFs7Si7tXUwEsvjc1rOJdZCn4QTU+Q==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.18.20: resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -2934,6 +3036,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.21.3: + resolution: {integrity: sha512-5rMOWkp7FQGtAH3QJddP4w3s47iT20hwftqdm7b+loe95o8JU8ro3qZbhgMRy0VuFU0DizymF1pBKkn3YHWtsw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.18.20: resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -2960,6 +3071,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.21.3: + resolution: {integrity: sha512-h0zj1ldel89V5sjPLo5H1SyMzp4VrgN1tPkN29TmjvO1/r0MuMRwJxL8QY05SmfsZRs6TF0c/IDH3u7XYYmbAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.18.20: resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -2986,6 +3106,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.21.3: + resolution: {integrity: sha512-dkAKcTsTJ+CRX6bnO17qDJbLoW37npd5gSNtSzjYQr0svghLJYGYB0NF1SNcU1vDcjXLYS5pO4qOW4YbFama4A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.18.20: resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -3012,6 +3141,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.21.3: + resolution: {integrity: sha512-vnD1YUkovEdnZWEuMmy2X2JmzsHQqPpZElXx6dxENcIwTu+Cu5ERax6+Ke1QsE814Zf3c6rxCfwQdCTQ7tPuXA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.18.20: resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -3038,6 +3176,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.21.3: + resolution: {integrity: sha512-IOXOIm9WaK7plL2gMhsWJd+l2bfrhfilv0uPTptoRoSb2p09RghhQQp9YY6ZJhk/kqmeRt6siRdMSLLwzuT0KQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.18.20: resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -3064,6 +3211,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.21.3: + resolution: {integrity: sha512-uTgCwsvQ5+vCQnqM//EfDSuomo2LhdWhFPS8VL8xKf+PKTCrcT/2kPPoWMTs22aB63MLdGMJiE3f1PHvCDmUOw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.18.20: resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -3090,6 +3246,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.21.3: + resolution: {integrity: sha512-vNAkR17Ub2MgEud2Wag/OE4HTSI6zlb291UYzHez/psiKarp0J8PKGDnAhMBcHFoOHMXHfExzmjMojJNbAStrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.18.20: resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -3116,6 +3281,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.21.3: + resolution: {integrity: sha512-W8H9jlGiSBomkgmouaRoTXo49j4w4Kfbl6I1bIdO/vT0+0u4f20ko3ELzV3hPI6XV6JNBVX+8BC+ajHkvffIJA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.18.20: resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -3142,6 +3316,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.21.3: + resolution: {integrity: sha512-EjEomwyLSCg8Ag3LDILIqYCZAq/y3diJ04PnqGRgq8/4O3VNlXyMd54j/saShaN4h5o5mivOjAzmU6C3X4v0xw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.18.20: resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -3168,6 +3351,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.21.3: + resolution: {integrity: sha512-WGiE/GgbsEwR33++5rzjiYsKyHywE8QSZPF7Rfx9EBfK3Qn3xyR6IjyCr5Uk38Kg8fG4/2phN7sXp4NPWd3fcw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.18.20: resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -3194,6 +3386,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.21.3: + resolution: {integrity: sha512-xRxC0jaJWDLYvcUvjQmHCJSfMrgmUuvsoXgDeU/wTorQ1ngDdUBuFtgY3W1Pc5sprGAvZBtWdJX7RPg/iZZUqA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@fal-works/esbuild-plugin-global-externals@2.1.2: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: true @@ -7861,12 +8062,6 @@ packages: - supports-color dev: true - /esbuild-wasm@0.19.12: - resolution: {integrity: sha512-Zmc4hk6FibJZBcTx5/8K/4jT3/oG1vkGTEeKJUQFCUQKimD6Q7+adp/bdVQyYJFolMKaXkQnVZdV4O5ZaTYmyQ==} - engines: {node: '>=12'} - hasBin: true - dev: false - /esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -7958,6 +8153,37 @@ packages: '@esbuild/win32-x64': 0.20.2 dev: true + /esbuild@0.21.3: + resolution: {integrity: sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.3 + '@esbuild/android-arm': 0.21.3 + '@esbuild/android-arm64': 0.21.3 + '@esbuild/android-x64': 0.21.3 + '@esbuild/darwin-arm64': 0.21.3 + '@esbuild/darwin-x64': 0.21.3 + '@esbuild/freebsd-arm64': 0.21.3 + '@esbuild/freebsd-x64': 0.21.3 + '@esbuild/linux-arm': 0.21.3 + '@esbuild/linux-arm64': 0.21.3 + '@esbuild/linux-ia32': 0.21.3 + '@esbuild/linux-loong64': 0.21.3 + '@esbuild/linux-mips64el': 0.21.3 + '@esbuild/linux-ppc64': 0.21.3 + '@esbuild/linux-riscv64': 0.21.3 + '@esbuild/linux-s390x': 0.21.3 + '@esbuild/linux-x64': 0.21.3 + '@esbuild/netbsd-x64': 0.21.3 + '@esbuild/openbsd-x64': 0.21.3 + '@esbuild/sunos-x64': 0.21.3 + '@esbuild/win32-arm64': 0.21.3 + '@esbuild/win32-ia32': 0.21.3 + '@esbuild/win32-x64': 0.21.3 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -13480,7 +13706,7 @@ packages: peerDependencies: monaco-editor: '>=0.33.0' dependencies: - esbuild: 0.19.12 + esbuild: 0.21.3 monaco-editor: 0.45.0 dev: true diff --git a/provider/url-fetcher/package.json b/provider/url-fetcher/package.json index 952baacd..815030c1 100644 --- a/provider/url-fetcher/package.json +++ b/provider/url-fetcher/package.json @@ -11,12 +11,7 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", - "files": [ - "dist", - "index.ts", - "!**/*.test.*", - "README.md" - ], + "files": ["dist", "index.ts", "!**/*.test.*", "README.md"], "sideEffects": false, "scripts": { "build": "tsc --build", @@ -27,6 +22,6 @@ "@openctx/provider": "workspace:*" }, "devDependencies": { - "esbuild": "^0.19.11" + "esbuild": "^0.21.3" } }