diff --git a/patches/birpc@0.2.17.patch b/patches/birpc@0.2.17.patch index 65ff5ee2..e35b22c9 100644 --- a/patches/birpc@0.2.17.patch +++ b/patches/birpc@0.2.17.patch @@ -1,13 +1,275 @@ +diff --git a/dist/index.cjs b/dist/index.cjs +index d8119336758abcb11f5e247dfe579b1b5c5fefd5..00e4b1a01cfa480a874f5da6f8dcdd4db2b89e99 100644 +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -11,18 +11,24 @@ function createBirpc(functions, options) { + const { + post, + on, ++ off = () => { ++ }, + eventNames = [], + serialize = defaultSerialize, + deserialize = defaultDeserialize, + resolver, ++ bind = "rpc", + timeout = DEFAULT_TIMEOUT + } = options; + const rpcPromiseMap = /* @__PURE__ */ new Map(); + let _promise; ++ let closed = false; + const rpc = new Proxy({}, { + get(_, method) { + if (method === "$functions") + return functions; ++ if (method === "$close") ++ return close; + if (method === "then" && !eventNames.includes("then") && !("then" in functions)) + return void 0; + const sendEvent = (...args) => { +@@ -33,7 +39,15 @@ function createBirpc(functions, options) { + return sendEvent; + } + const sendCall = async (...args) => { +- await _promise; ++ if (closed) ++ throw new Error(`[birpc] rpc is closed, cannot call "${method}"`); ++ if (_promise) { ++ try { ++ await _promise; ++ } finally { ++ _promise = void 0; ++ } ++ } + return new Promise((resolve, reject) => { + const id = nanoid(); + let timeoutId; +@@ -58,7 +72,15 @@ function createBirpc(functions, options) { + return sendCall; + } + }); +- _promise = on(async (data, ...extra) => { ++ function close() { ++ closed = true; ++ rpcPromiseMap.forEach(({ reject }) => { ++ reject(new Error("[birpc] rpc is closed")); ++ }); ++ rpcPromiseMap.clear(); ++ off(onMessage); ++ } ++ async function onMessage(data, ...extra) { + const msg = deserialize(data); + if (msg.t === "q") { + const { m: method, a: args } = msg; +@@ -68,7 +90,7 @@ function createBirpc(functions, options) { + error = new Error(`[birpc] function "${method}" not found`); + } else { + try { +- result = await fn.apply(rpc, args); ++ result = await fn.apply(bind === "rpc" ? rpc : functions, args); + } catch (e) { + error = e; + } +@@ -90,7 +112,8 @@ function createBirpc(functions, options) { + } + rpcPromiseMap.delete(ack); + } +- }); ++ } ++ _promise = on(onMessage); + return rpc; + } + const cacheMap = /* @__PURE__ */ new WeakMap(); +diff --git a/dist/index.d.cts b/dist/index.d.cts +index 010f33e6343281a894b6835d89b7fcf1157250a7..f7618f7f8118d24723693415dc321adfa5881458 100644 +--- a/dist/index.d.cts ++++ b/dist/index.d.cts +@@ -11,6 +11,10 @@ interface ChannelOptions { + * Listener to receive raw message + */ + on: (fn: (data: any, ...extras: any[]) => void) => any | Promise; ++ /** ++ * Clear the listener when `$close` is called ++ */ ++ off?: (fn: (data: any, ...extras: any[]) => void) => any | Promise; + /** + * Custom function to serialize data + * +@@ -23,6 +27,10 @@ interface ChannelOptions { + * by default it passes the data as-is + */ + deserialize?: (data: any) => any; ++ /** ++ * Call the methods with the RPC context or the original functions object ++ */ ++ bind?: 'rpc' | 'functions'; + } + interface EventOptions { + /** +@@ -71,6 +79,7 @@ type BirpcReturn> = { + [K in keyof RemoteFunctions]: BirpcFn; + } & { + $functions: LocalFunctions; ++ $close: () => void; + }; + type BirpcGroupReturn = { + [K in keyof RemoteFunctions]: BirpcGroupFn; +diff --git a/dist/index.d.mts b/dist/index.d.mts +index 010f33e6343281a894b6835d89b7fcf1157250a7..f7618f7f8118d24723693415dc321adfa5881458 100644 +--- a/dist/index.d.mts ++++ b/dist/index.d.mts +@@ -11,6 +11,10 @@ interface ChannelOptions { + * Listener to receive raw message + */ + on: (fn: (data: any, ...extras: any[]) => void) => any | Promise; ++ /** ++ * Clear the listener when `$close` is called ++ */ ++ off?: (fn: (data: any, ...extras: any[]) => void) => any | Promise; + /** + * Custom function to serialize data + * +@@ -23,6 +27,10 @@ interface ChannelOptions { + * by default it passes the data as-is + */ + deserialize?: (data: any) => any; ++ /** ++ * Call the methods with the RPC context or the original functions object ++ */ ++ bind?: 'rpc' | 'functions'; + } + interface EventOptions { + /** +@@ -71,6 +79,7 @@ type BirpcReturn> = { + [K in keyof RemoteFunctions]: BirpcFn; + } & { + $functions: LocalFunctions; ++ $close: () => void; + }; + type BirpcGroupReturn = { + [K in keyof RemoteFunctions]: BirpcGroupFn; +diff --git a/dist/index.d.ts b/dist/index.d.ts +index 010f33e6343281a894b6835d89b7fcf1157250a7..f7618f7f8118d24723693415dc321adfa5881458 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -11,6 +11,10 @@ interface ChannelOptions { + * Listener to receive raw message + */ + on: (fn: (data: any, ...extras: any[]) => void) => any | Promise; ++ /** ++ * Clear the listener when `$close` is called ++ */ ++ off?: (fn: (data: any, ...extras: any[]) => void) => any | Promise; + /** + * Custom function to serialize data + * +@@ -23,6 +27,10 @@ interface ChannelOptions { + * by default it passes the data as-is + */ + deserialize?: (data: any) => any; ++ /** ++ * Call the methods with the RPC context or the original functions object ++ */ ++ bind?: 'rpc' | 'functions'; + } + interface EventOptions { + /** +@@ -71,6 +79,7 @@ type BirpcReturn> = { + [K in keyof RemoteFunctions]: BirpcFn; + } & { + $functions: LocalFunctions; ++ $close: () => void; + }; + type BirpcGroupReturn = { + [K in keyof RemoteFunctions]: BirpcGroupFn; diff --git a/dist/index.mjs b/dist/index.mjs -index 8396fdbfbd7e1df8935c0806af9e7b31f8ccc261..7fcc87a89d7ca21cbf3a3e97ddedec0c51a7ef2a 100644 +index 8396fdbfbd7e1df8935c0806af9e7b31f8ccc261..4544cebb4378902b8e08686ed882689d7102f0f5 100644 --- a/dist/index.mjs +++ b/dist/index.mjs -@@ -66,7 +66,7 @@ function createBirpc(functions, options) { +@@ -9,18 +9,24 @@ function createBirpc(functions, options) { + const { + post, + on, ++ off = () => { ++ }, + eventNames = [], + serialize = defaultSerialize, + deserialize = defaultDeserialize, + resolver, ++ bind = "rpc", + timeout = DEFAULT_TIMEOUT + } = options; + const rpcPromiseMap = /* @__PURE__ */ new Map(); + let _promise; ++ let closed = false; + const rpc = new Proxy({}, { + get(_, method) { + if (method === "$functions") + return functions; ++ if (method === "$close") ++ return close; + if (method === "then" && !eventNames.includes("then") && !("then" in functions)) + return void 0; + const sendEvent = (...args) => { +@@ -31,7 +37,15 @@ function createBirpc(functions, options) { + return sendEvent; + } + const sendCall = async (...args) => { +- await _promise; ++ if (closed) ++ throw new Error(`[birpc] rpc is closed, cannot call "${method}"`); ++ if (_promise) { ++ try { ++ await _promise; ++ } finally { ++ _promise = void 0; ++ } ++ } + return new Promise((resolve, reject) => { + const id = nanoid(); + let timeoutId; +@@ -48,7 +62,7 @@ function createBirpc(functions, options) { + if (typeof timeoutId === "object") + timeoutId = timeoutId.unref?.(); + } +- rpcPromiseMap.set(id, { resolve, reject, timeoutId }); ++ rpcPromiseMap.set(id, { resolve, reject, timeoutId, method }); + post(serialize({ m: method, a: args, i: id, t: "q" })); + }); + }; +@@ -56,7 +70,15 @@ function createBirpc(functions, options) { + return sendCall; + } + }); +- _promise = on(async (data, ...extra) => { ++ function close() { ++ closed = true; ++ rpcPromiseMap.forEach(({ reject, method }) => { ++ reject(new Error(`[birpc] rpc is closed, cannot call "${method}"`)); ++ }); ++ rpcPromiseMap.clear(); ++ off(onMessage); ++ } ++ async function onMessage(data, ...extra) { + const msg = deserialize(data); + if (msg.t === "q") { + const { m: method, a: args } = msg; +@@ -66,7 +88,7 @@ function createBirpc(functions, options) { error = new Error(`[birpc] function "${method}" not found`); } else { try { - result = await fn.apply(rpc, args); -+ result = await fn.apply(functions, args); ++ result = await fn.apply(bind === "rpc" ? rpc : functions, args); } catch (e) { error = e; } +@@ -88,7 +110,8 @@ function createBirpc(functions, options) { + } + rpcPromiseMap.delete(ack); + } +- }); ++ } ++ _promise = on(onMessage); + return rpc; + } + const cacheMap = /* @__PURE__ */ new WeakMap(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 555f560b..e3ff6371 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: birpc@0.2.17: - hash: 76ascauag252sgpxpwh3k26eya + hash: l7ecerzjhm2ujumvxksgxehwau path: patches/birpc@0.2.17.patch devDependencies: @@ -66,7 +66,7 @@ devDependencies: version: 8.3.3 birpc: specifier: ^0.2.17 - version: 0.2.17(patch_hash=76ascauag252sgpxpwh3k26eya) + version: 0.2.17(patch_hash=l7ecerzjhm2ujumvxksgxehwau) bumpp: specifier: ^9.3.0 version: 9.3.0 @@ -1679,7 +1679,7 @@ packages: engines: {node: '>=8'} dev: true - /birpc@0.2.17(patch_hash=76ascauag252sgpxpwh3k26eya): + /birpc@0.2.17(patch_hash=l7ecerzjhm2ujumvxksgxehwau): resolution: {integrity: sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==} dev: true patched: true diff --git a/src/api.ts b/src/api.ts index 3f7f05a8..accf0369 100644 --- a/src/api.ts +++ b/src/api.ts @@ -72,6 +72,8 @@ export class VitestFolderAPI { private handlers: ResolvedMeta['handlers'] + public createDate = Date.now() + constructor( private pkg: VitestPackage, private meta: ResolvedMeta, @@ -115,11 +117,17 @@ export class VitestFolderAPI { return this.meta.rpc.getFiles() } - onFileCreated = createQueuedHandler((files: string[]) => { + onFileCreated = createQueuedHandler(async (files: string[]) => { + if (this.process.closed) { + return + } return this.meta.rpc.onFilesCreated(files) }) - onFileChanged = createQueuedHandler((files: string[]) => { + onFileChanged = createQueuedHandler(async (files: string[]) => { + if (this.process.closed) { + return + } return this.meta.rpc.onFilesChanged(files) }) @@ -127,7 +135,10 @@ export class VitestFolderAPI { return this._collectTests(`${projectName}\0${normalize(testFile)}`) } - private _collectTests = createQueuedHandler((testsQueue: string[]) => { + private _collectTests = createQueuedHandler(async (testsQueue: string[]) => { + if (this.process.closed) { + return + } const tests = Array.from(testsQueue).map((spec) => { const [projectName, filepath] = spec.split('\0', 2) return [projectName, filepath] as [string, string] @@ -163,6 +174,7 @@ export class VitestFolderAPI { log.error('[API]', 'Failed to close Vitest process', err) }) } + this.meta.rpc.$close() } async cancelRun() { diff --git a/src/api/rpc.ts b/src/api/rpc.ts index f4986c28..73598dd7 100644 --- a/src/api/rpc.ts +++ b/src/api/rpc.ts @@ -96,6 +96,7 @@ export function createVitestRpc(options: { events, { timeout: -1, + bind: 'functions', on(listener) { options.on(listener) }, diff --git a/src/debug/api.ts b/src/debug/api.ts index 6eaa7b7c..41474a17 100644 --- a/src/debug/api.ts +++ b/src/debug/api.ts @@ -27,7 +27,7 @@ export async function debugTests( const wsAddress = `ws://localhost:${port}` const config = getConfig(pkg.folder) - const promise = Promise.withResolvers() + const deferredPromise = Promise.withResolvers() const { runtimeArgs, runtimeExecutable } = await getRuntimeOptions( pkg.folder, @@ -73,12 +73,12 @@ export async function debugTests( log.info('[DEBUG] Debugging started') } else { - promise.reject(new Error('Failed to start debugging. See output for more information.')) + deferredPromise.reject(new Error('Failed to start debugging. See output for more information.')) log.error('[DEBUG] Debugging failed') } }, (err) => { - promise.reject(new Error('Failed to start debugging', { cause: err })) + deferredPromise.reject(new Error('Failed to start debugging', { cause: err })) log.error('[DEBUG] Start debugging failed') log.error(err.toString()) }, @@ -112,10 +112,15 @@ export async function debugTests( await runner.runTests(request, token) - promise.resolve() + deferredPromise.resolve() } - catch (err) { - promise.reject(err) + catch (err: any) { + if (err.message.startsWith('[birpc] rpc is closed')) { + deferredPromise.resolve() + return + } + + deferredPromise.reject(err) } if (!token.isCancellationRequested) { @@ -127,13 +132,13 @@ export async function debugTests( const onDidTerminate = vscode.debug.onDidTerminateDebugSession((session) => { if (session.configuration.__name !== 'Vitest') return - disposables.forEach(d => d.dispose()) + disposables.reverse().forEach(d => d.dispose()) server.close() }) disposables.push(onDidStart, onDidTerminate) - await promise.promise + await deferredPromise.promise } async function getRuntimeOptions(folder: vscode.WorkspaceFolder) { diff --git a/src/extension.ts b/src/extension.ts index 6d16f051..0e51290c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,11 +62,15 @@ class VitestExtension { return await this._defineTestProfilePromise } - private async _defineTestProfiles(showWarning: boolean, _cancelToken?: vscode.CancellationToken) { + private async _defineTestProfiles(showWarning: boolean, cancelToken?: vscode.CancellationToken) { this.testTree.reset([]) const vitest = await resolveVitestPackages(showWarning) + if (cancelToken?.isCancellationRequested) { + return + } + if (!vitest.length) { log.error('[API]', 'Failed to start Vitest: No vitest config files found') this.testController.items.delete(this.loadingTestItem.id) @@ -122,6 +126,10 @@ class VitestExtension { try { await this.api?.dispose() + if (cancelToken?.isCancellationRequested) { + return + } + this.api = await resolveVitestAPI(vitest) this.api.onUnexpectedExit((code) => { @@ -263,6 +271,7 @@ class VitestExtension { const reloadConfigNames = [ 'vitest.vitestPackagePath', 'vitest.nodeExecutable', + 'vitest.nodeExecArgs', 'vitest.workspaceConfig', 'vitest.rootConfig', 'vitest.shellType', diff --git a/src/runner/runner.ts b/src/runner/runner.ts index 46a8cef3..e4ad4c48 100644 --- a/src/runner/runner.ts +++ b/src/runner/runner.ts @@ -30,13 +30,12 @@ export class TestRunner extends vscode.Disposable { super(() => { log.verbose?.('Disposing test runner') api.clearListeners() - this.testRun?.end() - this.testRun = undefined - this.testRunDefer?.resolve() - this.testRunDefer = undefined + this.endTestRun() this.nonContinuousRequest = undefined this.continuousRequests.clear() - this.api.cancelRun() + if (!this.api.process.closed) { + this.api.cancelRun() + } this._onRequestsExhausted.dispose() }) @@ -146,7 +145,7 @@ export class TestRunner extends vscode.Disposable { } protected endTestRun() { - log.verbose?.('Ending test run', this.testRun?.name || '') + log.verbose?.('Ending test run', this.testRun ? this.testRun.name || '' : '') this.testRun?.end() this.testRunDefer?.resolve() this.testRun = undefined @@ -238,6 +237,7 @@ export class TestRunner extends vscode.Disposable { await this.testRunDefer.promise } + log.verbose?.('Initiating deferred test run') this.testRunDefer = Promise.withResolvers() const runTests = (files?: string[], testNamePatern?: string) => @@ -538,5 +538,5 @@ function formatTestOutput(output: string) { function join(items: readonly vscode.TestItem[] | undefined) { if (!items) return '' - return items.map(p => p.label).join(', ') + return items.map(p => `"${p.label}"`).join(', ') } diff --git a/src/watcher.ts b/src/watcher.ts index 1418407a..b7f0d401 100644 --- a/src/watcher.ts +++ b/src/watcher.ts @@ -1,7 +1,6 @@ import { relative } from 'node:path' import * as vscode from 'vscode' import { normalize } from 'pathe' -import mm from 'micromatch' import type { TestTree } from './testTree' import { getConfig } from './config' import type { VitestFolderAPI } from './api' @@ -10,12 +9,6 @@ import { log } from './log' export class ExtensionWatcher extends vscode.Disposable { private watcherByFolder = new Map() - private readonly ignorePattern = [ - '**/.git/**', - '**/*.git', - '**/node_modules/**', - ] - constructor(private readonly testTree: TestTree) { super(() => { this.watcherByFolder.forEach(x => x.dispose()) @@ -39,7 +32,7 @@ export class ExtensionWatcher extends vscode.Disposable { watcher.onDidChange(async (file) => { const filepath = normalize(file.fsPath) - if (await this.shouldIgnoreFile(file)) { + if (await this.shouldIgnoreFile(filepath, file)) { return } log.verbose?.('[VSCODE] File changed:', relative(api.workspaceFolder.uri.fsPath, file.fsPath)) @@ -48,7 +41,7 @@ export class ExtensionWatcher extends vscode.Disposable { watcher.onDidCreate(async (file) => { const filepath = normalize(file.fsPath) - if (await this.shouldIgnoreFile(file)) { + if (await this.shouldIgnoreFile(filepath, file)) { return } log.verbose?.('[VSCODE] File created:', relative(api.workspaceFolder.uri.fsPath, file.fsPath)) @@ -56,8 +49,12 @@ export class ExtensionWatcher extends vscode.Disposable { }) } - private async shouldIgnoreFile(file: vscode.Uri) { - if (mm.isMatch(file.fsPath, this.ignorePattern)) { + private async shouldIgnoreFile(filepath: string, file: vscode.Uri) { + if ( + filepath.includes('/node_modules/') + || filepath.includes('/.git/') + || filepath.endsWith('.git') + ) { return true } try { diff --git a/src/worker/rpc.ts b/src/worker/rpc.ts index f9303ed9..fdcc2e9f 100644 --- a/src/worker/rpc.ts +++ b/src/worker/rpc.ts @@ -6,6 +6,7 @@ import type { Vitest } from './vitest' export function createWorkerRPC(vitest: Vitest, channel: ChannelOptions) { const rpc = createBirpc(vitest, { timeout: -1, + bind: 'functions', eventNames: [ 'onConsoleLog', 'onTaskUpdate',