From d3fae8db9e79a06f653913e54bd1111129a79210 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Sat, 22 Aug 2026 08:20:12 +0000 Subject: [PATCH 01/43] test(vscode): add real multi-root integration workspace --- packages/vscode/package.json | 7 +- packages/vscode/src/__test__/helper.ts | 16 + packages/vscode/src/__test__/index.ts | 3 +- .../src/__test__/language-server/README.md | 4 + packages/vscode/src/__test__/runTest.ts | 22 +- .../vscode/src/__test__/workspace.test.ts | 27 + .../integration-workspace.code-workspace | 12 + .../integration-workspace/root-a/package.json | 8 + .../root-a/schema.prisma | 8 + .../integration-workspace/root-b/package.json | 8 + .../root-b/schema.prisma | 8 + packages/vscode/tsconfig.test.json | 4 +- pnpm-lock.yaml | 3539 ++++++++++++++++- pnpm-workspace.yaml | 1 + 14 files changed, 3610 insertions(+), 57 deletions(-) create mode 100644 packages/vscode/src/__test__/workspace.test.ts create mode 100644 packages/vscode/tests/fixtures/integration-workspace.code-workspace create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/package.json create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-b/package.json create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma diff --git a/packages/vscode/package.json b/packages/vscode/package.json index a2591eba74..34034fdf6f 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "icon": "logo_white.png", "engines": { - "vscode": "^1.96.0" + "vscode": "^1.104.0" }, "publisher": "Prisma", "categories": [ @@ -46,6 +46,7 @@ "build:types": "tsc -p ./ --emitDeclarationOnly", "watch": "node esbuild.mjs --watch", "test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true", + "test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js", "test:playwright": "playwright test", "test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright", "vscode:prepublish": "pnpm run build", @@ -686,7 +687,7 @@ "@types/mocha": "10.0.10", "@types/node": "20.14.8", "@types/sinon": "^20.0.0", - "@types/vscode": "1.96.0", + "@types/vscode": "1.104.0", "@vscode/test-electron": "2.4.1", "@vscode/vsce": "2.29.0", "esbuild": "^0.27.1", @@ -703,4 +704,4 @@ "access": "public" }, "preview": false -} \ No newline at end of file +} diff --git a/packages/vscode/src/__test__/helper.ts b/packages/vscode/src/__test__/helper.ts index 2561fd25ea..9bcd2ab290 100644 --- a/packages/vscode/src/__test__/helper.ts +++ b/packages/vscode/src/__test__/helper.ts @@ -49,6 +49,22 @@ export const getDocUri = (p: string): vscode.Uri => { return vscode.Uri.file(getDocPath(p)) } +export function getWorkspaceFolder(name: string): vscode.WorkspaceFolder { + const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => folder.name === name) + if (!workspaceFolder) { + throw new Error(`Workspace folder not found: ${name}`) + } + return workspaceFolder +} + +export function getWorkspaceDocUri(workspaceFolder: vscode.WorkspaceFolder, relativePath: string): vscode.Uri { + return vscode.Uri.joinPath(workspaceFolder.uri, relativePath) +} + +export function getPrismaCliEntrypoint(workspaceFolder: vscode.WorkspaceFolder): vscode.Uri { + return vscode.Uri.joinPath(workspaceFolder.uri, 'node_modules', 'prisma', 'dist', 'prisma.js') +} + export async function setTestContent(content: string): Promise { const all = new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length)) return editor.edit((eb) => eb.replace(all, content)) diff --git a/packages/vscode/src/__test__/index.ts b/packages/vscode/src/__test__/index.ts index c7d41e4f1d..62afc97947 100644 --- a/packages/vscode/src/__test__/index.ts +++ b/packages/vscode/src/__test__/index.ts @@ -12,7 +12,8 @@ export function run(): Promise { const testsRoot = __dirname return new Promise((resolve, reject) => { - glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + const testPattern = process.env.VSCODE_TEST_PATTERN ?? '**/**.test.js' + glob(testPattern, { cwd: testsRoot }, (err, files) => { if (err) { return reject(err) } diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index daf8957eb8..2d1375eca5 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -2,3 +2,7 @@ Only one test per feature is done here. The goal is to check that the integration is working between the VS Code extension and the Language Server. + +The integration runner opens `tests/fixtures/integration-workspace.code-workspace`, which contains two workspace roots. Each root is a pnpm workspace importer with the same lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`. + +Run the full minimum-and-latest integration suite with `pnpm test:integration`. To verify only the multi-root fixture substrate on the minimum supported VS Code runtime, run `pnpm test:integration:workspace`. diff --git a/packages/vscode/src/__test__/runTest.ts b/packages/vscode/src/__test__/runTest.ts index 5c3f215a5a..578196c5a3 100644 --- a/packages/vscode/src/__test__/runTest.ts +++ b/packages/vscode/src/__test__/runTest.ts @@ -5,7 +5,7 @@ import { runTests } from '@vscode/test-electron' // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const packageJson = require('../../package.json') as { engines: { vscode: string } } -function test(version?: string) { +function test(version?: string, testPattern?: string) { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` const extensionDevelopmentPath = path.resolve(__dirname, '../../') @@ -14,12 +14,17 @@ function test(version?: string) { // Passed to --extensionTestsPath const extensionTestsPath = path.resolve(__dirname, './index') + // The explicit multi-root workspace opened by every integration test run. + const workspacePath = path.resolve(__dirname, '../../tests/fixtures/integration-workspace.code-workspace') + // Downloads VS Code, unzip it and run the integration test return runTests({ version, // optional, default = latest extensionDevelopmentPath, extensionTestsPath, + extensionTestsEnv: testPattern ? { VSCODE_TEST_PATTERN: testPattern } : undefined, launchArgs: [ + workspacePath, // This disables all extensions except the one being testing '--disable-extensions', // ? This may or may not be necessary? @@ -41,15 +46,24 @@ function test(version?: string) { async function main(): Promise { try { + const minimumOnly = process.argv.includes('--minimum-only') + const testPatternFlag = process.argv.indexOf('--test-pattern') + const testPattern = testPatternFlag === -1 ? undefined : process.argv[testPatternFlag + 1] + if (testPatternFlag !== -1 && !testPattern) { + throw new Error('--test-pattern requires a glob pattern') + } + // 1 - Run on our minimum supported version from package.json // eslint-disable-next-line const minimumSupportedVersion: string = packageJson.engines.vscode.replace('~', '').replace('^', '') // remove semver chars console.log(`*** Testing on minimum supported version of VS Code: ${minimumSupportedVersion} ***`) - await test(minimumSupportedVersion) + await test(minimumSupportedVersion, testPattern) // 2 - Run again on latest version - console.log(`*** Testing on latest version of VS Code ***`) - await test() + if (!minimumOnly) { + console.log(`*** Testing on latest version of VS Code ***`) + await test(undefined, testPattern) + } } catch (err) { const errMsg = err instanceof Error ? ` ${err.message}` : '' diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts new file mode 100644 index 0000000000..79aa5d58f9 --- /dev/null +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -0,0 +1,27 @@ +import assert from 'node:assert' +import { stat } from 'node:fs/promises' +import vscode from 'vscode' +import { getPrismaCliEntrypoint, getWorkspaceDocUri, getWorkspaceFolder } from './helper' + +suite('Multi-root integration workspace', () => { + test('resolves documents and real Prisma CLI entrypoints per workspace root', async () => { + const rootA = getWorkspaceFolder('integration-root-a') + const rootB = getWorkspaceFolder('integration-root-b') + + const documentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'schema.prisma')) + const documentB = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootB, 'schema.prisma')) + + assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentA.uri), rootA) + assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentB.uri), rootB) + assert.notStrictEqual(rootA.uri.toString(), rootB.uri.toString()) + + for (const workspaceFolder of [rootA, rootB]) { + const entrypoint = getPrismaCliEntrypoint(workspaceFolder) + assert.strictEqual( + (await stat(entrypoint.fsPath)).isFile(), + true, + `Missing Prisma CLI entrypoint: ${entrypoint.fsPath}`, + ) + } + }) +}) diff --git a/packages/vscode/tests/fixtures/integration-workspace.code-workspace b/packages/vscode/tests/fixtures/integration-workspace.code-workspace new file mode 100644 index 0000000000..5a08df1d6d --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "name": "integration-root-a", + "path": "integration-workspace/root-a", + }, + { + "name": "integration-root-b", + "path": "integration-workspace/root-b", + }, + ], +} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json new file mode 100644 index 0000000000..c16769f044 --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json @@ -0,0 +1,8 @@ +{ + "name": "prisma-vscode-integration-root-a", + "version": "1.0.0", + "private": true, + "devDependencies": { + "prisma": "8.0.0-rc.7" + } +} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma new file mode 100644 index 0000000000..f8d6d7e53b --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma @@ -0,0 +1,8 @@ +datasource db { + provider = "sqlite" + url = "file:./root-a.db" +} + +model RootARecord { + id Int @id +} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-b/package.json b/packages/vscode/tests/fixtures/integration-workspace/root-b/package.json new file mode 100644 index 0000000000..aa71609e45 --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "prisma-vscode-integration-root-b", + "version": "1.0.0", + "private": true, + "devDependencies": { + "prisma": "8.0.0-rc.7" + } +} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma new file mode 100644 index 0000000000..7292af1bef --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma @@ -0,0 +1,8 @@ +datasource db { + provider = "sqlite" + url = "file:./root-b.db" +} + +model RootBRecord { + id Int @id +} diff --git a/packages/vscode/tsconfig.test.json b/packages/vscode/tsconfig.test.json index 4110cf178b..1a08316e00 100644 --- a/packages/vscode/tsconfig.test.json +++ b/packages/vscode/tsconfig.test.json @@ -4,8 +4,8 @@ "outDir": "dist-tests", "composite": false, "declaration": false, - "module": "CommonJS", - "moduleResolution": "bundler", + "module": "Node16", + "moduleResolution": "Node16", "skipLibCheck": true }, "include": ["src/**/*"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbd9427f61..772154516b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: dependencies: '@prisma/config': specifier: 7.9.0-dev.4 - version: 7.9.0-dev.4(magicast@0.3.5) + version: 7.9.0-dev.4(magicast@0.5.4) '@prisma/prisma-schema-wasm': specifier: 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a version: 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a @@ -113,7 +113,7 @@ importers: version: 18.19.76 '@vitest/coverage-v8': specifier: 3.0.6 - version: 3.0.6(vitest@3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1)) + version: 3.0.6(vitest@3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0)) ts-dedent: specifier: 2.2.0 version: 2.2.0 @@ -122,7 +122,7 @@ importers: version: 5.7.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1) + version: 3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) packages/vscode: dependencies: @@ -206,8 +206,8 @@ importers: specifier: ^20.0.0 version: 20.0.0 '@types/vscode': - specifier: 1.96.0 - version: 1.96.0 + specifier: 1.104.0 + version: 1.104.0 '@vscode/test-electron': specifier: 2.4.1 version: 2.4.1 @@ -239,6 +239,18 @@ importers: specifier: 5.7.3 version: 5.7.3 + packages/vscode/tests/fixtures/integration-workspace/root-a: + devDependencies: + prisma: + specifier: 8.0.0-rc.7 + version: 8.0.0-rc.7(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + + packages/vscode/tests/fixtures/integration-workspace/root-b: + devDependencies: + prisma: + specifier: 8.0.0-rc.7 + version: 8.0.0-rc.7(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + packages: '@actions/core@1.11.1': @@ -256,10 +268,98 @@ packages: '@actions/io@1.1.3': resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} + '@alcalzone/ansi-tokenize@0.2.5': + resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} + engines: {node: '>=18'} + + '@alchemy.run/node-utils@0.0.5': + resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@ark/schema@0.56.2': + resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} + + '@ark/util@0.56.2': + resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.972.69': + resolution: {integrity: sha512-vpsh9VWmQVC/nsdzf72F2yUMBOFT1aq+hoLseYV03zjVXVHkjqSNJskFRKPFxc3MRFrHNbSSmOwsbYE15u9ecw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-providers@3.1116.0': + resolution: {integrity: sha512-y41rRJ1AWtcJka2YdFQ1BfTf0CZDXizh0VOZLwfUzxI2xhG7n88XXn0N0yvLwI73/tjtXalVy94/N5QCO1lgbg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@azu/format-text@1.0.2': resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==} @@ -318,19 +418,36 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.28.5': resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/types@7.28.5': resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -350,6 +467,64 @@ packages: '@chevrotain/utils@10.5.0': resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@clack/core@1.4.0': + resolution: {integrity: sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw==} + engines: {node: '>= 20.12.0'} + + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.5.0': + resolution: {integrity: sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260704.1': + resolution: {integrity: sha512-XO+vvdhhTNZSsIWCkZ+JaE/JrFUmQAB0H0y/sVkAf32xa2TYBRvFUMwjSqf6WxtlxcKPQvmbLfpO/UQ0l/q4eQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260704.1': + resolution: {integrity: sha512-6iI7nbOOO8PzEQ6UZVBZB/hv95m8jl0yvyjMuWrF5cJbiLb5zPw3KnpqvGi+aeOs2ZmUkc81i1FWKfXwLXzPRA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260704.1': + resolution: {integrity: sha512-3mT0YHtxT7eLjghu3hKSJDUQoz+AYv8FM43nLPYhM0YiHOxr8OlmvnCb2Lp7v/U3bwd3Gnx7WEqjSppR583mGg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260704.1': + resolution: {integrity: sha512-Opo7cPTPg4x0WwK+eZSDszCyFLKQkOGNCWxF005HRTJ5SJH8h0K9KyrC1k4LBwx3haXSVi+E1eGqo1gZ1Hmhkg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260704.1': + resolution: {integrity: sha512-a97Ecnzhy04x3U052VKPCNp863F7Wf00WY7Ga5P0SbtYvaO1PDzylsHrvFMPKeiDFcOwqiredawmJ+v6bhIgeQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@5.20260822.1': + resolution: {integrity: sha512-z922wN0pEWwYaAcYdncSf11fTPfj2j1Q2n2LCLbtBBhpkIu/aC8fotol5q4ek4isWgTKfWzUx389Dsa+lhF6yw==} + '@commitlint/cli@19.6.1': resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==} engines: {node: '>=v18'} @@ -419,20 +594,123 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} + '@distilled.cloud/aws@0.30.3': + resolution: {integrity: sha512-6U/wO+fLNnqBlRnqFpF79edS5t6njDl/6UmnCVUfWpIQ4n23X0VY1ft0lzsaBa506xRtF4vRhMELR9FIp5DKUA==} + peerDependencies: + effect: '>=4.0.0-beta.100 || >=4.0.0' + + '@distilled.cloud/axiom@0.30.3': + resolution: {integrity: sha512-U4YvXsvz/TDYfIbZNRVAv8Yx1mnbms/rBQ/RHnRQWhL0JzxZOZOQvToxkB4kh/oa8KT+QbjTkDxRJP/f6P0M1g==} + peerDependencies: + effect: '>=4.0.0-beta.100 || >=4.0.0' + + '@distilled.cloud/cloudflare-rolldown-plugin@0.15.0': + resolution: {integrity: sha512-cBVl4Ck4Prf9/dPSvyQeGW+2CDLIKs+xbyUm/MgNOSyYDZYOLbsCnDePO4YwdvaxsT0oOZZIkSDDki2ve9DCHA==} + peerDependencies: + rolldown: ^1.1.5 + vite: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + rolldown: + optional: true + vite: + optional: true + + '@distilled.cloud/cloudflare-runtime@0.15.0': + resolution: {integrity: sha512-0xx+LCiBzNwmMPN1SnnD4GixVl8WZET3zaMPU51LYRU+VQQrxtetDjcjnI5q83UComjBmbS0c0Bp4j/7hgobyg==} + peerDependencies: + '@distilled.cloud/cloudflare': ^0.29.0 + '@effect/platform-bun': '>=4.0.0-beta.100 || >=4.0.0' + '@effect/platform-node': '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-beta.100 || >=4.0.0' + peerDependenciesMeta: + '@effect/platform-bun': + optional: true + '@effect/platform-node': + optional: true + + '@distilled.cloud/cloudflare-vite-plugin@0.15.0': + resolution: {integrity: sha512-+JYCTv/1Bqk3GRSb6e+UW61Pn2DFF68EAVvAbTYg5Z6tlztc2E7sd/pC4E2fsnAxtSMjwpFdrzlPqwQUq2HUQA==} + peerDependencies: + '@distilled.cloud/cloudflare': ^0.29.0 + '@distilled.cloud/cloudflare-runtime': 0.15.0 + '@effect/platform-bun': '>=4.0.0-beta.100 || >=4.0.0' + '@effect/platform-node': '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-beta.100 || >=4.0.0' + vite: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@effect/platform-bun': + optional: true + '@effect/platform-node': + optional: true + + '@distilled.cloud/cloudflare@0.30.3': + resolution: {integrity: sha512-IwQyIZrfzRJ7Dn9Plsk5pMlr50CT72fMnD5YCFy31MKAthh906ewcO3NuJOKHGA8AF9u6SKTlebsQx4Jnj8uwA==} + peerDependencies: + effect: '>=4.0.0-beta.100 || >=4.0.0' + + '@distilled.cloud/core@0.30.3': + resolution: {integrity: sha512-RupX597cPmceEiOY6csihFbzH2WhDkfbwGCMk+Izx8+f16u+aLm4mgyrYoxj1/dzIHsyIfw8sFKgNPzAg0Rx2Q==} + peerDependencies: + effect: '>=4.0.0-beta.100 || >=4.0.0' + + '@distilled.cloud/neon@0.30.3': + resolution: {integrity: sha512-4iW6lNvrJ/BuraKQ572bTQPNNtK/pFMqALoKjgozXR5jAZXbohRrD8/3QHLZW9cOAPenaHugwk4YIm0/+u4j5Q==} + peerDependencies: + effect: '>=4.0.0-beta.100 || >=4.0.0' + + '@distilled.cloud/planetscale@0.30.3': + resolution: {integrity: sha512-0ZcPqoXKl5uhJ6Ytw9UpgHa2t2pjwsuQFZn4OlnjZl0F/lToO/8TdXp4iKyw7Wog5tKTkfRYrQXVzRbaPKP3QQ==} + peerDependencies: + effect: '>=4.0.0-beta.100 || >=4.0.0' + + '@effect/sql-d1@4.0.0-rc.111': + resolution: {integrity: sha512-hjIoVS59gAvP1DjB+R5hwL9n/UbS1598/qcT1Hp3Tn3ksuJUOPTXfsCCiAQT3Ueecfkbiy32fxrCbzD0Z1YJLA==} + peerDependencies: + effect: ^4.0.0-rc.111 + + '@effect/vitest@4.0.0-rc.111': + resolution: {integrity: sha512-YDaEVT+grREBVMzykRNFtJwGxy02achzT0WXZYwMJA4ukzuB9krkgQ5roc3N6zhC3NQq/j4IyM32/Pcov7AhHw==} + peerDependencies: + effect: ^4.0.0-rc.111 + vitest: '>=4.1.0 <5.0.0' + '@electric-sql/pglite-socket@0.0.19': resolution: {integrity: sha512-9Uq0aXJyfeQMQCk9OiX3dfjdKCKurjGPkhHemDo+sA5QeYPuH48Bxfue4sfA+dBXZGe9cUCjwrnZ351E3wPPIA==} hasBin: true peerDependencies: '@electric-sql/pglite': 0.3.14 + '@electric-sql/pglite-socket@0.0.20': + resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-tools@0.2.19': resolution: {integrity: sha512-Ls4ZcSymnFRlEHtDyO3k9qPXLg7awfRAE3YnXk4WLsint17JBsU4UEX8le9YE8SgPkWNnQC898SqbFGGU/5JUA==} peerDependencies: '@electric-sql/pglite': 0.3.14 + '@electric-sql/pglite-tools@0.2.20': + resolution: {integrity: sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==} + peerDependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite@0.3.14': resolution: {integrity: sha512-3DB258dhqdsArOI1fIt7cb9RpUOgcDg5hXWVgVHAeqVQ/qxtFy605QKs4gx6mFq3jWsSPqDN8TgSEsqC3OfV9Q==} + '@electric-sql/pglite@0.3.15': + resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -451,6 +729,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -469,6 +753,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -487,6 +777,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -505,6 +801,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -523,6 +825,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -541,6 +849,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -559,6 +873,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -577,6 +897,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -595,6 +921,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -613,6 +945,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -631,6 +969,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -649,6 +993,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -667,6 +1017,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -685,6 +1041,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -703,6 +1065,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -721,6 +1089,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -739,6 +1113,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -751,6 +1131,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -769,6 +1155,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -781,6 +1173,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -799,6 +1197,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -811,6 +1215,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -829,6 +1239,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -847,6 +1263,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -865,6 +1287,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -883,6 +1311,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -917,6 +1351,12 @@ packages: peerDependencies: hono: ^4 + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -942,6 +1382,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -959,30 +1403,154 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mrleebo/prisma-ast@0.13.1': - resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} - engines: {node: '>=16'} + '@libsql/client@0.17.4': + resolution: {integrity: sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@libsql/core@0.17.4': + resolution: {integrity: sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==} - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@libsql/darwin-arm64@0.5.29': + resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} + cpu: [arm64] + os: [darwin] - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@libsql/darwin-x64@0.5.29': + resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==} + cpu: [x64] + os: [darwin] - '@octokit/auth-token@4.0.0': - resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} - engines: {node: '>= 18'} + '@libsql/hrana-client@0.10.0': + resolution: {integrity: sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==} - '@octokit/core@5.2.2': - resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm-gnueabihf@0.5.29': + resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==} + cpu: [arm] + os: [linux] + + '@libsql/linux-arm-musleabihf@0.5.29': + resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==} + cpu: [arm] + os: [linux] + + '@libsql/linux-arm64-gnu@0.5.29': + resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.5.29': + resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.5.29': + resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.5.29': + resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.5.29': + resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==} + cpu: [x64] + os: [win32] + + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + + '@mongodb-js/saslprep@1.5.0': + resolution: {integrity: sha512-Hk1SKJCMcCos38+vqDnZzlIo4XRj9yCGzYkjB4LcqpeXRIYfia1UWTz+VrueLxoU+uSRJzgkufxoRZg8gi52YA==} + + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} + engines: {node: '>= 20.19.0'} + + '@nodable/entities@3.0.0': + resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@octokit/auth-token@4.0.0': + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/core@5.2.2': + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} engines: {node: '>= 18'} + '@octokit/core@7.0.7': + resolution: {integrity: sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.4': + resolution: {integrity: sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA==} + engines: {node: '>= 20'} + '@octokit/endpoint@9.0.6': resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} engines: {node: '>= 18'} @@ -991,38 +1559,98 @@ packages: resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} engines: {node: '>= 18'} + '@octokit/graphql@9.0.4': + resolution: {integrity: sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==} + engines: {node: '>= 20'} + '@octokit/openapi-types@20.0.0': resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} '@octokit/openapi-types@24.2.0': resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + '@octokit/openapi-types@27.0.0': + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + + '@octokit/openapi-types@28.0.0': + resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==} + + '@octokit/openapi-webhooks-types@12.1.0': + resolution: {integrity: sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==} + + '@octokit/plugin-paginate-rest@14.0.0': + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-paginate-rest@9.2.2': resolution: {integrity: sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '5' + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-rest-endpoint-methods@10.4.1': resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '5' + '@octokit/plugin-rest-endpoint-methods@17.0.0': + resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/request-error@5.1.1': resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} engines: {node: '>= 18'} + '@octokit/request-error@7.1.1': + resolution: {integrity: sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.15': + resolution: {integrity: sha512-3CBg9aJ0hO9Pjyij8LbK/xYtEaPws9SW7xKz67daPNxQB1q5Y9OMA7DDOG0A6Hwf9ygGu3tvzusg0LXQ8/wAjA==} + engines: {node: '>= 20'} + '@octokit/request@8.4.1': resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} engines: {node: '>= 18'} + '@octokit/rest@22.0.1': + resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} + engines: {node: '>= 20'} + '@octokit/types@12.6.0': resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} '@octokit/types@13.10.0': resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + '@octokit/types@16.0.0': + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + + '@octokit/types@17.0.0': + resolution: {integrity: sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==} + + '@octokit/webhooks-methods@6.0.0': + resolution: {integrity: sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==} + engines: {node: '>= 20'} + + '@octokit/webhooks@14.2.0': + resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==} + engines: {node: '>= 20'} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1036,6 +1664,27 @@ packages: engines: {node: '>=18'} hasBin: true + '@prisma/cli-engine@0.2.0': + resolution: {integrity: sha512-nd0sP3l7W79ASoONP9DV8LKVcADmtPm0KMwKJ9rVJL3BbSJdN6HLO2pHFugI4Tp1NdiwcwlJuK5rCKxJPu7kFw==} + engines: {node: '>=22.12.0'} + + '@prisma/composer-cli@0.11.0': + resolution: {integrity: sha512-qhibvb5ARF6JmvsUEpr3A0J2Kjx4whPRZP3LCOULk9+1/Cp2IWyHsLhDq+I6n0nmwrdvvySAWwW71ugSey24kA==} + engines: {node: '>=22.18.0'} + hasBin: true + peerDependencies: + '@prisma/cli-engine': 0.2.0 + + '@prisma/composer@0.11.0': + resolution: {integrity: sha512-NP4Ds9qrHQdtitj2pBRdUkuHs6Crtx+uCHHKyUfAaIeKW3b0vRrOibJUhaYXZ/dE9YrbYRmtpddLO0ZTL1h1yA==} + engines: {node: '>=22.18.0'} + + '@prisma/compute-sdk@0.39.0': + resolution: {integrity: sha512-Ir4yuCiqyv7XjhqsqolKZjXzzMCFiZJ4vvk1jl0dn6MjZx1j6puojD+1BLbCE8ty0NakaOyhWmXWyO63YeUf/Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@prisma/management-api-sdk': ^1.44.0 + '@prisma/config@6.19.0-dev.10': resolution: {integrity: sha512-y3jJxFpPw8shOW+h1bpKIbEMrVOLOupcgzAxI+ixMOLzHH7041BYrotftRvAz/edQhEAeG2nAVDOyKS45o+H8A==} @@ -1045,15 +1694,27 @@ packages: '@prisma/credentials-store@7.1.0': resolution: {integrity: sha512-hQ5XKET/AHCeWuBISD9Y0d93Ja3ep6bpCRNox283IGV+IBUBjwVArb8Q5QwQWK1dniCLRynkE+fEMIfL3c6N5w==} + '@prisma/credentials-store@7.9.1': + resolution: {integrity: sha512-WCrMfi3EGBbN9QCBPHfkBCl6ri83h5HteTRxeLeBVkUs4pn9JbxRcPinXjqnIcxKWrIx2As/Q8yWXWeWb0jAyQ==} + '@prisma/debug@7.1.0': resolution: {integrity: sha512-pPAckG6etgAsEBusmZiFwM9bldLSNkn++YuC4jCTJACdK5hLOVnOzX7eSL2FgaU6Gomd6wIw21snUX2dYroMZQ==} + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + '@prisma/dev@0.18.0': resolution: {integrity: sha512-bZZxOkqprTj9CGNIZ5V57GDIGATg+JSRxAYuS3iHZq8eDimzQQhaGJoTk1UszGMSKLgmTBYlptDAmRvQpuCx1w==} + '@prisma/dev@0.20.0': + resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==} + '@prisma/get-platform@7.1.0': resolution: {integrity: sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==} + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + '@prisma/language-server@6.19.0-hotfix.1': resolution: {integrity: sha512-3I5HiyEv5svyn5wZ+DJ0+Ht+mVVl58Q+YWzpCurZwi2QGDqAaD7vty4LDJ65iFWUMfQdFrQlSP0uZGOfrIngZw==} engines: {node: '>=20'} @@ -1062,6 +1723,32 @@ packages: '@prisma/management-api-sdk@1.13.0': resolution: {integrity: sha512-Ba6BBmsKLn9TBWAPfXNcyAVI5mkyP7Z/L650D9Paxz/pxuuOj4xbhgOTS4k3l/S+eNYUYUkJGYQqWTF8jCdK0w==} + '@prisma/management-api-sdk@1.55.0': + resolution: {integrity: sha512-WuDDOhxOHfROGY7QAU6wtlbWR0diNwfsQw1epQkhalvgOUe/JzIjxqpBpQzMA54zFVlMmcnT6OEiQfwCIKiRjA==} + + '@prisma/management-api-sdk@1.67.0': + resolution: {integrity: sha512-lgiCR2XD+xHXb5gIKcYmgjgEjIcM35TCBW6cAyWOLcKTlq88YfddqURaALC3TxDFo+Zp+3tXrf1Cr0aiXZgKPg==} + + '@prisma/orm-framework@8.0.0-rc.4': + resolution: {integrity: sha512-vEMX1h5UF5zOyIT5TVKeWR9c8TcghJWggaFTVp3uXuHFyRUOANPomMMocXTFt2bhvdp/Ny7KxJ3KDJXyc1wCmw==} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma/orm-toolchain@8.0.0-rc.4': + resolution: {integrity: sha512-YufxTbj0jB8f6iSCbo/KEgWwPP6Y1C2XFEg5N6YnVLHV7nP92NfR51qlUJrtvj153mltPZjKpGdAdtxvf0Besw==} + peerDependencies: + '@prisma/cli-engine': 0.2.0 + typescript: '>=5.9' + vite: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + vite: + optional: true + '@prisma/ppg@0.5.2': resolution: {integrity: sha512-WT5Kxj1gRLjcrnhMJfdTMsxs8RBhOHm4ZooCuaK0qhKP1ta1ukV3Ab11jEOSI2XeMWCRN6fvwO8+wqWzGmTXuQ==} @@ -1074,6 +1761,9 @@ packages: '@prisma/query-plan-executor@7.1.0': resolution: {integrity: sha512-NUtqMOPXZsIlnKAGGC/SZpRMZVjYMgnMC5QI0lqQ8Cjm23w89ZO7gaP9Wz78pKLFfC9pS2xa4BX6AUucCBkyVQ==} + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + '@prisma/schema-files-loader@6.19.0-dev.10': resolution: {integrity: sha512-FpjL53iN1ApEUyNbbaBJ2Yn575SMYiwplEeMpeSS4249DSf4O9B1l+RB6wLnXQ6Gk415GX4Y6+MWS3DAZfw4Ow==} @@ -1087,6 +1777,107 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.53.3': resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] @@ -1197,6 +1988,9 @@ packages: cpu: [x64] os: [win32] + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@secretlint/config-creator@10.2.2': resolution: {integrity: sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==} engines: {node: '>=20.0.0'} @@ -1246,6 +2040,10 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -1255,9 +2053,63 @@ packages: '@sinonjs/samsam@8.0.3': resolution: {integrity: sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==} + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-config-provider@4.6.2': + resolution: {integrity: sha512-zMrXu/O5tPa7GLtra8L4wFG6DACcXT9QV4Ay+WEAjUhXm1dVq7c/q9Qv9gkJZNLY8hmQKg08778kDcxpKNMqOA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.7.2': + resolution: {integrity: sha512-XsIDj5gVG4YRxGS4n4TiBDAogPWRHXKZyor6JW/sEuaa/7BvKADe9j45jI2dPYCnYbKkLBmbZ4qN9ns0sH+kMQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.6.2': + resolution: {integrity: sha512-wTQX1hPfElIqY6AzM/s6c4UgpMxCL4MwJ5u6340ksLPq78lur6Y+keheIDk5gMX4G3KFh4MERcDt6xhRq+ie1Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stricli/core@1.3.0': + resolution: {integrity: sha512-LnBe2HntygaLDU5trtjiC3J4C/YkmIZuM0XB52IF4qaLqJH09kgD1fnZiS2gaYUnt0nAN2a+1b4PSOBcfPrN4Q==} + '@textlint/ast-node-types@15.4.1': resolution: {integrity: sha512-XifMpBMdo0E1Fuh85YdcYAgy+okNg9WKBzIPIO4JUDnSWUVFihnogrM4cjDapeHkgzSgulwR8oJVJ17eyxI1bA==} @@ -1273,6 +2125,12 @@ packages: '@textlint/types@15.4.1': resolution: {integrity: sha512-WByVZ3zblbvuI+voWQplUP7seSTKXI9z6TMVXEB3dY3JFrZCIXWKNfLbETX5lZV7fYkCMaDtILO1l6s11wdbQA==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aws-lambda@8.10.162': + resolution: {integrity: sha512-Fn658grtLOci1oxi1391vvDWJRKNGWRSqfxRkmN/Iy3c0tQH1USMKEXcPYHLvope+ZgTFocx9FRQJx1muBL6qw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1324,8 +2182,17 @@ packages: '@types/sinonjs__fake-timers@15.0.1': resolution: {integrity: sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==} - '@types/vscode@1.96.0': - resolution: {integrity: sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==} + '@types/vscode@1.104.0': + resolution: {integrity: sha512-0KwoU2rZ2ecsTGFxo4K1+f+AErRsYW0fsp6A0zufzGuhyczc2IoKqYqcwXidKXmy2u8YB2GsYsOtiI9Izx3Tig==} + + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} '@typescript-eslint/eslint-plugin@7.18.0': resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} @@ -1392,6 +2259,15 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vercel/detect-agent@1.2.5': + resolution: {integrity: sha512-0krENrjuitlW8s6TJu0MlqCevyCU7K7JK63jZAf7xZ6n17tx+vUEwzHT3sTxawtwZxaW21hu+oFUpoOrm49FsQ==} + engines: {node: '>=14'} + + '@vercel/nft@1.11.0': + resolution: {integrity: sha512-m1QFg+U+3yPOnP1xSYJ73UIRxLOXdts1JOhiOiyPYqEsALgrXFFINvgUaD6R6iNvaBFAjHllBCbkfx4FuOdpaA==} + engines: {node: '>=20'} + hasBin: true + '@vitest/coverage-v8@2.1.9': resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} peerDependencies: @@ -1534,9 +2410,18 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + aborter@1.1.0: resolution: {integrity: sha512-9rHWMcWTEYsMB4l+ttgPujR7OiXH9NQbP0ej+SSVaK1e2yU/tePbYm8g/g9cQhJkgczp6lpEB2fdJYLKT/T0mg==} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1557,6 +2442,37 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alchemy@2.0.0-beta.67: + resolution: {integrity: sha512-kFEKEXtRdf781lRzGyYinPRVgrMKJFs5MNQTxF2Y5RIxyA2qCsK0NOOBBoOzYmGOln8e3CIYF/oEASFQjffXJA==} + hasBin: true + peerDependencies: + '@aws/durable-execution-sdk-js': ^2.1.0 + '@effect/platform-bun': '>=4.0.0-beta.100 || >=4.0.0' + '@effect/platform-node': '>=4.0.0-beta.100 || >=4.0.0' + '@effect/sql-pg': '>=4.0.0-beta.100 || >=4.0.0' + drizzle-kit: 1.0.0-rc.4 + drizzle-orm: 1.0.0-rc.4 + effect: '>=4.0.0-beta.100 || >=4.0.0' + vite: ^8.0.7 + ws: ^8.20.0 + peerDependenciesMeta: + '@aws/durable-execution-sdk-js': + optional: true + '@effect/platform-bun': + optional: true + '@effect/platform-node': + optional: true + '@effect/sql-pg': + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + vite: + optional: true + ws: + optional: true + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1565,6 +2481,10 @@ packages: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1589,12 +2509,21 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + are-shallow-equal@1.1.1: resolution: {integrity: sha512-Y0MC/7IP+WZSo0NgYDwww7euKssEodUJxjby3fmNurEDcbq8htqSgyI7a7HELJzkzNv26dOH5vKQFlzCt1H9Ag==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arkregex@0.0.8: + resolution: {integrity: sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==} + + arktype@2.2.3: + resolution: {integrity: sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==} + array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} @@ -1610,6 +2539,9 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1617,18 +2549,84 @@ packages: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.0: + resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.3: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.2: + resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + + better-result@2.10.0: + resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1637,6 +2635,9 @@ packages: resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==} engines: {node: '>=4'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1649,12 +2650,19 @@ packages: boundary@2.0.0: resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1662,6 +2670,10 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -1714,6 +2726,9 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + capnweb@0.6.1: + resolution: {integrity: sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1766,6 +2781,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -1777,9 +2796,17 @@ packages: resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1796,6 +2823,15 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + clipanion@4.0.0-rc.4: + resolution: {integrity: sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==} + peerDependencies: + typanion: '*' + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -1803,10 +2839,17 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + closest-match@1.3.3: + resolution: {integrity: sha512-RSdHrZwNOvt2uMQgqJDJdM/I+5MlJ1tQJEXYrbRjSMXWiCRo06g2hwObJ7+WKt2J9ySK9/pJ0Q2vbL+BPkofDA==} + cockatiel@3.2.1: resolution: {integrity: sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==} engines: {node: '>=16'} + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1851,6 +2894,10 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-type@3.0.0: + resolution: {integrity: sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw==} + engines: {node: '>=22'} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -1864,6 +2911,10 @@ packages: engines: {node: '>=16'} hasBin: true + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1965,6 +3016,10 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2010,6 +3065,10 @@ packages: resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2030,6 +3089,9 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + effect@4.0.0-beta.103: + resolution: {integrity: sha512-pE8TxF4m2tQzVI+77dIlm3s+81TACV1AiX1JEkvY+zVuxgQQ8aGSkqXNJF6b/ST+coCSk5cUdbULpQ7sm4oHyw==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -2090,6 +3152,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2105,6 +3170,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2113,6 +3183,10 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2167,6 +3241,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2177,6 +3254,9 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2185,6 +3265,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -2200,12 +3284,19 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2216,9 +3307,25 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fast-xml-builder@1.3.1: + resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} + + fast-xml-parser@5.11.0: + resolution: {integrity: sha512-9IGxMqvqLOnqP+Egi1nqDHKv5k8aZ7r9n558enxcucmyVGEBNPAU+MOg/8jPIS7rO7sSq4gFm1/nHtiaubMruw==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -2234,14 +3341,24 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-my-way-ts@0.1.6: + resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2305,6 +3422,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2313,6 +3433,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2336,6 +3460,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -2369,6 +3497,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -2438,6 +3570,10 @@ packages: resolution: {integrity: sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==} engines: {node: '>=16.9.0'} + hono@4.11.4: + resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + engines: {node: '>=16.9.0'} + hono@4.7.10: resolution: {integrity: sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==} engines: {node: '>=16.9.0'} @@ -2475,6 +3611,10 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -2484,6 +3624,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2509,6 +3653,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + index-to-position@1.2.0: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} @@ -2527,6 +3675,23 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@7.0.0: + resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + ink@6.8.0: + resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} + engines: {node: '>=20'} + peerDependencies: + '@types/react': '>=19.0.0' + react: '>=19.0.0' + react-devtools-core: '>=6.1.2' + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -2567,6 +3732,15 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -2592,10 +3766,17 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-primitive@3.0.1: resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} engines: {node: '>=0.10.0'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2604,6 +3785,10 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} @@ -2616,6 +3801,13 @@ packages: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-unsafe@2.0.2: + resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -2657,6 +3849,13 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-base64@3.9.3: + resolution: {integrity: sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g==} + js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -2686,6 +3885,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-with-bigint@3.5.12: + resolution: {integrity: sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2724,6 +3926,9 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2732,6 +3937,17 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libsodium-wrappers@0.8.4: + resolution: {integrity: sha512-mu8aAWucZjTB5O/BtGXtW4e1agy7uHxNYG7zPthmmD1jU43LCDmSWZLN4JhflbdPXj3yDO4lxM1O9hLDgIOXDw==} + + libsodium@0.8.4: + resolution: {integrity: sha512-lMcYaRi0zcs7tarATsQUYC7rstliIXZuoq0c6zXSgNtSNtdvBgkSegjWhpMJAXzKX3SUSwIp7+zEsob+j3LuRw==} + + libsql@0.5.29: + resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==} + cpu: [x64, arm64, wasm32, arm] + os: [darwin, linux, win32] + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -2832,6 +4048,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -2846,12 +4065,19 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -2874,6 +4100,9 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -2922,6 +4151,10 @@ packages: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2944,6 +4177,14 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -2952,12 +4193,62 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.21.0: + resolution: {integrity: sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.3.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} + hasBin: true + + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} + + multipasta@0.2.8: + resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mysql2@3.23.4: + resolution: {integrity: sha512-J1Rgl8Oy5iw3mOBjKeTMQ3cJNjMYtJYavQVXShsMtSj9rKV8Q1+QaGKNJqDVQFcNINqYYp6Vxd90r69cCoBxBA==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2988,10 +4279,23 @@ packages: encoding: optional: true + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-sarif-builder@3.3.1: resolution: {integrity: sha512-8z5dAbhpxmk/WRQHXlv4V0h+9Y4Ugk+w08lyhV/7E/CQX9yDdBc3025/EG+RSMJU2aPFh/IQ7XDV7Ti5TLt/TA==} engines: {node: '>=20'} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} @@ -3008,6 +4312,10 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -3042,6 +4350,10 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + open@11.0.1: + resolution: {integrity: sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==} + engines: {node: '>=20'} + openapi-fetch@0.14.0: resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==} @@ -3092,6 +4404,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -3107,6 +4422,10 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-semver@1.1.1: resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} @@ -3119,6 +4438,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3127,6 +4450,10 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3147,6 +4474,10 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3174,6 +4505,40 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3185,6 +4550,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -3217,10 +4586,34 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + postgres@3.4.7: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + powershell-utils@0.2.0: + resolution: {integrity: sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==} + engines: {node: '>=20'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -3239,12 +4632,29 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prisma@8.0.0-rc.7: + resolution: {integrity: sha512-fIdFG8puEM+NVqGbyUtS+rGGCmm8ODday1CGZNgqxQdzoFDm9Tmj4HqetfAZOLKac2YLD6R/8IyAtChTKgZ7mw==} + engines: {node: '>=22.18.0'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} promise-concurrency-limiter@1.0.0: resolution: {integrity: sha512-OI96yL5DUck9KCLee5H6DnRfVsHIstQspXk8xsYrWr9ur9IlFuzKvoU70HwQb99MqHg2mpdkuGa92NuoXue3cw==} + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -3262,6 +4672,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -3290,6 +4703,12 @@ packages: peerDependencies: react: ^19.2.1 + react-reconciler@0.33.0: + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.0 + react@19.2.1: resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} engines: {node: '>=0.10.0'} @@ -3327,6 +4746,9 @@ packages: remeda@2.32.0: resolution: {integrity: sha512-BZx9DsT4FAgXDTOdgJIc5eY6ECIXMwtlSPQoPglF20ycSWigttDDe88AozEsPPT4OWk5NujroGSBC1phw5uU+w==} + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3374,6 +4796,11 @@ packages: ripstat@1.1.1: resolution: {integrity: sha512-O+KrJUwY3Q8cArNraH136svsDlNmRh6mnJ9TogkpcGWBvd2Kks5d5HGsZRnWt9h3kh8D4uq62kdlYihONjgj5w==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3464,6 +4891,9 @@ packages: sinon@21.0.0: resolution: {integrity: sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3484,10 +4914,17 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -3504,6 +4941,14 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sql-escaper@1.5.1: + resolution: {integrity: sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3514,6 +4959,9 @@ packages: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3537,6 +4985,10 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -3551,6 +5003,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -3559,6 +5015,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -3570,6 +5030,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.4.2: + resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} + structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} @@ -3597,6 +5060,10 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -3604,14 +5071,31 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terminal-link@4.0.0: resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} engines: {node: '>=18'} + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -3626,6 +5110,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-readdir@1.5.0: resolution: {integrity: sha512-Nep9qu34bOZApNkEnJu4V1WcgxW1kCGlYN8SYwMzZfqpv6f2E1n5vPHLczJqy2vOQ1rQG/m9fI3DQbIFXAQNGw==} engines: {node: '>= 10.12.0'} @@ -3672,9 +5159,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toml@4.3.0: + resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} + engines: {node: '>=20'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -3729,6 +5224,9 @@ packages: resolution: {integrity: sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA==} hasBin: true + typanion@3.14.0: + resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3749,6 +5247,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} @@ -3777,6 +5279,9 @@ packages: resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -3785,9 +5290,16 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + uniku@0.5.0: + resolution: {integrity: sha512-giSrg7xqM5YWkSlyheulHgTTInhYh/m0cFZOOuChi/TO87hKlxmZLllETlvDw/lPB54NIs1iW/x2rr0y2yFXHg==} + engines: {node: '>=20.19.0'} + universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3801,8 +5313,13 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: @@ -3966,6 +5483,10 @@ packages: resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==} engines: {node: '>=14.0.0'} + vscode-jsonrpc@9.0.1: + resolution: {integrity: sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==} + engines: {node: '>=14.0.0'} + vscode-languageclient@7.0.0: resolution: {integrity: sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==} engines: {vscode: ^1.52.0} @@ -3976,15 +5497,28 @@ packages: vscode-languageserver-protocol@3.17.3: resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==} + vscode-languageserver-protocol@3.18.2: + resolution: {integrity: sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==} + vscode-languageserver-textdocument@1.0.11: resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + vscode-languageserver-types@3.16.0: resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} vscode-languageserver-types@3.17.3: resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==} + vscode-languageserver-types@3.18.0: + resolution: {integrity: sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==} + + vscode-languageserver@10.1.0: + resolution: {integrity: sha512-9gEWpXkYGXoqG7pBnE8O8hx/yP7+Aabn4+peQ3KDicQv6qunHSWyLTud3OF0w4S2+HfDD+5HqYKiXQW9HAU6mA==} + hasBin: true + vscode-languageserver@8.1.0: resolution: {integrity: sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==} hasBin: true @@ -3999,6 +5533,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -4007,6 +5545,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4020,13 +5562,26 @@ packages: engines: {node: '>=8'} hasBin: true - word-wrap@1.2.5: + widest-line@6.0.0: + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} + engines: {node: '>=20'} + + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerd@1.20260704.1: + resolution: {integrity: sha512-GDZ0jzIYDYfN7rCt/oFJv4BG3QJ+4IS2kfRvxEMY9VKIfPhzo63PH69Ir7ug8LfORCgCtmfkiQVXrqbot7pZTQ==} + engines: {node: '>=16'} + hasBin: true + workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + wrap-ansi@10.0.1: + resolution: {integrity: sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q==} + engines: {node: '>=20'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4042,10 +5597,26 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + wsl-utils@1.0.0: + resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==} + engines: {node: '>=20'} + xdg-app-paths@8.3.0: resolution: {integrity: sha512-mgxlWVZw0TNWHoGmXq+NC3uhCIc55dDpAlDkMQUaIAcQzysb0kxctwv//fvuW61/nAAeUBJMQ8mnZjMmuYwOcQ==} engines: {node: '>= 4.0'} @@ -4054,6 +5625,10 @@ packages: resolution: {integrity: sha512-xrcqhWDvtZ7WLmt8G4f3hHy37iK7D2idtosRgkeiSPZEPmBShp0VfmRBLWAPC6zLF48APJ21yfea+RfQMF4/Aw==} engines: {node: '>= 4.0'} + xml-naming@0.3.0: + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} + engines: {node: '>=16.0.0'} + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -4062,6 +5637,10 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4069,11 +5648,20 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.6.1: resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -4112,6 +5700,13 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} @@ -4143,11 +5738,197 @@ snapshots: '@actions/io@1.1.3': {} + '@alcalzone/ansi-tokenize@0.2.5': + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + '@alchemy.run/node-utils@0.0.5': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@ark/schema@0.56.2': + dependencies: + '@ark/util': 0.56.2 + + '@ark/util@0.56.2': {} + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.5 + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.9': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-cognito-identity@3.972.69': + dependencies: + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.77': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-cognito-identity': 3.972.69 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.44': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@azu/format-text@1.0.2': {} '@azu/style-format@1.0.1': @@ -4245,17 +6026,30 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/parser@7.28.5': dependencies: '@babel/types': 7.28.5 + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} '@bcoe/v8-coverage@1.0.2': {} @@ -4275,6 +6069,53 @@ snapshots: '@chevrotain/utils@10.5.0': {} + '@clack/core@1.4.0': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.5.0': + dependencies: + '@clack/core': 1.4.0 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260704.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260704.1 + + '@cloudflare/workerd-darwin-64@1.20260704.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260704.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260704.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260704.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260704.1': + optional: true + + '@cloudflare/workers-types@5.20260822.1': {} + '@commitlint/cli@19.6.1(@types/node@14.18.63)(typescript@5.7.3)': dependencies: '@commitlint/format': 19.8.1 @@ -4385,16 +6226,119 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@distilled.cloud/aws@0.30.3(effect@4.0.0-beta.103)': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/credential-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + '@smithy/shared-ini-file-loader': 4.7.2 + '@smithy/types': 4.17.2 + '@smithy/util-base64': 4.6.2 + aws4fetch: 1.0.20 + effect: 4.0.0-beta.103 + fast-xml-parser: 5.11.0 + + '@distilled.cloud/axiom@0.30.3(effect@4.0.0-beta.103)': + dependencies: + '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + + '@distilled.cloud/cloudflare-rolldown-plugin@0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': + dependencies: + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260704.1) + magic-string: 0.30.21 + unenv: 2.0.0-rc.24 + optionalDependencies: + rolldown: 1.1.5 + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - workerd + + '@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)': + dependencies: + '@alchemy.run/node-utils': 0.0.5 + '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + workerd: 1.20260704.1 + + '@distilled.cloud/cloudflare-vite-plugin@0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': + dependencies: + '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) + '@distilled.cloud/cloudflare-runtime': 0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - rolldown + - workerd + + '@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103)': + dependencies: + '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + + '@distilled.cloud/core@0.30.3(effect@4.0.0-beta.103)': + dependencies: + effect: 4.0.0-beta.103 + + '@distilled.cloud/neon@0.30.3(effect@4.0.0-beta.103)': + dependencies: + '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + + '@distilled.cloud/planetscale@0.30.3(effect@4.0.0-beta.103)': + dependencies: + '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + + '@effect/sql-d1@4.0.0-rc.111(effect@4.0.0-beta.103)': + dependencies: + '@cloudflare/workers-types': 5.20260822.1 + effect: 4.0.0-beta.103 + + '@effect/vitest@4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + effect: 4.0.0-beta.103 + vitest: 3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + '@electric-sql/pglite-socket@0.0.19(@electric-sql/pglite@0.3.14)': dependencies: '@electric-sql/pglite': 0.3.14 + '@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-tools@0.2.19(@electric-sql/pglite@0.3.14)': dependencies: '@electric-sql/pglite': 0.3.14 + '@electric-sql/pglite-tools@0.2.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite@0.3.14': {} + '@electric-sql/pglite@0.3.15': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -4404,6 +6348,9 @@ snapshots: '@esbuild/aix-ppc64@0.27.1': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true @@ -4413,6 +6360,9 @@ snapshots: '@esbuild/android-arm64@0.27.1': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.21.5': optional: true @@ -4422,6 +6372,9 @@ snapshots: '@esbuild/android-arm@0.27.1': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.21.5': optional: true @@ -4431,6 +6384,9 @@ snapshots: '@esbuild/android-x64@0.27.1': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true @@ -4440,6 +6396,9 @@ snapshots: '@esbuild/darwin-arm64@0.27.1': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true @@ -4449,6 +6408,9 @@ snapshots: '@esbuild/darwin-x64@0.27.1': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true @@ -4458,6 +6420,9 @@ snapshots: '@esbuild/freebsd-arm64@0.27.1': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true @@ -4467,6 +6432,9 @@ snapshots: '@esbuild/freebsd-x64@0.27.1': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true @@ -4476,6 +6444,9 @@ snapshots: '@esbuild/linux-arm64@0.27.1': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true @@ -4485,6 +6456,9 @@ snapshots: '@esbuild/linux-arm@0.27.1': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true @@ -4494,6 +6468,9 @@ snapshots: '@esbuild/linux-ia32@0.27.1': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true @@ -4503,6 +6480,9 @@ snapshots: '@esbuild/linux-loong64@0.27.1': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true @@ -4512,6 +6492,9 @@ snapshots: '@esbuild/linux-mips64el@0.27.1': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true @@ -4521,6 +6504,9 @@ snapshots: '@esbuild/linux-ppc64@0.27.1': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true @@ -4530,6 +6516,9 @@ snapshots: '@esbuild/linux-riscv64@0.27.1': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true @@ -4539,6 +6528,9 @@ snapshots: '@esbuild/linux-s390x@0.27.1': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true @@ -4548,12 +6540,18 @@ snapshots: '@esbuild/linux-x64@0.27.1': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.27.1': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true @@ -4563,12 +6561,18 @@ snapshots: '@esbuild/netbsd-x64@0.27.1': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.27.1': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true @@ -4578,12 +6582,18 @@ snapshots: '@esbuild/openbsd-x64@0.27.1': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.27.1': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true @@ -4593,6 +6603,9 @@ snapshots: '@esbuild/sunos-x64@0.27.1': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true @@ -4602,6 +6615,9 @@ snapshots: '@esbuild/win32-arm64@0.27.1': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true @@ -4611,6 +6627,9 @@ snapshots: '@esbuild/win32-ia32@0.27.1': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true @@ -4620,6 +6639,9 @@ snapshots: '@esbuild/win32-x64@0.27.1': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -4653,6 +6675,10 @@ snapshots: dependencies: hono: 4.11.1 + '@hono/node-server@1.19.9(hono@4.11.4)': + dependencies: + hono: 4.11.4 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4680,6 +6706,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@istanbuljs/schema@0.1.3': {} '@jridgewell/gen-mapping@0.3.13': @@ -4696,11 +6726,117 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@libsql/client@0.17.4': + dependencies: + '@libsql/core': 0.17.4 + '@libsql/hrana-client': 0.10.0 + js-base64: 3.9.3 + libsql: 0.5.29 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.17.4': + dependencies: + js-base64: 3.9.3 + + '@libsql/darwin-arm64@0.5.29': + optional: true + + '@libsql/darwin-x64@0.5.29': + optional: true + + '@libsql/hrana-client@0.10.0': + dependencies: + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.18.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm-gnueabihf@0.5.29': + optional: true + + '@libsql/linux-arm-musleabihf@0.5.29': + optional: true + + '@libsql/linux-arm64-gnu@0.5.29': + optional: true + + '@libsql/linux-arm64-musl@0.5.29': + optional: true + + '@libsql/linux-x64-gnu@0.5.29': + optional: true + + '@libsql/linux-x64-musl@0.5.29': + optional: true + + '@libsql/win32-x64-msvc@0.5.29': + optional: true + + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.4.2 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.6.3 + tar: 7.5.22 + transitivePeerDependencies: + - encoding + - supports-color + + '@mongodb-js/saslprep@1.5.0': + dependencies: + sparse-bitfield: 3.0.3 + '@mrleebo/prisma-ast@0.13.1': dependencies: chevrotain: 10.5.0 lilconfig: 2.1.0 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + optional: true + + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@neon-rs/load@0.0.4': {} + + '@noble/hashes@2.3.0': {} + + '@nodable/entities@3.0.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4715,6 +6851,8 @@ snapshots: '@octokit/auth-token@4.0.0': {} + '@octokit/auth-token@6.0.0': {} + '@octokit/core@5.2.2': dependencies: '@octokit/auth-token': 4.0.0 @@ -4725,6 +6863,21 @@ snapshots: before-after-hook: 2.2.3 universal-user-agent: 6.0.1 + '@octokit/core@7.0.7': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.4 + '@octokit/request': 10.0.15 + '@octokit/request-error': 7.1.1 + '@octokit/types': 17.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.4': + dependencies: + '@octokit/types': 17.0.0 + universal-user-agent: 7.0.3 + '@octokit/endpoint@9.0.6': dependencies: '@octokit/types': 13.10.0 @@ -4736,26 +6889,65 @@ snapshots: '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 + '@octokit/graphql@9.0.4': + dependencies: + '@octokit/request': 10.0.15 + '@octokit/types': 17.0.0 + universal-user-agent: 7.0.3 + '@octokit/openapi-types@20.0.0': {} '@octokit/openapi-types@24.2.0': {} + '@octokit/openapi-types@27.0.0': {} + + '@octokit/openapi-types@28.0.0': {} + + '@octokit/openapi-webhooks-types@12.1.0': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.7)': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/types': 16.0.0 + '@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2)': dependencies: '@octokit/core': 5.2.2 '@octokit/types': 12.6.0 + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.7)': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.2)': dependencies: '@octokit/core': 5.2.2 '@octokit/types': 12.6.0 + '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.7)': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/types': 16.0.0 + '@octokit/request-error@5.1.1': dependencies: '@octokit/types': 13.10.0 deprecation: 2.3.1 once: 1.4.0 + '@octokit/request-error@7.1.1': + dependencies: + '@octokit/types': 17.0.0 + + '@octokit/request@10.0.15': + dependencies: + '@octokit/endpoint': 11.0.4 + '@octokit/request-error': 7.1.1 + '@octokit/types': 17.0.0 + content-type: 3.0.0 + json-with-bigint: 3.5.12 + universal-user-agent: 7.0.3 + '@octokit/request@8.4.1': dependencies: '@octokit/endpoint': 9.0.6 @@ -4763,6 +6955,13 @@ snapshots: '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 + '@octokit/rest@22.0.1': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.7) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.7) + '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.7) + '@octokit/types@12.6.0': dependencies: '@octokit/openapi-types': 20.0.0 @@ -4771,6 +6970,24 @@ snapshots: dependencies: '@octokit/openapi-types': 24.2.0 + '@octokit/types@16.0.0': + dependencies: + '@octokit/openapi-types': 27.0.0 + + '@octokit/types@17.0.0': + dependencies: + '@octokit/openapi-types': 28.0.0 + + '@octokit/webhooks-methods@6.0.0': {} + + '@octokit/webhooks@14.2.0': + dependencies: + '@octokit/openapi-webhooks-types': 12.1.0 + '@octokit/request-error': 7.1.1 + '@octokit/webhooks-methods': 6.0.0 + + '@oxc-project/types@0.139.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -4780,6 +6997,115 @@ snapshots: dependencies: playwright: 1.57.0 + '@prisma/cli-engine@0.2.0(magicast@0.5.4)': + dependencies: + '@clack/prompts': 1.5.0 + '@prisma/management-api-sdk': 1.55.0 + '@stricli/core': 1.3.0 + c12: 3.3.4(magicast@0.5.4) + colorette: 2.0.20 + package-manager-detector: 1.8.0 + string-width: 8.2.2 + transitivePeerDependencies: + - magicast + + '@prisma/composer-cli@0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3)': + dependencies: + '@prisma/cli-engine': 0.2.0(magicast@0.5.4) + '@prisma/composer': 0.11.0(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + alchemy: 2.0.0-beta.67(@types/node@20.14.8)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + c12: 3.3.4(magicast@0.5.4) + effect: 4.0.0-beta.103 + esbuild: 0.28.2 + transitivePeerDependencies: + - '@aws/durable-execution-sdk-js' + - '@effect/platform-bun' + - '@effect/platform-node' + - '@effect/sql-pg' + - '@mongodb-js/zstd' + - '@types/node' + - '@types/react' + - bufferutil + - drizzle-kit + - drizzle-orm + - encoding + - gcp-metadata + - kerberos + - magicast + - mongodb-client-encryption + - pg-native + - react-devtools-core + - rollup + - snappy + - socks + - supports-color + - typescript + - utf-8-validate + - vite + - vitest + - workerd + - ws + + '@prisma/composer@0.11.0(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3)': + dependencies: + '@prisma/management-api-sdk': 1.67.0 + '@standard-schema/spec': 1.1.0 + alchemy: 2.0.0-beta.67(@types/node@20.14.8)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + arktype: 2.2.3 + c12: 3.3.4(magicast@0.5.4) + effect: 4.0.0-beta.103 + esbuild: 0.28.2 + transitivePeerDependencies: + - '@aws/durable-execution-sdk-js' + - '@effect/platform-bun' + - '@effect/platform-node' + - '@effect/sql-pg' + - '@mongodb-js/zstd' + - '@types/node' + - '@types/react' + - bufferutil + - drizzle-kit + - drizzle-orm + - encoding + - gcp-metadata + - kerberos + - magicast + - mongodb-client-encryption + - pg-native + - react-devtools-core + - rollup + - snappy + - socks + - supports-color + - typescript + - utf-8-validate + - vite + - vitest + - workerd + - ws + + '@prisma/compute-sdk@0.39.0(@prisma/management-api-sdk@1.55.0)(rollup@4.53.3)': + dependencies: + '@prisma/management-api-sdk': 1.55.0 + '@vercel/nft': 1.11.0(rollup@4.53.3) + better-result: 2.10.0 + jiti: 2.7.0 + magicast: 0.5.4 + picomatch: 4.0.5 + tar-stream: 3.2.0 + tiny-invariant: 1.3.3 + ws: 8.21.3 + yaml: 2.9.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - rollup + - supports-color + - utf-8-validate + '@prisma/config@6.19.0-dev.10(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) @@ -4789,9 +7115,9 @@ snapshots: transitivePeerDependencies: - magicast - '@prisma/config@7.9.0-dev.4(magicast@0.3.5)': + '@prisma/config@7.9.0-dev.4(magicast@0.5.4)': dependencies: - c12: 3.3.4(magicast@0.3.5) + c12: 3.3.4(magicast@0.5.4) deepmerge-ts: 7.1.5 effect: 3.20.0 empathic: 2.0.0 @@ -4802,8 +7128,14 @@ snapshots: dependencies: xdg-app-paths: 8.3.0 + '@prisma/credentials-store@7.9.1': + dependencies: + xdg-app-paths: 8.3.0 + '@prisma/debug@7.1.0': {} + '@prisma/debug@7.2.0': {} + '@prisma/dev@0.18.0(typescript@5.7.3)': dependencies: '@electric-sql/pglite': 0.3.14 @@ -4815,11 +7147,33 @@ snapshots: '@prisma/query-plan-executor': 7.1.0 foreground-child: 3.3.1 get-port-please: 3.2.0 - hono: 4.11.1 + hono: 4.11.1 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.32.0 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.7.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/dev@0.20.0(typescript@5.7.3)': + dependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15) + '@electric-sql/pglite-tools': 0.2.20(@electric-sql/pglite@0.3.15) + '@hono/node-server': 1.19.9(hono@4.11.4) + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.11.4 http-status-codes: 2.3.0 pathe: 2.0.3 proper-lockfile: 4.1.2 - remeda: 2.32.0 + remeda: 2.33.4 std-env: 3.10.0 valibot: 1.2.0(typescript@5.7.3) zeptomatch: 2.1.0 @@ -4830,6 +7184,10 @@ snapshots: dependencies: '@prisma/debug': 7.1.0 + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + '@prisma/language-server@6.19.0-hotfix.1(magicast@0.3.5)': dependencies: '@prisma/config': 6.19.0-dev.10(magicast@0.3.5) @@ -4848,6 +7206,51 @@ snapshots: dependencies: openapi-fetch: 0.14.0 + '@prisma/management-api-sdk@1.55.0': + dependencies: + openapi-fetch: 0.14.0 + + '@prisma/management-api-sdk@1.67.0': + dependencies: + openapi-fetch: 0.14.0 + + '@prisma/orm-framework@8.0.0-rc.4(typescript@5.7.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + pathe: 2.0.3 + uniku: 0.5.0 + optionalDependencies: + typescript: 5.7.3 + + '@prisma/orm-toolchain@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@prisma/cli-engine': 0.2.0(magicast@0.5.4) + '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.7.3) + '@vercel/detect-agent': 1.2.5 + arktype: 2.2.3 + c12: 3.3.4(magicast@0.5.4) + ci-info: 4.4.0 + clipanion: 4.0.0-rc.4(typanion@3.14.0) + closest-match: 1.3.3 + colorette: 2.0.20 + esbuild: 0.28.2 + jsonc-parser: 3.3.1 + package-manager-detector: 1.8.0 + pathe: 2.0.3 + prettier: 3.9.6 + string-width: 8.2.2 + strip-ansi: 7.2.0 + vscode-languageserver: 10.1.0 + vscode-languageserver-textdocument: 1.0.12 + wrap-ansi: 10.0.1 + optionalDependencies: + typescript: 5.7.3 + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - magicast + - typanion + '@prisma/ppg@0.5.2': {} '@prisma/prisma-schema-wasm@6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773': {} @@ -4856,6 +7259,8 @@ snapshots: '@prisma/query-plan-executor@7.1.0': {} + '@prisma/query-plan-executor@7.2.0': {} + '@prisma/schema-files-loader@6.19.0-dev.10': dependencies: '@prisma/prisma-schema-wasm': 6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773 @@ -4872,6 +7277,65 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/pluginutils@5.4.0(rollup@4.53.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.53.3 + '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -4938,6 +7402,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true + '@sec-ant/readable-stream@0.4.1': {} + '@secretlint/config-creator@10.2.2': dependencies: '@secretlint/types': 10.2.2 @@ -5014,6 +7480,8 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -5027,8 +7495,74 @@ snapshots: '@sinonjs/commons': 3.0.1 type-detect: 4.1.0 + '@smithy/core@3.33.3': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-config-provider@4.6.2': + dependencies: + '@smithy/core': 3.33.3 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/shared-ini-file-loader@4.7.2': + dependencies: + '@smithy/core': 3.33.3 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-base64@4.6.2': + dependencies: + '@smithy/core': 3.33.3 + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + + '@stricli/core@1.3.0': {} + '@textlint/ast-node-types@15.4.1': {} '@textlint/linter-formatter@15.4.1': @@ -5058,6 +7592,13 @@ snapshots: dependencies: '@textlint/ast-node-types': 15.4.1 + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aws-lambda@8.10.162': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5108,7 +7649,17 @@ snapshots: '@types/sinonjs__fake-timers@15.0.1': {} - '@types/vscode@1.96.0': {} + '@types/vscode@1.104.0': {} + + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.14.8 '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': dependencies: @@ -5201,6 +7752,27 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vercel/detect-agent@1.2.5': {} + + '@vercel/nft@1.11.0(rollup@4.53.3)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + '@rollup/pluginutils': 5.4.0(rollup@4.53.3) + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.5 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@14.18.63))': dependencies: '@ampproject/remapping': 2.3.0 @@ -5219,7 +7791,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.6(vitest@3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1))': + '@vitest/coverage-v8@3.0.6(vitest@3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -5233,7 +7805,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1) + vitest: 3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -5260,13 +7832,21 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@14.18.63) - '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1))': + '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) + + '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.6(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1) + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@2.1.9': dependencies: @@ -5440,8 +8020,14 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + abbrev@3.0.1: {} + aborter@1.1.0: {} + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5464,12 +8050,81 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alchemy@2.0.0-beta.67(@types/node@20.14.8)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3): + dependencies: + '@alchemy.run/node-utils': 0.0.5 + '@aws-sdk/credential-providers': 3.1116.0 + '@clack/prompts': 1.7.0 + '@distilled.cloud/aws': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/axiom': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) + '@distilled.cloud/cloudflare-runtime': 0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103) + '@distilled.cloud/cloudflare-vite-plugin': 0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) + '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/neon': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/planetscale': 0.30.3(effect@4.0.0-beta.103) + '@effect/sql-d1': 4.0.0-rc.111(effect@4.0.0-beta.103) + '@effect/vitest': 4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0)) + '@libsql/client': 0.17.4 + '@octokit/rest': 22.0.1 + '@octokit/webhooks': 14.2.0 + '@prisma/dev': 0.20.0(typescript@5.7.3) + '@smithy/node-config-provider': 4.6.2 + '@smithy/shared-ini-file-loader': 4.7.2 + '@smithy/types': 4.17.2 + '@types/aws-lambda': 8.10.162 + '@vercel/nft': 1.11.0(rollup@4.53.3) + aws4fetch: 1.0.20 + capnweb: 0.6.1 + effect: 4.0.0-beta.103 + fast-glob: 3.3.3 + fast-xml-parser: 5.11.0 + ink: 6.8.0(@types/react@19.2.7)(react@19.2.1) + jszip: 3.10.1 + libsodium-wrappers: 0.8.4 + mongodb: 6.21.0(@aws-sdk/credential-providers@3.1116.0) + mysql2: 3.23.4(@types/node@20.14.8) + pathe: 2.0.3 + pg: 8.23.0 + picomatch: 4.0.5 + react: 19.2.1 + rolldown: 1.1.5 + undici: 7.16.0 + yaml: 2.6.1 + optionalDependencies: + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + ws: 8.21.3 + transitivePeerDependencies: + - '@mongodb-js/zstd' + - '@types/node' + - '@types/react' + - bufferutil + - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption + - pg-native + - react-devtools-core + - rollup + - snappy + - socks + - supports-color + - typescript + - utf-8-validate + - vitest + - workerd + ansi-colors@4.1.3: {} ansi-escapes@7.2.0: dependencies: environment: 1.1.0 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -5489,12 +8144,24 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + anynum@1.0.1: {} + are-shallow-equal@1.1.1: dependencies: is-primitive: 3.0.1 argparse@2.0.1: {} + arkregex@0.0.8: + dependencies: + '@ark/util': 0.56.2 + + arktype@2.2.3: + dependencies: + '@ark/schema': 0.56.2 + '@ark/util': 0.56.2 + arkregex: 0.0.8 + array-ify@1.0.0: {} array-union@2.1.0: {} @@ -5503,27 +8170,76 @@ snapshots: astral-regex@2.0.0: {} + async-sema@3.1.1: {} + asynckit@0.4.0: {} atomically@1.7.0: {} + auto-bind@5.0.1: {} + + aws-ssl-profiles@1.1.2: {} + + aws4fetch@1.0.20: {} + azure-devops-node-api@12.5.0: dependencies: tunnel: 0.0.6 typed-rest-client: 1.8.11 + b4a@1.8.1: {} + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + + bare-events@2.9.1: {} + + bare-fs@4.8.0: + dependencies: + bare-events: 2.9.1 + bare-path: 3.1.1 + bare-stream: 2.13.3(bare-events@2.9.1) + bare-url: 2.5.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.1: {} + + bare-stream@2.13.3(bare-events@2.9.1): + dependencies: + b4a: 1.8.1 + streamx: 2.28.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.5.2: + dependencies: + bare-path: 3.1.1 + base64-js@1.5.1: {} before-after-hook@2.2.3: {} + before-after-hook@4.0.0: {} + + better-result@2.10.0: {} + binary-extensions@2.3.0: {} binaryextensions@6.11.0: dependencies: editions: 6.22.0 + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -5541,6 +8257,8 @@ snapshots: boundary@2.0.0: {} + bowser@2.14.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5550,12 +8268,18 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 browser-stdout@1.3.1: {} + bson@6.10.4: {} + buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} @@ -5592,7 +8316,7 @@ snapshots: optionalDependencies: magicast: 0.3.5 - c12@3.3.4(magicast@0.3.5): + c12@3.3.4(magicast@0.5.4): dependencies: chokidar: 5.0.0 confbox: 0.2.4 @@ -5607,7 +8331,7 @@ snapshots: pkg-types: 2.3.0 rc9: 3.0.1 optionalDependencies: - magicast: 0.3.5 + magicast: 0.5.4 cac@6.7.14: {} @@ -5625,6 +8349,8 @@ snapshots: camelcase@6.3.0: {} + capnweb@0.6.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -5716,16 +8442,22 @@ snapshots: chownr@1.1.4: optional: true + chownr@3.0.0: {} + ci-info@2.0.0: {} ci-info@3.9.0: {} ci-info@4.0.0: {} + ci-info@4.4.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 + cli-boxes@3.0.0: {} + cli-cursor@4.0.0: dependencies: restore-cursor: 4.0.0 @@ -5741,6 +8473,15 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.2 + + clipanion@4.0.0-rc.4(typanion@3.14.0): + dependencies: + typanion: 3.14.0 + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -5753,8 +8494,14 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + closest-match@1.3.3: {} + cockatiel@3.2.1: {} + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -5790,6 +8537,8 @@ snapshots: consola@3.4.2: {} + content-type@3.0.0: {} + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -5805,6 +8554,8 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + convert-to-spaces@2.0.1: {} + core-util-is@1.0.3: {} cosmiconfig-typescript-loader@6.2.0(@types/node@14.18.63)(cosmiconfig@9.0.0(typescript@5.7.3))(typescript@5.7.3): @@ -5886,8 +8637,9 @@ snapshots: destr@2.0.5: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.0.2: {} + + detect-libc@2.1.2: {} diff@5.2.0: {} @@ -5927,6 +8679,8 @@ snapshots: dotenv@17.4.1: {} + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5953,6 +8707,19 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 + effect@4.0.0-beta.103: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 4.9.0 + find-my-way-ts: 0.1.6 + ini: 7.0.0 + kubernetes-types: 1.30.0 + msgpackr: 2.0.5 + multipasta: 0.2.8 + toml: 4.3.0 + uuid: 14.0.2 + yaml: 2.9.0 + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -6002,6 +8769,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.51.0: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -6086,10 +8855,41 @@ snapshots: '@esbuild/win32-ia32': 0.27.1 '@esbuild/win32-x64': 0.27.1 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.0.1(eslint@8.57.1): @@ -6171,6 +8971,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -6179,6 +8981,12 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -6203,6 +9011,21 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.2.0 + expand-template@2.0.3: optional: true @@ -6214,10 +9037,16 @@ snapshots: dependencies: pure-rand: 6.1.0 + fast-check@4.9.0: + dependencies: + pure-rand: 8.4.2 + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6230,8 +9059,32 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fast-xml-builder@1.3.1: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.3.0 + + fast-xml-parser@5.11.0: + dependencies: + '@nodable/entities': 3.0.0 + fast-xml-builder: 1.3.1 + is-unsafe: 2.0.2 + path-expression-matcher: 1.6.2 + strnum: 2.4.2 + xml-naming: 0.3.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -6244,14 +9097,22 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-my-way-ts@0.1.6: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6313,10 +9174,16 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-caller-file@2.0.5: {} get-east-asian-width@1.4.0: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6343,6 +9210,11 @@ snapshots: get-stream@8.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + giget@2.0.0: dependencies: citty: 0.1.6 @@ -6389,6 +9261,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.1 + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -6460,6 +9338,8 @@ snapshots: hono@4.11.1: {} + hono@4.11.4: {} + hono@4.7.10: {} hosted-git-info@4.1.0: @@ -6499,12 +9379,18 @@ snapshots: human-signals@5.0.0: {} + human-signals@8.0.1: {} + husky@9.1.7: {} iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -6522,6 +9408,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@5.0.0: {} + index-to-position@1.2.0: {} inflight@1.0.6: @@ -6536,6 +9424,42 @@ snapshots: ini@4.1.1: {} + ini@7.0.0: {} + + ink@6.8.0(@types/react@19.2.7)(react@19.2.1): + dependencies: + '@alcalzone/ansi-tokenize': 0.2.5 + ansi-escapes: 7.3.0 + ansi-styles: 6.2.3 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + cli-cursor: 4.0.0 + cli-truncate: 5.2.0 + code-excerpt: 4.0.0 + es-toolkit: 1.51.0 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.1 + react-reconciler: 0.33.0(react@19.2.1) + scheduler: 0.27.0 + signal-exit: 3.0.7 + slice-ansi: 8.0.0 + stack-utils: 2.0.6 + string-width: 8.2.2 + terminal-size: 4.0.1 + type-fest: 5.8.0 + widest-line: 6.0.0 + wrap-ansi: 9.0.2 + ws: 8.21.3 + yoga-layout: 3.2.1 + optionalDependencies: + '@types/react': 19.2.7 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + is-arrayish@0.2.1: {} is-binary-path@2.1.0: @@ -6566,6 +9490,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-in-ci@2.0.0: {} + + is-in-ssh@1.0.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -6580,12 +9508,18 @@ snapshots: is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} + is-primitive@3.0.1: {} + is-property@1.0.2: {} + is-stream@2.0.1: {} is-stream@3.0.0: {} + is-stream@4.0.1: {} + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 @@ -6594,6 +9528,10 @@ snapshots: is-unicode-supported@1.3.0: {} + is-unicode-supported@2.1.0: {} + + is-unsafe@2.0.2: {} + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -6641,6 +9579,10 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + + js-base64@3.9.3: {} + js-levenshtein@1.1.6: {} js-tokens@4.0.0: {} @@ -6661,6 +9603,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-with-bigint@3.5.12: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -6716,6 +9660,8 @@ snapshots: klona@2.0.6: {} + kubernetes-types@1.30.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -6723,6 +9669,27 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libsodium-wrappers@0.8.4: + dependencies: + libsodium: 0.8.4 + + libsodium@0.8.4: {} + + libsql@0.5.29: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.5.29 + '@libsql/darwin-x64': 0.5.29 + '@libsql/linux-arm-gnueabihf': 0.5.29 + '@libsql/linux-arm-musleabihf': 0.5.29 + '@libsql/linux-arm64-gnu': 0.5.29 + '@libsql/linux-arm64-musl': 0.5.29 + '@libsql/linux-x64-gnu': 0.5.29 + '@libsql/linux-x64-musl': 0.5.29 + '@libsql/win32-x64-msvc': 0.5.29 + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -6825,6 +9792,8 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + long@5.3.2: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} @@ -6835,6 +9804,8 @@ snapshots: dependencies: yallist: 4.0.0 + lru.min@1.1.4: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6845,6 +9816,12 @@ snapshots: '@babel/types': 7.28.5 source-map-js: 1.2.1 + magicast@0.5.4: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + make-dir@4.0.0: dependencies: semver: 7.6.3 @@ -6872,6 +9849,8 @@ snapshots: mdurl@2.0.0: {} + memory-pager@1.5.0: {} + meow@12.1.1: {} merge-stream@2.0.0: {} @@ -6904,6 +9883,10 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -6924,6 +9907,12 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mkdirp-classic@0.5.3: optional: true @@ -6950,10 +9939,56 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0): + dependencies: + '@mongodb-js/saslprep': 1.5.0 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + optionalDependencies: + '@aws-sdk/credential-providers': 3.1116.0 + ms@2.1.3: {} + msgpackr-extract@3.0.4: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 + optional: true + + msgpackr@2.0.5: + optionalDependencies: + msgpackr-extract: 3.0.4 + + multipasta@0.2.8: {} + mute-stream@0.0.8: {} + mysql2@3.23.4(@types/node@20.14.8): + dependencies: + '@types/node': 20.14.8 + aws-ssl-profiles: 1.1.2 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.5.1 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + nanoid@3.3.11: {} napi-build-utils@2.0.0: @@ -6975,11 +10010,22 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-gyp-build@4.8.4: {} + node-sarif-builder@3.3.1: dependencies: '@types/sarif': 2.1.7 fs-extra: 11.3.2 + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 @@ -6996,6 +10042,11 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -7035,6 +10086,15 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + open@11.0.1: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.2.0 + wsl-utils: 1.0.0 + openapi-fetch@0.14.0: dependencies: openapi-typescript-helpers: 0.0.15 @@ -7105,6 +10165,8 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.8.0: {} + pako@1.0.11: {} parent-module@1.0.1: @@ -7124,6 +10186,8 @@ snapshots: index-to-position: 1.2.0 type-fest: 4.41.0 + parse-ms@4.0.0: {} + parse-semver@1.1.1: dependencies: semver: 5.7.2 @@ -7141,10 +10205,14 @@ snapshots: dependencies: entities: 6.0.1 + patch-console@2.0.0: {} + path-exists@4.0.0: {} path-exists@5.0.0: {} + path-expression-matcher@1.6.2: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -7161,6 +10229,11 @@ snapshots: lru-cache: 11.2.4 minipass: 7.1.2 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.4 + minipass: 7.1.3 + path-type@4.0.0: {} path-type@6.0.0: {} @@ -7177,12 +10250,49 @@ snapshots: perfect-debounce@2.1.0: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.3: {} + picomatch@4.0.5: {} + pidtree@0.6.0: {} pkg-types@2.2.0: @@ -7215,8 +10325,22 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + postgres@3.4.7: {} + powershell-utils@0.1.0: {} + + powershell-utils@0.2.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -7241,10 +10365,64 @@ snapshots: prettier@3.4.2: {} + prettier@3.9.6: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prisma@8.0.0-rc.7(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3): + dependencies: + '@prisma/cli-engine': 0.2.0(magicast@0.5.4) + '@prisma/composer-cli': 0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + '@prisma/compute-sdk': 0.39.0(@prisma/management-api-sdk@1.55.0)(rollup@4.53.3) + '@prisma/credentials-store': 7.9.1 + '@prisma/management-api-sdk': 1.55.0 + '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0)) + '@vercel/detect-agent': 1.2.5 + better-result: 2.10.0 + dotenv: 17.4.2 + execa: 9.6.1 + open: 11.0.1 + transitivePeerDependencies: + - '@aws/durable-execution-sdk-js' + - '@effect/platform-bun' + - '@effect/platform-node' + - '@effect/sql-pg' + - '@mongodb-js/zstd' + - '@types/node' + - '@types/react' + - bare-abort-controller + - bare-buffer + - bufferutil + - drizzle-kit + - drizzle-orm + - encoding + - gcp-metadata + - kerberos + - magicast + - mongodb-client-encryption + - pg-native + - react-devtools-core + - react-native-b4a + - rollup + - snappy + - socks + - supports-color + - typanion + - typescript + - utf-8-validate + - vite + - vitest + - workerd + - ws + process-nextick-args@2.0.1: {} promise-concurrency-limiter@1.0.0: {} + promise-limit@2.7.0: {} + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -7263,6 +10441,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@8.4.2: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -7305,6 +10485,11 @@ snapshots: react: 19.2.1 scheduler: 0.27.0 + react-reconciler@0.33.0(react@19.2.1): + dependencies: + react: 19.2.1 + scheduler: 0.27.0 + react@19.2.1: {} read-pkg@9.0.1: @@ -7349,6 +10534,8 @@ snapshots: dependencies: type-fest: 4.41.0 + remeda@2.33.4: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -7383,6 +10570,27 @@ snapshots: dependencies: atomically: 1.7.0 + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 @@ -7507,6 +10715,8 @@ snapshots: diff: 7.0.0 supports-color: 7.2.0 + sisteransi@1.0.5: {} + slash@3.0.0: {} slash@5.1.0: {} @@ -7527,8 +10737,17 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -7545,6 +10764,12 @@ snapshots: split2@4.2.0: {} + sql-escaper@1.5.1: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stackback@0.0.2: {} std-env@3.10.0: {} @@ -7553,6 +10778,15 @@ snapshots: dependencies: bl: 5.1.0 + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-argv@0.3.2: {} string-indexes@1.0.0: {} @@ -7581,6 +10815,11 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.1.2 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -7597,10 +10836,16 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-final-newline@2.0.0: {} strip-final-newline@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: optional: true @@ -7610,6 +10855,10 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@2.4.2: + dependencies: + anynum: 1.0.1 + structured-source@4.0.0: dependencies: boundary: 2.0.0 @@ -7644,6 +10893,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tagged-tag@1.0.0: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -7661,17 +10912,51 @@ snapshots: readable-stream: 3.6.2 optional: true + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.0 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + teex@1.0.1: + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + terminal-link@4.0.0: dependencies: ansi-escapes: 7.2.0 supports-hyperlinks: 3.2.0 + terminal-size@4.0.1: {} + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 glob: 10.5.0 minimatch: 9.0.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + text-extensions@2.4.0: {} text-table@0.2.0: {} @@ -7682,6 +10967,8 @@ snapshots: through@2.3.8: {} + tiny-invariant@1.3.3: {} + tiny-readdir@1.5.0: dependencies: promise-concurrency-limiter: 1.0.0 @@ -7713,8 +11000,14 @@ snapshots: dependencies: is-number: 7.0.0 + toml@4.3.0: {} + tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + ts-api-utils@1.4.3(typescript@5.7.3): dependencies: typescript: 5.7.3 @@ -7757,6 +11050,8 @@ snapshots: turbo-windows-64: 2.6.1 turbo-windows-arm64: 2.6.1 + typanion@3.14.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -7769,6 +11064,10 @@ snapshots: type-fest@4.41.0: {} + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + typed-rest-client@1.8.11: dependencies: qs: 6.14.0 @@ -7791,12 +11090,22 @@ snapshots: undici@7.16.0: {} + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} + uniku@0.5.0: + dependencies: + '@noble/hashes': 2.3.0 + universal-user-agent@6.0.1: {} + universal-user-agent@7.0.3: {} + universalify@2.0.1: {} uri-js@4.4.1: @@ -7807,6 +11116,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@14.0.2: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -7840,13 +11151,34 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1): + vite-node@3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.6(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1) + vite: 7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-node@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7870,7 +11202,7 @@ snapshots: '@types/node': 14.18.63 fsevents: 2.3.3 - vite@7.2.6(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1): + vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -7881,8 +11213,22 @@ snapshots: optionalDependencies: '@types/node': 18.19.76 fsevents: 2.3.3 - jiti: 2.6.1 - yaml: 2.6.1 + jiti: 2.7.0 + yaml: 2.9.0 + + vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.14.8 + fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.9.0 vitest@2.1.9(@types/node@14.18.63): dependencies: @@ -7919,11 +11265,11 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1): + vitest@3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1)) + '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -7941,8 +11287,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.6(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1) - vite-node: 3.2.4(@types/node@18.19.76)(jiti@2.6.1)(yaml@2.6.1) + vite: 7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 18.19.76 @@ -7960,10 +11306,53 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.14.8 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-jsonrpc@6.0.0: {} vscode-jsonrpc@8.1.0: {} + vscode-jsonrpc@9.0.1: {} + vscode-languageclient@7.0.0: dependencies: minimatch: 3.1.2 @@ -7980,12 +11369,25 @@ snapshots: vscode-jsonrpc: 8.1.0 vscode-languageserver-types: 3.17.3 + vscode-languageserver-protocol@3.18.2: + dependencies: + vscode-jsonrpc: 9.0.1 + vscode-languageserver-types: 3.18.0 + vscode-languageserver-textdocument@1.0.11: {} + vscode-languageserver-textdocument@1.0.12: {} + vscode-languageserver-types@3.16.0: {} vscode-languageserver-types@3.17.3: {} + vscode-languageserver-types@3.18.0: {} + + vscode-languageserver@10.1.0: + dependencies: + vscode-languageserver-protocol: 3.18.2 + vscode-languageserver@8.1.0: dependencies: vscode-languageserver-protocol: 3.17.3 @@ -8003,12 +11405,19 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 whatwg-mimetype@4.0.0: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -8023,10 +11432,27 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@6.0.0: + dependencies: + string-width: 8.2.2 + word-wrap@1.2.5: {} + workerd@1.20260704.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260704.1 + '@cloudflare/workerd-darwin-arm64': 1.20260704.1 + '@cloudflare/workerd-linux-64': 1.20260704.1 + '@cloudflare/workerd-linux-arm64': 1.20260704.1 + '@cloudflare/workerd-windows-64': 1.20260704.1 + workerpool@6.5.1: {} + wrap-ansi@10.0.1: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -8047,10 +11473,17 @@ snapshots: wrappy@1.0.2: {} + ws@8.21.3: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 + wsl-utils@1.0.0: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + xdg-app-paths@8.3.0: dependencies: xdg-portable: 10.6.0 @@ -8063,6 +11496,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + xml-naming@0.3.0: {} + xml2js@0.5.0: dependencies: sax: 1.4.3 @@ -8070,12 +11505,18 @@ snapshots: xmlbuilder@11.0.1: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@4.0.0: {} + yallist@5.0.0: {} + yaml@2.6.1: {} + yaml@2.9.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -8125,6 +11566,10 @@ snapshots: yocto-queue@1.2.2: {} + yoctocolors@2.2.0: {} + + yoga-layout@3.2.1: {} + zeptomatch@2.1.0: dependencies: grammex: 3.1.12 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18ec407efc..ed1845f440 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - 'packages/*' + - 'packages/vscode/tests/fixtures/integration-workspace/*' From 997db8854ea5db99f45f60945066b5ccf32fa67c Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 07:55:52 +0000 Subject: [PATCH 02/43] feat(vscode): add document ownership coordinator --- packages/vscode/package.json | 4 +- .../documentOwnership.test.ts | 234 ++++++++++++++++++ .../documentOwnership.ts | 177 +++++++++++++ pnpm-lock.yaml | 65 +++++ 4 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 34034fdf6f..68c927a289 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -47,6 +47,7 @@ "watch": "node esbuild.mjs --watch", "test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true", "test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js", + "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts", "test:playwright": "playwright test", "test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright", "vscode:prepublish": "pnpm run build", @@ -697,7 +698,8 @@ "ovsx": "0.10.1", "pkg-types": "2.2.0", "sinon": "^21.0.0", - "typescript": "5.7.3" + "typescript": "5.7.3", + "vitest": "^2.1.0" }, "gitHead": "7d51b157647fe1705813a30d1a77b8ccf136b8d4", "publishConfig": { diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts new file mode 100644 index 0000000000..7bc61005f3 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts @@ -0,0 +1,234 @@ +import { describe, expect, test, vi } from 'vitest' +import type { TextDocument, Uri, WorkspaceFolder } from 'vscode' +import { + DocumentOwnershipCoordinator, + type DocumentOwner, + type DocumentOwnershipCoordinatorOptions, + type DocumentOwnershipTestEvent, +} from './documentOwnership' + +const rootA = workspaceFolder('file:///workspace-a') +const rootB = workspaceFolder('file:///workspace-b') + +function uri(value: string): Uri { + const scheme = value.slice(0, value.indexOf(':')) + return { scheme, toString: () => value } as Uri +} + +function workspaceFolder(value: string): WorkspaceFolder { + return { uri: uri(value) } as WorkspaceFolder +} + +function document(value: string, text: string): TextDocument & { setText(nextText: string): void } { + let currentText = text + return { + uri: uri(value), + languageId: 'prisma', + getText: () => currentText, + setText: (nextText: string) => { + currentText = nextText + }, + } as TextDocument & { setText(nextText: string): void } +} + +function coordinator(overrides: Partial = {}): DocumentOwnershipCoordinator { + return new DocumentOwnershipCoordinator({ + workspace: { + isTrusted: true, + getWorkspaceFolder: (documentUri) => + documentUri.toString().includes('workspace-a') + ? rootA + : documentUri.toString().includes('workspace-b') + ? rootB + : undefined, + }, + policy: { isPinnedToPrisma6: () => false }, + ...overrides, + }) +} + +function deferred(): { promise: Promise; resolve(): void } { + let resolvePromise: (() => void) | undefined + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + return { + promise, + resolve: () => resolvePromise?.(), + } +} + +describe('DocumentOwnershipCoordinator', () => { + test.each([ + '// use prisma-next\nmodel User { id Int @id }', + '\n\t//use prisma-next\nmodel User { id Int @id }', + '// use prisma-next \nmodel User { id Int @id }', + ])('preserves canonical Prisma Next directive matching for %j', (text) => { + const subject = coordinator() + + expect(subject.classify(document('file:///workspace-a/schema.prisma', text))).toEqual({ + kind: 'local', + workspaceFolderUri: rootA.uri.toString(), + }) + }) + + test('keeps unsupported directive spellings with the bundled owner', () => { + const subject = coordinator() + + expect(subject.classify(document('file:///workspace-a/schema.prisma', '// use prisma next'))).toEqual({ + kind: 'bundled', + }) + }) + + test('classifies marked files independently by matching workspace folder', async () => { + const subject = coordinator() + const first = document('file:///workspace-a/first.prisma', '// use prisma-next') + const second = document('file:///workspace-b/second.prisma', 'model User { id Int @id }') + + await Promise.all([subject.synchronize(first), subject.synchronize(second)]) + + expect(subject.getOwner(first.uri)).toEqual({ kind: 'local', workspaceFolderUri: rootA.uri.toString() }) + expect(subject.getOwner(second.uri)).toEqual({ kind: 'bundled' }) + }) + + test.each([ + { name: 'untrusted workspace', value: 'file:///workspace-a/schema.prisma', trusted: false }, + { name: 'non-file document', value: 'untitled:Untitled-1', trusted: true }, + { name: 'unmatched workspace', value: 'file:///outside/schema.prisma', trusted: true }, + ])('leaves a marked document unowned in an $name', ({ value, trusted }) => { + const subject = coordinator({ + workspace: { + isTrusted: trusted, + getWorkspaceFolder: (documentUri) => (documentUri.toString().includes('workspace-a') ? rootA : undefined), + }, + }) + + expect(subject.classify(document(value, '// use prisma-next'))).toEqual({ kind: 'unowned' }) + }) + + test('lets pin policy force bundled ownership', () => { + const subject = coordinator({ policy: { isPinnedToPrisma6: () => true } }) + + expect(subject.classify(document('file:///workspace-a/schema.prisma', '// use prisma-next'))).toEqual({ + kind: 'bundled', + }) + }) + + test('serializes transitions for each document URI', async () => { + let activeTransitions = 0 + let maximumActiveTransitions = 0 + const firstGate = deferred() + const secondGate = deferred() + const gates = [firstGate, secondGate] + const subject = coordinator({ + beforeCommit: async () => { + const gate = gates.shift() + activeTransitions += 1 + maximumActiveTransitions = Math.max(maximumActiveTransitions, activeTransitions) + await gate?.promise + activeTransitions -= 1 + }, + }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + + const first = subject.synchronize(schema) + await vi.waitFor(() => expect(activeTransitions).toBe(1)) + schema.setText('model User { id Int @id }') + const second = subject.synchronize(schema) + + firstGate.resolve() + await vi.waitFor(() => expect(activeTransitions).toBe(1)) + secondGate.resolve() + await Promise.all([first, second]) + + expect(maximumActiveTransitions).toBe(1) + expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) + }) + + test('reclassifies current text after asynchronous work', async () => { + const gate = deferred() + let calls = 0 + const subject = coordinator({ + beforeCommit: async () => { + calls += 1 + if (calls === 1) { + await gate.promise + } + }, + }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + + const transition = subject.synchronize(schema) + await vi.waitFor(() => expect(calls).toBe(1)) + schema.setText('model User { id Int @id }') + gate.resolve() + + await expect(transition).resolves.toEqual({ kind: 'bundled' }) + expect(calls).toBe(2) + expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) + }) + + test('reclassifies pin policy after asynchronous work', async () => { + const gate = deferred() + let pinnedToPrisma6 = false + let calls = 0 + const subject = coordinator({ + policy: { isPinnedToPrisma6: () => pinnedToPrisma6 }, + beforeCommit: async () => { + calls += 1 + if (calls === 1) { + await gate.promise + } + }, + }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + + const transition = subject.synchronize(schema) + await vi.waitFor(() => expect(calls).toBe(1)) + pinnedToPrisma6 = true + gate.resolve() + + await expect(transition).resolves.toEqual({ kind: 'bundled' }) + expect(calls).toBe(2) + expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) + }) + + test('discards stale asynchronous work after a newer transition', async () => { + const gate = deferred() + const events: DocumentOwnershipTestEvent[] = [] + let calls = 0 + const subject = coordinator({ + beforeCommit: async () => { + calls += 1 + if (calls === 1) { + await gate.promise + } + }, + testObserver: (event) => events.push(event), + }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + + const staleTransition = subject.synchronize(schema) + await vi.waitFor(() => expect(calls).toBe(1)) + schema.setText('model User { id Int @id }') + const currentTransition = subject.synchronize(schema) + gate.resolve() + + await Promise.all([staleTransition, currentTransition]) + + expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) + expect(events).toContainEqual({ + type: 'staleTransitionDiscarded', + documentUri: schema.uri.toString(), + revision: 1, + owner: { kind: 'unowned' } satisfies DocumentOwner, + }) + expect(events.at(-1)).toEqual({ + type: 'ownerChanged', + documentUri: schema.uri.toString(), + revision: 2, + previousOwner: { kind: 'unowned' }, + owner: { kind: 'bundled' }, + }) + }) +}) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts new file mode 100644 index 0000000000..3a2b16f68d --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -0,0 +1,177 @@ +import { isPrismaNextSchema } from '@prisma/language-server/prisma-next' +import type { TextDocument, Uri, WorkspaceFolder } from 'vscode' + +export type DocumentOwner = + | { readonly kind: 'bundled' } + | { readonly kind: 'local'; readonly workspaceFolderUri: string } + | { readonly kind: 'unowned' } + +export interface DocumentOwnershipPolicy { + isPinnedToPrisma6(): boolean +} + +export interface DocumentOwnershipWorkspace { + readonly isTrusted: boolean + getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined +} + +export interface DocumentOwnershipTransition { + readonly document: TextDocument + readonly previousOwner: DocumentOwner + readonly nextOwner: DocumentOwner + readonly revision: number +} + +export type BeforeDocumentOwnerCommit = (transition: DocumentOwnershipTransition) => Promise | void + +export type DocumentOwnershipTestEvent = + | { + readonly type: 'ownerChanged' + readonly documentUri: string + readonly revision: number + readonly previousOwner: DocumentOwner + readonly owner: DocumentOwner + } + | { + readonly type: 'staleTransitionDiscarded' + readonly documentUri: string + readonly revision: number + readonly owner: DocumentOwner + } + +export interface DocumentOwnershipCoordinatorOptions { + readonly workspace: DocumentOwnershipWorkspace + readonly policy: DocumentOwnershipPolicy + readonly beforeCommit?: BeforeDocumentOwnerCommit + readonly testObserver?: (event: DocumentOwnershipTestEvent) => void +} + +interface DocumentOwnershipState { + revision: number + owner: DocumentOwner + pending: Promise +} + +const bundledOwner: DocumentOwner = { kind: 'bundled' } +const unownedOwner: DocumentOwner = { kind: 'unowned' } + +export class DocumentOwnershipCoordinator { + private readonly states = new Map() + + constructor(private readonly options: DocumentOwnershipCoordinatorOptions) {} + + classify(document: TextDocument): DocumentOwner { + if (this.options.policy.isPinnedToPrisma6()) { + return bundledOwner + } + + if (!isPrismaNextSchema(document.getText())) { + return bundledOwner + } + + if (document.uri.scheme !== 'file' || !this.options.workspace.isTrusted) { + return unownedOwner + } + + const workspaceFolder = this.options.workspace.getWorkspaceFolder(document.uri) + if (!workspaceFolder) { + return unownedOwner + } + + return { kind: 'local', workspaceFolderUri: workspaceFolder.uri.toString() } + } + + getOwner(documentUri: Uri): DocumentOwner { + return this.states.get(documentUri.toString())?.owner ?? unownedOwner + } + + synchronize(document: TextDocument): Promise { + const documentUri = document.uri.toString() + const state = this.getOrCreateState(documentUri) + const revision = ++state.revision + + const operation = state.pending.then(() => this.commitCurrentOwner(document, state, revision)) + state.pending = operation.then( + () => undefined, + () => undefined, + ) + + return operation + } + + private getOrCreateState(documentUri: string): DocumentOwnershipState { + const existing = this.states.get(documentUri) + if (existing) { + return existing + } + + const state: DocumentOwnershipState = { + revision: 0, + owner: unownedOwner, + pending: Promise.resolve(), + } + this.states.set(documentUri, state) + return state + } + + private async commitCurrentOwner( + document: TextDocument, + state: DocumentOwnershipState, + revision: number, + ): Promise { + const documentUri = document.uri.toString() + + while (revision === state.revision) { + const nextOwner = this.classify(document) + await this.options.beforeCommit?.({ + document, + previousOwner: state.owner, + nextOwner, + revision, + }) + + if (revision !== state.revision) { + this.options.testObserver?.({ + type: 'staleTransitionDiscarded', + documentUri, + revision, + owner: state.owner, + }) + return state.owner + } + + const currentOwner = this.classify(document) + if (!ownersEqual(currentOwner, nextOwner)) { + continue + } + + const previousOwner = state.owner + state.owner = currentOwner + if (!ownersEqual(previousOwner, currentOwner)) { + this.options.testObserver?.({ + type: 'ownerChanged', + documentUri, + revision, + previousOwner, + owner: currentOwner, + }) + } + return currentOwner + } + + this.options.testObserver?.({ + type: 'staleTransitionDiscarded', + documentUri, + revision, + owner: state.owner, + }) + return state.owner + } +} + +function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { + if (left.kind !== right.kind) { + return false + } + return left.kind !== 'local' || (right.kind === 'local' && left.workspaceFolderUri === right.workspaceFolderUri) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 772154516b..a53744e4bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,6 +238,9 @@ importers: typescript: specifier: 5.7.3 version: 5.7.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@20.14.8) packages/vscode/tests/fixtures/integration-workspace/root-a: devDependencies: @@ -11151,6 +11154,24 @@ snapshots: - supports-color - terser + vite-node@2.1.9(@types/node@20.14.8): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@20.14.8) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: cac: 6.7.14 @@ -11202,6 +11223,15 @@ snapshots: '@types/node': 14.18.63 fsevents: 2.3.3 + vite@5.4.21(@types/node@20.14.8): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.53.3 + optionalDependencies: + '@types/node': 20.14.8 + fsevents: 2.3.3 + vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 @@ -11265,6 +11295,41 @@ snapshots: - supports-color - terser + vitest@2.1.9(@types/node@20.14.8): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@14.18.63)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@20.14.8) + vite-node: 2.1.9(@types/node@20.14.8) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.14.8 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.2.4(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 From e21d76c26b7c155bde203ee395b61b4dbc6b812e Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 08:04:50 +0000 Subject: [PATCH 03/43] fix(vscode): guard ownership commit effects --- .../documentOwnership.test.ts | 42 +++++++++++++++++-- .../documentOwnership.ts | 22 ++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts index 7bc61005f3..97ad62e9dd 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts @@ -121,7 +121,7 @@ describe('DocumentOwnershipCoordinator', () => { const secondGate = deferred() const gates = [firstGate, secondGate] const subject = coordinator({ - beforeCommit: async () => { + prepareOwner: async () => { const gate = gates.shift() activeTransitions += 1 maximumActiveTransitions = Math.max(maximumActiveTransitions, activeTransitions) @@ -149,7 +149,7 @@ describe('DocumentOwnershipCoordinator', () => { const gate = deferred() let calls = 0 const subject = coordinator({ - beforeCommit: async () => { + prepareOwner: async () => { calls += 1 if (calls === 1) { await gate.promise @@ -174,7 +174,7 @@ describe('DocumentOwnershipCoordinator', () => { let calls = 0 const subject = coordinator({ policy: { isPinnedToPrisma6: () => pinnedToPrisma6 }, - beforeCommit: async () => { + prepareOwner: async () => { calls += 1 if (calls === 1) { await gate.promise @@ -193,12 +193,46 @@ describe('DocumentOwnershipCoordinator', () => { expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) }) + test('guards ownership side effects from superseded transitions', async () => { + const firstPreparation = deferred() + const committedOwners: DocumentOwner[] = [] + let activeCommits = 0 + let maximumActiveCommits = 0 + const subject = coordinator({ + prepareOwner: async (transition) => { + if (transition.revision === 1) { + await firstPreparation.promise + } + return async () => { + activeCommits += 1 + maximumActiveCommits = Math.max(maximumActiveCommits, activeCommits) + committedOwners.push(transition.nextOwner) + await Promise.resolve() + activeCommits -= 1 + } + }, + }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + + const supersededTransition = subject.synchronize(schema) + await Promise.resolve() + schema.setText('model User { id Int @id }') + const survivingTransition = subject.synchronize(schema) + firstPreparation.resolve() + + await Promise.all([supersededTransition, survivingTransition]) + + expect(committedOwners).toEqual([{ kind: 'bundled' }]) + expect(maximumActiveCommits).toBe(1) + expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) + }) + test('discards stale asynchronous work after a newer transition', async () => { const gate = deferred() const events: DocumentOwnershipTestEvent[] = [] let calls = 0 const subject = coordinator({ - beforeCommit: async () => { + prepareOwner: async () => { calls += 1 if (calls === 1) { await gate.promise diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index 3a2b16f68d..ca44dc3242 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -22,7 +22,11 @@ export interface DocumentOwnershipTransition { readonly revision: number } -export type BeforeDocumentOwnerCommit = (transition: DocumentOwnershipTransition) => Promise | void +export type PreparedDocumentOwnerCommit = () => Promise | void + +export type PrepareDocumentOwnerCommit = ( + transition: DocumentOwnershipTransition, +) => Promise | PreparedDocumentOwnerCommit | void export type DocumentOwnershipTestEvent = | { @@ -42,7 +46,7 @@ export type DocumentOwnershipTestEvent = export interface DocumentOwnershipCoordinatorOptions { readonly workspace: DocumentOwnershipWorkspace readonly policy: DocumentOwnershipPolicy - readonly beforeCommit?: BeforeDocumentOwnerCommit + readonly prepareOwner?: PrepareDocumentOwnerCommit readonly testObserver?: (event: DocumentOwnershipTestEvent) => void } @@ -123,7 +127,7 @@ export class DocumentOwnershipCoordinator { while (revision === state.revision) { const nextOwner = this.classify(document) - await this.options.beforeCommit?.({ + const commitOwner = await this.options.prepareOwner?.({ document, previousOwner: state.owner, nextOwner, @@ -145,6 +149,18 @@ export class DocumentOwnershipCoordinator { continue } + await commitOwner?.() + + if (revision !== state.revision) { + this.options.testObserver?.({ + type: 'staleTransitionDiscarded', + documentUri, + revision, + owner: state.owner, + }) + return state.owner + } + const previousOwner = state.owner state.owner = currentOwner if (!ownersEqual(previousOwner, currentOwner)) { From a459ee212991e06e353cc99103bd5b506a6c38a9 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 08:53:55 +0000 Subject: [PATCH 04/43] fix(vscode): align ownership after async commits --- .../documentOwnership.test.ts | 36 +++++++++++++++++++ .../documentOwnership.ts | 12 ++----- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts index 97ad62e9dd..f623c10ace 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts @@ -227,6 +227,42 @@ describe('DocumentOwnershipCoordinator', () => { expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) }) + test('records a completed commit before the queued successor transitions', async () => { + const firstCommitStarted = deferred() + const releaseFirstCommit = deferred() + const previousOwners: DocumentOwner[] = [] + let externalOwner: DocumentOwner = { kind: 'unowned' } + let activeCommits = 0 + let maximumActiveCommits = 0 + const subject = coordinator({ + prepareOwner: (transition) => async () => { + activeCommits += 1 + maximumActiveCommits = Math.max(maximumActiveCommits, activeCommits) + previousOwners.push(transition.previousOwner) + if (transition.revision === 1) { + firstCommitStarted.resolve() + await releaseFirstCommit.promise + } + externalOwner = transition.nextOwner + activeCommits -= 1 + }, + }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + + const firstTransition = subject.synchronize(schema) + await firstCommitStarted.promise + schema.setText('model User { id Int @id }') + const survivingTransition = subject.synchronize(schema) + releaseFirstCommit.resolve() + + await Promise.all([firstTransition, survivingTransition]) + + expect(previousOwners).toEqual([{ kind: 'unowned' }, { kind: 'local', workspaceFolderUri: rootA.uri.toString() }]) + expect(externalOwner).toEqual({ kind: 'bundled' }) + expect(subject.getOwner(schema.uri)).toEqual(externalOwner) + expect(maximumActiveCommits).toBe(1) + }) + test('discards stale asynchronous work after a newer transition', async () => { const gate = deferred() const events: DocumentOwnershipTestEvent[] = [] diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index ca44dc3242..67358d19a5 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -149,16 +149,8 @@ export class DocumentOwnershipCoordinator { continue } - await commitOwner?.() - - if (revision !== state.revision) { - this.options.testObserver?.({ - type: 'staleTransitionDiscarded', - documentUri, - revision, - owner: state.owner, - }) - return state.owner + if (commitOwner) { + await commitOwner() } const previousOwner = state.owner From bb24c24e78c3c585579f4907f955fbbcc2ab28dc Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 09:06:38 +0000 Subject: [PATCH 05/43] feat(vscode): gate bundled client by document owner --- packages/vscode/package.json | 2 +- .../bundledClientMiddleware.test.ts | 234 ++++++++++++++++++ .../bundledClientMiddleware.ts | 147 +++++++++++ .../plugins/prisma-language-server/index.ts | 95 ++----- 4 files changed, 402 insertions(+), 76 deletions(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 68c927a289..36243f3974 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -47,7 +47,7 @@ "watch": "node esbuild.mjs --watch", "test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true", "test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js", - "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts", + "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts", "test:playwright": "playwright test", "test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright", "vscode:prepublish": "pnpm run build", diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts new file mode 100644 index 0000000000..b54a218021 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts @@ -0,0 +1,234 @@ +import { describe, expect, test, vi } from 'vitest' +import type { + CancellationToken, + CodeAction, + CodeActionContext, + CompletionContext, + CompletionItem, + Diagnostic, + FormattingOptions, + Position, + Range, + TextDocument, + TextDocumentChangeEvent, + Uri, + WorkspaceFolder, +} from 'vscode' +import type { LanguageClient, Middleware } from 'vscode-languageclient/node' +import { createBundledClientMiddleware } from './bundledClientMiddleware' +import { DocumentOwnershipCoordinator } from './documentOwnership' + +const position = {} as Position +const range = {} as Range +const token = {} as CancellationToken +const completionContext = {} as CompletionContext +const formattingOptions = {} as FormattingOptions +const codeActionContext = { diagnostics: [] } as unknown as CodeActionContext +const root = workspaceFolder('file:///workspace') + +function uri(value: string): Uri { + return { + scheme: value.slice(0, value.indexOf(':')), + toString: () => value, + } as Uri +} + +function workspaceFolder(value: string): WorkspaceFolder { + return { uri: uri(value) } as WorkspaceFolder +} + +function document(value: string, text: string): TextDocument & { setText(nextText: string): void } { + let currentText = text + return { + uri: uri(value), + languageId: 'prisma', + getText: () => currentText, + setText: (nextText: string) => { + currentText = nextText + }, + } as TextDocument & { setText(nextText: string): void } +} + +function createSubject(options: { pinned?: boolean } = {}): { + middleware: Middleware + client: LanguageClient + documents: Map + diagnosticMessages: string[] + isSnippetEdit: ReturnType + sendRequest: ReturnType +} { + const documents = new Map() + const diagnosticMessages: string[] = [] + const isSnippetEdit = vi.fn().mockReturnValue(true) + const sendRequest = vi.fn() + const client = { + code2ProtocolConverter: { + asTextDocumentIdentifier: (textDocument: TextDocument) => ({ uri: textDocument.uri.toString() }), + asRange: () => ({ start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }), + asCodeActionContext: () => ({ diagnostics: [] }), + }, + protocol2CodeConverter: { + asCodeAction: (action: { title: string }) => ({ title: action.title, edit: { changes: {} } }), + asCommand: (command: { title: string; command: string }) => command, + }, + sendRequest, + } as unknown as LanguageClient + const ownership = new DocumentOwnershipCoordinator({ + workspace: { + isTrusted: true, + getWorkspaceFolder: (documentUri) => (documentUri.toString().startsWith(root.uri.toString()) ? root : undefined), + }, + policy: { isPinnedToPrisma6: () => options.pinned ?? false }, + }) + + return { + middleware: createBundledClientMiddleware({ + ownership, + getClient: () => client, + getDocument: (documentUri) => documents.get(documentUri.toString()), + handleDiagnosticMessage: (message) => diagnosticMessages.push(message), + isSnippetEdit, + }), + client, + documents, + diagnosticMessages, + isSnippetEdit, + sendRequest, + } +} + +describe('bundled client ownership middleware', () => { + test('does not forward marked document lifecycle notifications', () => { + const { middleware } = createSubject() + const schema = document('file:///workspace/schema.prisma', '// use prisma-next') + const next = vi.fn() + + middleware.didOpen?.(schema, next) + middleware.didChange?.({ document: schema } as unknown as TextDocumentChangeEvent, next) + middleware.didClose?.(schema, next) + + expect(next).not.toHaveBeenCalled() + }) + + test('forwards unmarked document lifecycle notifications', () => { + const { middleware } = createSubject() + const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + const didOpen = vi.fn() + const didChange = vi.fn() + const didClose = vi.fn() + const change = { document: schema } as unknown as TextDocumentChangeEvent + + middleware.didOpen?.(schema, didOpen) + middleware.didChange?.(change, didChange) + middleware.didClose?.(schema, didClose) + + expect(didOpen).toHaveBeenCalledWith(schema) + expect(didChange).toHaveBeenCalledWith(change) + expect(didClose).toHaveBeenCalledWith(schema) + }) + + test('gates every advertised document request while preserving unmarked forwarding', async () => { + const { middleware } = createSubject() + const marked = document('file:///workspace/marked.prisma', '// use prisma-next') + const unmarked = document('file:///workspace/unmarked.prisma', 'model User { id Int @id }') + + const completionItem = { label: 'id' } as CompletionItem + const completionNext = vi.fn().mockReturnValue([completionItem]) + expect( + middleware.provideCompletionItem?.(marked, position, completionContext, token, completionNext), + ).toBeUndefined() + await expect( + middleware.provideCompletionItem?.(unmarked, position, completionContext, token, completionNext), + ).resolves.toEqual([{ label: 'id' }]) + + const resolveNext = vi.fn().mockReturnValue(completionItem) + expect(middleware.resolveCompletionItem?.(completionItem, token, resolveNext)).toBe(completionItem) + unmarked.setText('// use prisma-next') + expect(middleware.resolveCompletionItem?.(completionItem, token, resolveNext)).toBeUndefined() + + const markedNext = vi.fn() + expect(middleware.provideHover?.(marked, position, token, markedNext)).toBeUndefined() + expect(middleware.provideDefinition?.(marked, position, token, markedNext)).toBeUndefined() + expect( + middleware.provideReferences?.(marked, position, { includeDeclaration: true }, token, markedNext), + ).toBeUndefined() + expect(middleware.provideDocumentSymbols?.(marked, token, markedNext)).toBeUndefined() + expect(middleware.provideDocumentFormattingEdits?.(marked, formattingOptions, token, markedNext)).toBeUndefined() + expect(middleware.provideRenameEdits?.(marked, position, 'Renamed', token, markedNext)).toBeUndefined() + expect(markedNext).not.toHaveBeenCalled() + + const forwarded = Symbol('forwarded') + const unmarkedNext = vi.fn().mockReturnValue(forwarded) + unmarked.setText('model User { id Int @id }') + expect(middleware.provideHover?.(unmarked, position, token, unmarkedNext)).toBe(forwarded) + expect(middleware.provideDefinition?.(unmarked, position, token, unmarkedNext)).toBe(forwarded) + expect(middleware.provideReferences?.(unmarked, position, { includeDeclaration: true }, token, unmarkedNext)).toBe( + forwarded, + ) + expect(middleware.provideDocumentSymbols?.(unmarked, token, unmarkedNext)).toBe(forwarded) + expect(middleware.provideDocumentFormattingEdits?.(unmarked, formattingOptions, token, unmarkedNext)).toBe( + forwarded, + ) + expect(middleware.provideRenameEdits?.(unmarked, position, 'Renamed', token, unmarkedNext)).toBe(forwarded) + }) + + test('clears diagnostics when a document loses bundled ownership', () => { + const { middleware, documents, diagnosticMessages } = createSubject() + const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + documents.set(schema.uri.toString(), schema) + const diagnostics = [{ message: 'bundled diagnostic' }] as Diagnostic[] + const next = vi.fn() + + middleware.handleDiagnostics?.(schema.uri, diagnostics, next) + schema.setText('// use prisma-next') + middleware.handleDiagnostics?.(schema.uri, diagnostics, next) + + expect(next.mock.calls).toEqual([ + [schema.uri, diagnostics], + [schema.uri, []], + ]) + expect(diagnosticMessages).toEqual(['bundled diagnostic']) + }) + + test('keeps code-action conversion ownership-gated', async () => { + const { middleware, sendRequest, isSnippetEdit } = createSubject() + sendRequest.mockResolvedValue([ + { + title: 'Insert block', + kind: 'quickfix', + edit: { changes: { 'file:///workspace/schema.prisma': [] } }, + }, + ] as never) + const unmarked = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + + const actions = await middleware.provideCodeActions?.(unmarked, range, codeActionContext, token, vi.fn()) + + expect(sendRequest).toHaveBeenCalledOnce() + expect(isSnippetEdit).toHaveBeenCalledOnce() + expect(actions).toEqual([ + { + title: 'Insert block', + command: { + command: 'prisma.applySnippetWorkspaceEdit', + title: '', + arguments: [{ changes: {} }], + }, + edit: undefined, + } satisfies CodeAction, + ]) + + unmarked.setText('// use prisma-next') + expect(await middleware.provideCodeActions?.(unmarked, range, codeActionContext, token, vi.fn())).toBeUndefined() + expect(sendRequest).toHaveBeenCalledOnce() + }) + + test('allows pinned marked documents to use the bundled client', () => { + const { middleware } = createSubject({ pinned: true }) + const schema = document('file:///workspace/schema.prisma', '// use prisma-next') + const next = vi.fn() + + middleware.didOpen?.(schema, next) + + expect(next).toHaveBeenCalledWith(schema) + }) +}) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts new file mode 100644 index 0000000000..c1077a4216 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts @@ -0,0 +1,147 @@ +import type { CodeAction, Command, CompletionItem, CompletionList, ProviderResult, TextDocument, Uri } from 'vscode' +import type { + CodeAction as ProtocolCodeAction, + Command as ProtocolCommand, + TextDocumentIdentifier, +} from 'vscode-languageclient' +import type { LanguageClient, Middleware } from 'vscode-languageclient/node' +import { DocumentOwnershipCoordinator, type DocumentOwner } from './documentOwnership' + +export interface BundledClientMiddlewareOptions { + readonly ownership: DocumentOwnershipCoordinator + readonly getClient: () => LanguageClient + readonly getDocument: (uri: Uri) => TextDocument | undefined + readonly handleDiagnosticMessage: (message: string) => void + readonly isSnippetEdit: (action: ProtocolCodeAction, document: TextDocumentIdentifier) => boolean +} + +export function createBundledClientMiddleware(options: BundledClientMiddlewareOptions): Middleware { + const completionDocuments = new WeakMap() + + const ownerForDocument = (document: TextDocument): DocumentOwner => { + void options.ownership.synchronize(document) + return options.ownership.classify(document) + } + + const isBundledDocument = (document: TextDocument): boolean => ownerForDocument(document).kind === 'bundled' + + const middleware: Middleware = { + didOpen: (document, next) => { + if (isBundledDocument(document)) { + next(document) + } + }, + didChange: (event, next) => { + if (isBundledDocument(event.document)) { + next(event) + } + }, + didClose: (document, next) => { + if (isBundledDocument(document)) { + next(document) + } + }, + handleDiagnostics: (uri, diagnostics, next) => { + const document = options.getDocument(uri) + const owner = document ? ownerForDocument(document) : options.ownership.getOwner(uri) + if (owner.kind !== 'bundled') { + next(uri, []) + return + } + + for (const diagnostic of diagnostics) { + options.handleDiagnosticMessage(diagnostic.message) + } + next(uri, diagnostics) + }, + provideCompletionItem: (document, position, context, token, next) => { + if (!isBundledDocument(document)) { + return undefined + } + + return mapProviderResult(next(document, position, context, token), (result) => { + for (const item of completionItems(result)) { + completionDocuments.set(item, document) + } + return result + }) + }, + resolveCompletionItem: (item, token, next) => { + const document = completionDocuments.get(item) + if (!document || !isBundledDocument(document)) { + return undefined + } + return next(item, token) + }, + provideHover: (document, position, token, next) => + isBundledDocument(document) ? next(document, position, token) : undefined, + provideDefinition: (document, position, token, next) => + isBundledDocument(document) ? next(document, position, token) : undefined, + provideReferences: (document, position, referenceContext, token, next) => + isBundledDocument(document) ? next(document, position, referenceContext, token) : undefined, + provideDocumentSymbols: (document, token, next) => + isBundledDocument(document) ? next(document, token) : undefined, + provideDocumentFormattingEdits: (document, formattingOptions, token, next) => + isBundledDocument(document) ? next(document, formattingOptions, token) : undefined, + provideRenameEdits: (document, position, newName, token, next) => + isBundledDocument(document) ? next(document, position, newName, token) : undefined, + provideCodeActions: async (document, range, context, token) => { + if (!isBundledDocument(document)) { + return undefined + } + + const client = options.getClient() + const documentIdentifier = client.code2ProtocolConverter.asTextDocumentIdentifier(document) + const params = { + textDocument: documentIdentifier, + range: client.code2ProtocolConverter.asRange(range), + context: client.code2ProtocolConverter.asCodeActionContext(context), + } + + return client + .sendRequest<(ProtocolCodeAction | ProtocolCommand)[] | null>('textDocument/codeAction', params, token) + .then( + (values) => { + if (values === null) return undefined + const result: (CodeAction | Command)[] = [] + for (const item of values) { + if (isProtocolCodeAction(item)) { + const action = client.protocol2CodeConverter.asCodeAction(item) + if (options.isSnippetEdit(item, documentIdentifier) && item.edit !== undefined) { + action.command = { + command: 'prisma.applySnippetWorkspaceEdit', + title: '', + arguments: [action.edit], + } + action.edit = undefined + } + result.push(action) + } else { + result.push(client.protocol2CodeConverter.asCommand(item)) + } + } + return result + }, + () => undefined, + ) + }, + } + + return middleware +} + +function completionItems(result: CompletionItem[] | CompletionList | undefined | null): CompletionItem[] { + if (!result) return [] + return Array.isArray(result) ? result : result.items +} + +function mapProviderResult( + result: ProviderResult, + map: (value: T | undefined | null) => T | undefined | null, +): ProviderResult { + return Promise.resolve(result).then(map) +} + +function isProtocolCodeAction(item: ProtocolCodeAction | ProtocolCommand): item is ProtocolCodeAction { + return typeof item.command !== 'string' +} diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 979e302685..256cca461a 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -2,27 +2,8 @@ import path from 'path' import os from 'os' import minimatch from 'minimatch' -import { - CancellationToken, - CodeAction, - CodeActionContext, - Command, - commands, - ExtensionContext, - Range, - TextDocument, - window, - workspace, - languages, - WorkspaceConfiguration, -} from 'vscode' -import { - CodeAction as lsCodeAction, - CodeActionParams, - CodeActionRequest, - LanguageClientOptions, - ProvideCodeActionsSignature, -} from 'vscode-languageclient' +import { commands, ExtensionContext, TextDocument, window, workspace, languages, WorkspaceConfiguration } from 'vscode' +import { LanguageClientOptions } from 'vscode-languageclient' import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node' import TelemetryReporter from '../../telemetryReporter' import { @@ -30,7 +11,6 @@ import { checkForMinimalColorTheme, checkForOtherPrismaExtension, isDebugOrTestSession, - isPrismaNextSchema, isSnippetEdit, restartClient, createLanguageServer, @@ -41,6 +21,8 @@ import FileWatcher from 'watcher' import { CodelensProvider, generateClient } from '../../CodeLensProvider' import * as prisma6Handling from '../../prisma6Handling' import { getPackageJSON } from '../../getPackageJSON' +import { DocumentOwnershipCoordinator } from './documentOwnership' +import { createBundledClientMiddleware } from './bundledClientMiddleware' let client: LanguageClient let serverModule: string @@ -123,68 +105,31 @@ const plugin: PrismaVSCodePlugin = { setGenerateWatcher(!!workspace.getConfiguration('prisma').get('fileWatcher')) + const ownership = new DocumentOwnershipCoordinator({ + workspace, + policy: { + isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), + }, + }) + // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for prisma documents documentSelector: [{ scheme: 'file', language: 'prisma' }], - - /* This middleware is part of the workaround for https://github.com/prisma/language-tools/issues/311 */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - middleware: { - async provideCodeActions( - document: TextDocument, - range: Range, - context: CodeActionContext, - token: CancellationToken, - _: ProvideCodeActionsSignature, - ) { - const params: CodeActionParams = { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), - range: client.code2ProtocolConverter.asRange(range), - context: client.code2ProtocolConverter.asCodeActionContext(context), - } - - return client.sendRequest(CodeActionRequest.type, params, token).then( - (values) => { - if (values === null) return undefined - const result: (CodeAction | Command)[] = [] - for (const item of values) { - if (lsCodeAction.is(item)) { - const action = client.protocol2CodeConverter.asCodeAction(item) - if ( - isSnippetEdit(item, client.code2ProtocolConverter.asTextDocumentIdentifier(document)) && - item.edit !== undefined - ) { - action.command = { - command: 'prisma.applySnippetWorkspaceEdit', - title: '', - arguments: [action.edit], - } - action.edit = undefined - } - result.push(action) - } else { - const command = client.protocol2CodeConverter.asCommand(item) - result.push(command) - } - } - return result - }, - (_) => undefined, - ) - }, - handleDiagnostics: (uri, diagnostics, next) => { - for (const diagnostic of diagnostics) { - void prisma6Handling.handleDiagnostic(diagnostic.message, context) - } - next(uri, diagnostics) + middleware: createBundledClientMiddleware({ + ownership, + getClient: () => client, + getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), + handleDiagnosticMessage: (message) => { + void prisma6Handling.handleDiagnostic(message, context) }, - }, + isSnippetEdit, + }), } let started = false const needsLanguageServer = (doc: TextDocument): boolean => - doc.languageId === 'prisma' && !isPrismaNextSchema(doc.getText()) + doc.languageId === 'prisma' && ownership.classify(doc).kind === 'bundled' const maybeStart = () => { if (started) return From d10f54bf10b25189ad92aa9b980b3e232de5a5bb Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 09:24:42 +0000 Subject: [PATCH 06/43] fix(vscode): balance bundled document synchronization --- .../bundledClientMiddleware.test.ts | 131 +++++++++++++++--- .../bundledClientMiddleware.ts | 54 +++++++- 2 files changed, 164 insertions(+), 21 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts index b54a218021..fa1dcff09e 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts @@ -56,14 +56,29 @@ function createSubject(options: { pinned?: boolean } = {}): { diagnosticMessages: string[] isSnippetEdit: ReturnType sendRequest: ReturnType + sendNotification: ReturnType + deleteDiagnostics: ReturnType } { const documents = new Map() const diagnosticMessages: string[] = [] const isSnippetEdit = vi.fn().mockReturnValue(true) const sendRequest = vi.fn() + const sendNotification = vi.fn().mockResolvedValue(undefined) + const deleteDiagnostics = vi.fn() const client = { code2ProtocolConverter: { asTextDocumentIdentifier: (textDocument: TextDocument) => ({ uri: textDocument.uri.toString() }), + asOpenTextDocumentParams: (textDocument: TextDocument) => ({ + textDocument: { + uri: textDocument.uri.toString(), + languageId: textDocument.languageId, + version: 1, + text: textDocument.getText(), + }, + }), + asCloseTextDocumentParams: (textDocument: TextDocument) => ({ + textDocument: { uri: textDocument.uri.toString() }, + }), asRange: () => ({ start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }), asCodeActionContext: () => ({ diagnostics: [] }), }, @@ -71,6 +86,8 @@ function createSubject(options: { pinned?: boolean } = {}): { asCodeAction: (action: { title: string }) => ({ title: action.title, edit: { changes: {} } }), asCommand: (command: { title: string; command: string }) => command, }, + diagnostics: { delete: deleteDiagnostics }, + sendNotification, sendRequest, } as unknown as LanguageClient const ownership = new DocumentOwnershipCoordinator({ @@ -94,37 +111,113 @@ function createSubject(options: { pinned?: boolean } = {}): { diagnosticMessages, isSnippetEdit, sendRequest, + sendNotification, + deleteDiagnostics, } } describe('bundled client ownership middleware', () => { - test('does not forward marked document lifecycle notifications', () => { - const { middleware } = createSubject() + test('balances bundled lifecycle notifications without duplicate opens or closes', () => { + const { middleware, sendNotification, deleteDiagnostics } = createSubject() + const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + const didOpen = vi.fn() + const didChange = vi.fn() + const didClose = vi.fn() + const change = { document: schema } as unknown as TextDocumentChangeEvent + + middleware.didOpen?.(schema, didOpen) + middleware.didOpen?.(schema, didOpen) + middleware.didChange?.(change, didChange) + middleware.didClose?.(schema, didClose) + middleware.didClose?.(schema, didClose) + middleware.didOpen?.(schema, didOpen) + middleware.didClose?.(schema, didClose) + + expect(didOpen).toHaveBeenCalledTimes(2) + expect(didOpen).toHaveBeenCalledWith(schema) + expect(didChange).toHaveBeenCalledOnce() + expect(didChange).toHaveBeenCalledWith(change) + expect(didClose).toHaveBeenCalledTimes(2) + expect(didClose).toHaveBeenCalledWith(schema) + expect(sendNotification).not.toHaveBeenCalled() + expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) + }) + + test('closes and clears a bundled document immediately when it becomes marked', () => { + const { middleware, sendNotification, deleteDiagnostics } = createSubject() + const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + const didOpen = vi.fn() + const didChange = vi.fn() + + middleware.didOpen?.(schema, didOpen) + schema.setText('// use prisma-next\nmodel User { id Int @id }') + const markedChange = { document: schema } as unknown as TextDocumentChangeEvent + middleware.didChange?.(markedChange, didChange) + middleware.didChange?.(markedChange, didChange) + + expect(didChange).not.toHaveBeenCalled() + expect(sendNotification).toHaveBeenCalledOnce() + expect(sendNotification).toHaveBeenCalledWith('textDocument/didClose', { + textDocument: { uri: schema.uri.toString() }, + }) + expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) + }) + + test('reopens a reacquired bundled document with complete current text', () => { + const { middleware, sendNotification } = createSubject() const schema = document('file:///workspace/schema.prisma', '// use prisma-next') - const next = vi.fn() + const didOpen = vi.fn() + const didChange = vi.fn() - middleware.didOpen?.(schema, next) - middleware.didChange?.({ document: schema } as unknown as TextDocumentChangeEvent, next) - middleware.didClose?.(schema, next) + middleware.didOpen?.(schema, didOpen) + schema.setText('model User {\n id Int @id\n name String\n}') + const unmarkedChange = { document: schema } as unknown as TextDocumentChangeEvent + middleware.didChange?.(unmarkedChange, didChange) - expect(next).not.toHaveBeenCalled() + expect(didOpen).not.toHaveBeenCalled() + expect(didChange).not.toHaveBeenCalled() + expect(sendNotification).toHaveBeenCalledOnce() + expect(sendNotification).toHaveBeenCalledWith('textDocument/didOpen', { + textDocument: { + uri: schema.uri.toString(), + languageId: 'prisma', + version: 1, + text: schema.getText(), + }, + }) + + middleware.didChange?.(unmarkedChange, didChange) + expect(didChange).toHaveBeenCalledOnce() + expect(sendNotification).toHaveBeenCalledOnce() }) - test('forwards unmarked document lifecycle notifications', () => { - const { middleware } = createSubject() + test('forwards a real close for every URI still tracked by the bundled server', () => { + const { middleware, sendNotification, deleteDiagnostics } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') const didOpen = vi.fn() const didChange = vi.fn() const didClose = vi.fn() - const change = { document: schema } as unknown as TextDocumentChangeEvent middleware.didOpen?.(schema, didOpen) - middleware.didChange?.(change, didChange) + schema.setText('// use prisma-next') middleware.didClose?.(schema, didClose) - expect(didOpen).toHaveBeenCalledWith(schema) - expect(didChange).toHaveBeenCalledWith(change) + expect(didClose).toHaveBeenCalledOnce() expect(didClose).toHaveBeenCalledWith(schema) + expect(sendNotification).not.toHaveBeenCalled() + expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) + + const reopened = document('file:///workspace/reopened.prisma', 'model User { id Int @id }') + middleware.didOpen?.(reopened, didOpen) + reopened.setText('// use prisma-next') + middleware.didChange?.({ document: reopened } as unknown as TextDocumentChangeEvent, didChange) + middleware.didClose?.(reopened, didClose) + + expect(sendNotification).toHaveBeenCalledOnce() + expect(sendNotification).toHaveBeenCalledWith('textDocument/didClose', { + textDocument: { uri: reopened.uri.toString() }, + }) + expect(didClose).toHaveBeenCalledOnce() }) test('gates every advertised document request while preserving unmarked forwarding', async () => { @@ -222,13 +315,17 @@ describe('bundled client ownership middleware', () => { expect(sendRequest).toHaveBeenCalledOnce() }) - test('allows pinned marked documents to use the bundled client', () => { + test('allows pinned marked documents to stay synchronized with the bundled client', () => { const { middleware } = createSubject({ pinned: true }) const schema = document('file:///workspace/schema.prisma', '// use prisma-next') - const next = vi.fn() + const didOpen = vi.fn() + const didChange = vi.fn() + const change = { document: schema } as unknown as TextDocumentChangeEvent - middleware.didOpen?.(schema, next) + middleware.didOpen?.(schema, didOpen) + middleware.didChange?.(change, didChange) - expect(next).toHaveBeenCalledWith(schema) + expect(didOpen).toHaveBeenCalledWith(schema) + expect(didChange).toHaveBeenCalledWith(change) }) }) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts index c1077a4216..1f1a24fef1 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts @@ -17,6 +17,7 @@ export interface BundledClientMiddlewareOptions { export function createBundledClientMiddleware(options: BundledClientMiddlewareOptions): Middleware { const completionDocuments = new WeakMap() + const bundledDocuments = new Set() const ownerForDocument = (document: TextDocument): DocumentOwner => { void options.ownership.synchronize(document) @@ -25,21 +26,66 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp const isBundledDocument = (document: TextDocument): boolean => ownerForDocument(document).kind === 'bundled' + const clearDiagnostics = (uri: Uri): void => { + options.getClient().diagnostics?.delete(uri) + } + + const openBundledDocument = (document: TextDocument): void => { + const documentUri = document.uri.toString() + if (bundledDocuments.has(documentUri)) return + + bundledDocuments.add(documentUri) + const client = options.getClient() + void client.sendNotification( + 'textDocument/didOpen', + client.code2ProtocolConverter.asOpenTextDocumentParams(document), + ) + } + + const closeBundledDocument = (document: TextDocument): void => { + const documentUri = document.uri.toString() + if (!bundledDocuments.delete(documentUri)) return + + const client = options.getClient() + void client.sendNotification( + 'textDocument/didClose', + client.code2ProtocolConverter.asCloseTextDocumentParams(document), + ) + } + const middleware: Middleware = { didOpen: (document, next) => { + const documentUri = document.uri.toString() if (isBundledDocument(document)) { - next(document) + if (!bundledDocuments.has(documentUri)) { + bundledDocuments.add(documentUri) + next(document) + } + } else { + closeBundledDocument(document) + clearDiagnostics(document.uri) } }, didChange: (event, next) => { - if (isBundledDocument(event.document)) { - next(event) + const document = event.document + const documentUri = document.uri.toString() + if (isBundledDocument(document)) { + if (bundledDocuments.has(documentUri)) { + next(event) + } else { + openBundledDocument(document) + } + } else { + closeBundledDocument(document) + clearDiagnostics(document.uri) } }, didClose: (document, next) => { - if (isBundledDocument(document)) { + void options.ownership.synchronize(document) + if (bundledDocuments.delete(document.uri.toString())) { next(document) } + clearDiagnostics(document.uri) }, handleDiagnostics: (uri, diagnostics, next) => { const document = options.getDocument(uri) From a75f6bbb3db95a6f55328995acd651cf61f7eb43 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 09:34:24 +0000 Subject: [PATCH 07/43] fix(vscode): reset bundled state on restart --- .../bundledClientMiddleware.test.ts | 52 +++++++++++++++++-- .../bundledClientMiddleware.ts | 14 +++-- .../plugins/prisma-language-server/index.ts | 27 ++++++---- packages/vscode/src/util.ts | 8 +++ 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts index fa1dcff09e..80d8851333 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts @@ -14,8 +14,8 @@ import type { Uri, WorkspaceFolder, } from 'vscode' -import type { LanguageClient, Middleware } from 'vscode-languageclient/node' -import { createBundledClientMiddleware } from './bundledClientMiddleware' +import type { LanguageClient } from 'vscode-languageclient/node' +import { createBundledClientMiddleware, type BundledClientMiddleware } from './bundledClientMiddleware' import { DocumentOwnershipCoordinator } from './documentOwnership' const position = {} as Position @@ -50,7 +50,7 @@ function document(value: string, text: string): TextDocument & { setText(nextTex } function createSubject(options: { pinned?: boolean } = {}): { - middleware: Middleware + middleware: BundledClientMiddleware client: LanguageClient documents: Map diagnosticMessages: string[] @@ -143,6 +143,52 @@ describe('bundled client ownership middleware', () => { expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) }) + test('resynchronizes an open document exactly once after the bundled client restarts', async () => { + const { middleware, client, sendNotification } = createSubject() + const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + const oldDidOpen = vi.fn() + const oldCompletion = { label: 'id' } as CompletionItem + + middleware.didOpen?.(schema, oldDidOpen) + await middleware.provideCompletionItem?.(schema, position, completionContext, token, () => [oldCompletion]) + schema.setText('model User {\n id Int @id\n name String\n}') + middleware.resetClientState() + + expect(middleware.resolveCompletionItem?.(oldCompletion, token, vi.fn())).toBeUndefined() + + const replacementOpenParams: unknown[] = [] + const replacementDidOpen = vi.fn((textDocument: TextDocument) => { + replacementOpenParams.push(client.code2ProtocolConverter.asOpenTextDocumentParams(textDocument)) + }) + const replacementDidChange = vi.fn() + const replacementDidClose = vi.fn() + const change = { document: schema } as unknown as TextDocumentChangeEvent + + middleware.didOpen?.(schema, replacementDidOpen) + middleware.didOpen?.(schema, replacementDidOpen) + middleware.didChange?.(change, replacementDidChange) + middleware.didClose?.(schema, replacementDidClose) + middleware.didClose?.(schema, replacementDidClose) + + expect(oldDidOpen).toHaveBeenCalledOnce() + expect(replacementDidOpen).toHaveBeenCalledOnce() + expect(replacementOpenParams).toEqual([ + { + textDocument: { + uri: schema.uri.toString(), + languageId: 'prisma', + version: 1, + text: schema.getText(), + }, + }, + ]) + expect(replacementDidChange).toHaveBeenCalledOnce() + expect(replacementDidChange).toHaveBeenCalledWith(change) + expect(replacementDidClose).toHaveBeenCalledOnce() + expect(replacementDidClose).toHaveBeenCalledWith(schema) + expect(sendNotification).not.toHaveBeenCalled() + }) + test('closes and clears a bundled document immediately when it becomes marked', () => { const { middleware, sendNotification, deleteDiagnostics } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts index 1f1a24fef1..3b6bf0e74f 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts @@ -15,8 +15,12 @@ export interface BundledClientMiddlewareOptions { readonly isSnippetEdit: (action: ProtocolCodeAction, document: TextDocumentIdentifier) => boolean } -export function createBundledClientMiddleware(options: BundledClientMiddlewareOptions): Middleware { - const completionDocuments = new WeakMap() +export interface BundledClientMiddleware extends Middleware { + resetClientState(): void +} + +export function createBundledClientMiddleware(options: BundledClientMiddlewareOptions): BundledClientMiddleware { + let completionDocuments = new WeakMap() const bundledDocuments = new Set() const ownerForDocument = (document: TextDocument): DocumentOwner => { @@ -53,7 +57,11 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp ) } - const middleware: Middleware = { + const middleware: BundledClientMiddleware = { + resetClientState: () => { + bundledDocuments.clear() + completionDocuments = new WeakMap() + }, didOpen: (document, next) => { const documentUri = document.uri.toString() if (isBundledDocument(document)) { diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 256cca461a..bb8177d482 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -112,19 +112,21 @@ const plugin: PrismaVSCodePlugin = { }, }) + const bundledClientMiddleware = createBundledClientMiddleware({ + ownership, + getClient: () => client, + getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), + handleDiagnosticMessage: (message) => { + void prisma6Handling.handleDiagnostic(message, context) + }, + isSnippetEdit, + }) + // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for prisma documents documentSelector: [{ scheme: 'file', language: 'prisma' }], - middleware: createBundledClientMiddleware({ - ownership, - getClient: () => client, - getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), - handleDiagnosticMessage: (message) => { - void prisma6Handling.handleDiagnostic(message, context) - }, - isSnippetEdit, - }), + middleware: bundledClientMiddleware, } let started = false @@ -144,7 +146,12 @@ const plugin: PrismaVSCodePlugin = { return } const serverOptions = getServerOptions(workspace.getConfiguration('prisma'), context) - client = await restartClient(context, client, serverOptions, clientOptions) + client = await restartClient(context, client, serverOptions, clientOptions, { + onClientStopped: () => bundledClientMiddleware.resetClientState(), + onClientCreated: (replacementClient) => { + client = replacementClient + }, + }) } context.subscriptions.push( diff --git a/packages/vscode/src/util.ts b/packages/vscode/src/util.ts index 90a646132c..d9601e135c 100644 --- a/packages/vscode/src/util.ts +++ b/packages/vscode/src/util.ts @@ -112,15 +112,23 @@ export function createLanguageServer( ): LanguageClient { return new LanguageClient('prisma', 'Prisma Language Server', serverOptions, clientOptions) } +export interface RestartClientLifecycle { + onClientStopped(): void + onClientCreated(client: LanguageClient): void +} + export const restartClient = async ( context: ExtensionContext, client: LanguageClient, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, + lifecycle?: RestartClientLifecycle, ): Promise => { client?.diagnostics?.dispose() if (client) await client.stop() + lifecycle?.onClientStopped() client = createLanguageServer(serverOptions, clientOptions) + lifecycle?.onClientCreated(client) context.subscriptions.push(client.start()) await client.onReady() return client From bcefc7e5cd1999225b1c91b45ddc113366fe1c2f Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 09:48:02 +0000 Subject: [PATCH 08/43] feat(vscode): start root-local Prisma Next clients --- packages/vscode/package.json | 2 +- .../vscode/src/__test__/workspace.test.ts | 39 +++- .../plugins/prisma-language-server/index.ts | 39 +++- .../localPrismaNextClientRegistry.test.ts | 190 ++++++++++++++++++ .../localPrismaNextClientRegistry.ts | 140 +++++++++++++ 5 files changed, 405 insertions(+), 5 deletions(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 36243f3974..3e3f25923c 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -47,7 +47,7 @@ "watch": "node esbuild.mjs --watch", "test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true", "test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js", - "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts", + "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts", "test:playwright": "playwright test", "test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright", "vscode:prepublish": "pnpm run build", diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index 79aa5d58f9..3819a60e6f 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -1,7 +1,11 @@ import assert from 'node:assert' import { stat } from 'node:fs/promises' import vscode from 'vscode' -import { getPrismaCliEntrypoint, getWorkspaceDocUri, getWorkspaceFolder } from './helper' +import { + localPrismaNextClientTestStateCommand, + type LocalPrismaNextClientTestState, +} from '../plugins/prisma-language-server/localPrismaNextClientRegistry' +import { getPrismaCliEntrypoint, getWorkspaceDocUri, getWorkspaceFolder, sleep } from './helper' suite('Multi-root integration workspace', () => { test('resolves documents and real Prisma CLI entrypoints per workspace root', async () => { @@ -24,4 +28,37 @@ suite('Multi-root integration workspace', () => { ) } }) + + test('lazily starts the real root-local Prisma Next clients', async () => { + const rootA = getWorkspaceFolder('integration-root-a') + const rootB = getWorkspaceFolder('integration-root-b') + const documentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'schema.prisma')) + const documentB = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootB, 'schema.prisma')) + const extension = vscode.extensions.getExtension('Prisma.prisma') + assert.ok(extension) + await extension.activate() + + assert.deepStrictEqual(await getLocalClientState(), { startedWorkspaceFolderUris: [] }) + + const edit = new vscode.WorkspaceEdit() + edit.insert(documentA.uri, new vscode.Position(0, 0), '// use prisma-next\n') + edit.insert(documentB.uri, new vscode.Position(0, 0), '// use prisma-next\n') + assert.strictEqual(await vscode.workspace.applyEdit(edit), true) + + const expectedRoots = [rootA.uri.toString(), rootB.uri.toString()].sort() + for (let attempt = 0; attempt < 100; attempt += 1) { + const state = await getLocalClientState() + if (state.startedWorkspaceFolderUris.join() === expectedRoots.join()) { + assert.deepStrictEqual(state.startedWorkspaceFolderUris, expectedRoots) + return + } + await sleep(100) + } + + assert.deepStrictEqual((await getLocalClientState()).startedWorkspaceFolderUris, expectedRoots) + }) }) + +async function getLocalClientState(): Promise { + return vscode.commands.executeCommand(localPrismaNextClientTestStateCommand) +} diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index bb8177d482..e4222852c5 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -23,6 +23,7 @@ import * as prisma6Handling from '../../prisma6Handling' import { getPackageJSON } from '../../getPackageJSON' import { DocumentOwnershipCoordinator } from './documentOwnership' import { createBundledClientMiddleware } from './bundledClientMiddleware' +import { LocalPrismaNextClientRegistry, localPrismaNextClientTestStateCommand } from './localPrismaNextClientRegistry' let client: LanguageClient let serverModule: string @@ -105,11 +106,25 @@ const plugin: PrismaVSCodePlugin = { setGenerateWatcher(!!workspace.getConfiguration('prisma').get('fileWatcher')) + const localClients = new LocalPrismaNextClientRegistry({ + workspace, + createClient: (id, name, serverOptions, localClientOptions) => + new LanguageClient(id, name, serverOptions, localClientOptions), + registerDisposable: (disposable) => context.subscriptions.push(disposable), + handleStartError: (workspaceFolder, error) => { + console.error(`Failed to start Prisma Next Language Server for ${workspaceFolder.uri.toString()}`, error) + }, + }) const ownership = new DocumentOwnershipCoordinator({ workspace, policy: { isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), }, + prepareOwner: async ({ document, nextOwner }) => { + if (nextOwner.kind === 'local') { + await localClients.ensureClientForDocument(document) + } + }, }) const bundledClientMiddleware = createBundledClientMiddleware({ @@ -132,6 +147,11 @@ const plugin: PrismaVSCodePlugin = { let started = false const needsLanguageServer = (doc: TextDocument): boolean => doc.languageId === 'prisma' && ownership.classify(doc).kind === 'bundled' + const prepareLocalClient = (document: TextDocument): void => { + if (document.languageId === 'prisma' && ownership.classify(document).kind === 'local') { + void ownership.synchronize(document) + } + } const maybeStart = () => { if (started) return @@ -204,13 +224,26 @@ const plugin: PrismaVSCodePlugin = { void window.showInformationMessage('Unpinned workspace from Prisma 6.') }), - workspace.onDidOpenTextDocument(() => maybeStart()), - workspace.onDidChangeTextDocument(() => maybeStart()), + workspace.onDidOpenTextDocument((document) => { + maybeStart() + prepareLocalClient(document) + }), + workspace.onDidChangeTextDocument((event) => { + maybeStart() + prepareLocalClient(event.document) + }), ) maybeStart() + for (const document of workspace.textDocuments) { + prepareLocalClient(document) + } - if (!isDebugOrTest) { + if (isDebugOrTest) { + context.subscriptions.push( + commands.registerCommand(localPrismaNextClientTestStateCommand, () => localClients.getTestState()), + ) + } else { const packageJSON = getPackageJSON(context) const extensionId = 'prisma.' + packageJSON.name const extensionVersion = packageJSON.version ?? 'unknown' diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts new file mode 100644 index 0000000000..bf2e2d1086 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts @@ -0,0 +1,190 @@ +import path from 'node:path' +import { describe, expect, test, vi } from 'vitest' +import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' +import type { LanguageClientOptions } from 'vscode-languageclient' +import type { LanguageClient, ServerOptions } from 'vscode-languageclient/node' +import { + createLocalPrismaNextClientOptions, + createLocalPrismaNextServerOptions, + getLocalPrismaNextEntrypoint, + LocalPrismaNextClientRegistry, +} from './localPrismaNextClientRegistry' + +vi.mock('vscode-languageclient/node', () => ({ + TransportKind: { stdio: 0 }, +})) + +const rootA = workspaceFolder('file:///workspace-a', '/workspace-a', 'workspace-a') +const rootB = workspaceFolder('file:///workspace-b', '/workspace-b', 'workspace-b') + +function uri(value: string, fsPath = value): Uri { + return { + scheme: value.slice(0, value.indexOf(':')), + fsPath, + toString: () => value, + } as Uri +} + +function workspaceFolder(value: string, fsPath: string, name: string): WorkspaceFolder { + return { uri: uri(value, fsPath), name } as WorkspaceFolder +} + +function document(value: string): TextDocument { + return { + uri: uri(value), + languageId: 'prisma', + getText: () => '// use prisma-next', + } as TextDocument +} + +function deferred(): { promise: Promise; resolve(value: T): void } { + let resolvePromise: ((value: T) => void) | undefined + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + return { + promise, + resolve: (value) => resolvePromise?.(value), + } +} + +function fakeClient(name: string, onReady = vi.fn().mockResolvedValue(undefined)): LanguageClient { + return { + name, + start: vi.fn().mockReturnValue({ dispose: vi.fn() } satisfies Disposable), + onReady, + } as unknown as LanguageClient +} + +function matchingWorkspaceFolder(documentUri: Uri): WorkspaceFolder | undefined { + if (documentUri.toString().includes('workspace-a')) return rootA + if (documentUri.toString().includes('workspace-b')) return rootB + return undefined +} + +describe('LocalPrismaNextClientRegistry', () => { + test('builds exact module options for the matching workspace root', () => { + const entrypoint = getLocalPrismaNextEntrypoint(rootA) + const serverOptions = createLocalPrismaNextServerOptions(rootA) + const clientOptions = createLocalPrismaNextClientOptions(rootA) + + expect(entrypoint).toBe(path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js')) + expect(serverOptions).toEqual({ + module: entrypoint, + args: ['lsp'], + transport: 0, + options: { cwd: '/workspace-a' }, + }) + expect(serverOptions).not.toHaveProperty('runtime') + expect(clientOptions).toEqual({ documentSelector: [], workspaceFolder: rootA }) + }) + + test('publishes pending startup per root and starts independent clients', async () => { + const discovery = deferred() + const entrypointExists = vi.fn().mockReturnValue(discovery.promise) + const clients = new Map([ + [rootA.uri.toString(), fakeClient('root-a')], + [rootB.uri.toString(), fakeClient('root-b')], + ]) + const createClient = vi.fn( + (_id: string, _name: string, _serverOptions: ServerOptions, clientOptions: LanguageClientOptions) => + clients.get(clientOptions.workspaceFolder?.uri.toString() ?? '') as LanguageClient, + ) + const registerDisposable = vi.fn() + const registry = new LocalPrismaNextClientRegistry({ + workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, + entrypointExists, + createClient, + registerDisposable, + }) + + const firstRootA = registry.ensureClientForDocument(document('file:///workspace-a/first.prisma')) + const secondRootA = registry.ensureClientForDocument(document('file:///workspace-a/second.prisma')) + const firstRootB = registry.ensureClientForDocument(document('file:///workspace-b/schema.prisma')) + + await vi.waitFor(() => expect(entrypointExists).toHaveBeenCalledTimes(2)) + expect(createClient).not.toHaveBeenCalled() + discovery.resolve(true) + + await expect(Promise.all([firstRootA, secondRootA, firstRootB])).resolves.toEqual([ + clients.get(rootA.uri.toString()), + clients.get(rootA.uri.toString()), + clients.get(rootB.uri.toString()), + ]) + expect(createClient).toHaveBeenCalledTimes(2) + expect(registerDisposable).toHaveBeenCalledTimes(2) + expect(registry.getTestState()).toEqual({ + startedWorkspaceFolderUris: [rootA.uri.toString(), rootB.uri.toString()], + }) + }) + + test('does no discovery until an eligible document requests a client', async () => { + const entrypointExists = vi.fn().mockResolvedValue(false) + const createClient = vi.fn() + const handleStartError = vi.fn() + const registry = new LocalPrismaNextClientRegistry({ + workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, + entrypointExists, + createClient, + registerDisposable: vi.fn(), + handleStartError, + }) + + expect(entrypointExists).not.toHaveBeenCalled() + expect(createClient).not.toHaveBeenCalled() + + const schema = document('file:///workspace-a/schema.prisma') + await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() + await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() + + expect(entrypointExists).toHaveBeenCalledOnce() + expect(entrypointExists).toHaveBeenCalledWith( + path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js'), + ) + expect(createClient).not.toHaveBeenCalled() + expect(handleStartError).not.toHaveBeenCalled() + }) + + test.each([ + { name: 'untrusted workspace', trusted: false, documentUri: 'file:///workspace-a/schema.prisma' }, + { name: 'non-file document', trusted: true, documentUri: 'untitled:Untitled-1' }, + { name: 'unmatched workspace', trusted: true, documentUri: 'file:///outside/schema.prisma' }, + ])('does not discover or start for an $name', async ({ trusted, documentUri }) => { + const entrypointExists = vi.fn().mockResolvedValue(true) + const createClient = vi.fn() + const registry = new LocalPrismaNextClientRegistry({ + workspace: { isTrusted: trusted, getWorkspaceFolder: matchingWorkspaceFolder }, + entrypointExists, + createClient, + registerDisposable: vi.fn(), + }) + + await expect(registry.ensureClientForDocument(document(documentUri))).resolves.toBeUndefined() + + expect(entrypointExists).not.toHaveBeenCalled() + expect(createClient).not.toHaveBeenCalled() + }) + + test('reports a real startup failure once without retrying automatically', async () => { + const startError = new Error('startup failed') + const client = fakeClient('root-a', vi.fn().mockRejectedValue(startError)) + const handleStartError = vi.fn() + const createClient = vi.fn().mockReturnValue(client) + const registry = new LocalPrismaNextClientRegistry({ + workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, + entrypointExists: vi.fn().mockResolvedValue(true), + createClient, + registerDisposable: vi.fn(), + handleStartError, + }) + const schema = document('file:///workspace-a/schema.prisma') + + await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() + await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() + + expect(createClient).toHaveBeenCalledOnce() + expect(handleStartError).toHaveBeenCalledOnce() + expect(handleStartError).toHaveBeenCalledWith(rootA, startError) + expect(registry.getTestState()).toEqual({ startedWorkspaceFolderUris: [] }) + }) +}) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts new file mode 100644 index 0000000000..98294ce717 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -0,0 +1,140 @@ +import path from 'node:path' +import { stat } from 'node:fs/promises' +import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' +import type { LanguageClientOptions } from 'vscode-languageclient' +import { TransportKind, type LanguageClient, type ServerOptions } from 'vscode-languageclient/node' + +const prismaCliRelativePath = ['node_modules', 'prisma', 'dist', 'prisma.js'] as const + +export const localPrismaNextClientTestStateCommand = 'prisma.test.localPrismaNextClientState' + +export interface LocalPrismaNextClientRegistryWorkspace { + readonly isTrusted: boolean + getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined +} + +export interface LocalPrismaNextClientRegistryOptions { + readonly workspace: LocalPrismaNextClientRegistryWorkspace + readonly createClient: ( + id: string, + name: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions, + ) => LanguageClient + readonly registerDisposable: (disposable: Disposable) => void + readonly entrypointExists?: (entrypoint: string) => Promise + readonly handleStartError?: (workspaceFolder: WorkspaceFolder, error: unknown) => void +} + +export interface LocalPrismaNextClientTestState { + readonly startedWorkspaceFolderUris: readonly string[] +} + +export class LocalPrismaNextClientRegistry { + private readonly clients = new Map>() + private readonly startedClients = new Map() + + constructor(private readonly options: LocalPrismaNextClientRegistryOptions) {} + + ensureClientForDocument(document: TextDocument): Promise { + if (!this.options.workspace.isTrusted || document.uri.scheme !== 'file') { + return Promise.resolve(undefined) + } + + const workspaceFolder = this.options.workspace.getWorkspaceFolder(document.uri) + if (!workspaceFolder || workspaceFolder.uri.scheme !== 'file') { + return Promise.resolve(undefined) + } + + return this.ensureClient(workspaceFolder) + } + + getTestState(): LocalPrismaNextClientTestState { + return { + startedWorkspaceFolderUris: [...this.startedClients.keys()].sort(), + } + } + + private ensureClient(workspaceFolder: WorkspaceFolder): Promise { + const workspaceFolderUri = workspaceFolder.uri.toString() + const existing = this.clients.get(workspaceFolderUri) + if (existing) { + return existing + } + + const pending = Promise.resolve().then(() => this.discoverAndStart(workspaceFolder)) + this.clients.set(workspaceFolderUri, pending) + return pending + } + + private async discoverAndStart(workspaceFolder: WorkspaceFolder): Promise { + const entrypoint = getLocalPrismaNextEntrypoint(workspaceFolder) + + try { + const exists = await (this.options.entrypointExists ?? isFile)(entrypoint) + if (!exists) { + return undefined + } + + const workspaceFolderUri = workspaceFolder.uri.toString() + const client = this.options.createClient( + `prisma-next:${workspaceFolderUri}`, + `Prisma Next Language Server (${workspaceFolder.name})`, + createLocalPrismaNextServerOptions(workspaceFolder, entrypoint), + createLocalPrismaNextClientOptions(workspaceFolder), + ) + this.options.registerDisposable(client.start()) + await client.onReady() + this.startedClients.set(workspaceFolderUri, client) + return client + } catch (error) { + this.options.handleStartError?.(workspaceFolder, error) + return undefined + } + } +} + +export function getLocalPrismaNextEntrypoint(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, ...prismaCliRelativePath) +} + +export function createLocalPrismaNextServerOptions( + workspaceFolder: WorkspaceFolder, + entrypoint = getLocalPrismaNextEntrypoint(workspaceFolder), +): ServerOptions { + return { + module: entrypoint, + args: ['lsp'], + transport: TransportKind.stdio, + // No runtime is specified: vscode-languageclient forks the module with the extension-host Node runtime. + options: { cwd: workspaceFolder.uri.fsPath }, + } +} + +export function createLocalPrismaNextClientOptions(workspaceFolder: WorkspaceFolder): LanguageClientOptions { + return { + // Synchronization stays disabled until owner-filtered local middleware is attached. + documentSelector: [], + workspaceFolder, + } +} + +async function isFile(entrypoint: string): Promise { + try { + return (await stat(entrypoint)).isFile() + } catch (error) { + if (isMissingPathError(error)) { + return false + } + throw error + } +} + +function isMissingPathError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error.code === 'ENOENT' || error.code === 'ENOTDIR') + ) +} From 15e27dd7bc275db5dfbe1a3a50bdc2839b873666 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 10:01:56 +0000 Subject: [PATCH 09/43] fix(vscode): launch local Prisma CLI with exact args --- .../localPrismaNextClientRegistry.test.ts | 157 +++++++++++++++--- .../localPrismaNextClientRegistry.ts | 98 ++++++++++- 2 files changed, 226 insertions(+), 29 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts index bf2e2d1086..0829215681 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts @@ -1,19 +1,20 @@ import path from 'node:path' +import { EventEmitter } from 'node:events' +import { PassThrough } from 'node:stream' +import type { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'node:child_process' import { describe, expect, test, vi } from 'vitest' import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' -import type { LanguageClient, ServerOptions } from 'vscode-languageclient/node' +import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' import { + createExtensionHostNodeEnvironment, createLocalPrismaNextClientOptions, createLocalPrismaNextServerOptions, getLocalPrismaNextEntrypoint, + launchLocalPrismaNextServer, LocalPrismaNextClientRegistry, } from './localPrismaNextClientRegistry' -vi.mock('vscode-languageclient/node', () => ({ - TransportKind: { stdio: 0 }, -})) - const rootA = workspaceFolder('file:///workspace-a', '/workspace-a', 'workspace-a') const rootB = workspaceFolder('file:///workspace-b', '/workspace-b', 'workspace-b') @@ -56,6 +57,27 @@ function fakeClient(name: string, onReady = vi.fn().mockResolvedValue(undefined) } as unknown as LanguageClient } +function fakeChildProcess(pid = 123): ChildProcessWithoutNullStreams { + let killed = false + const child = Object.assign(new EventEmitter(), { + stdin: new PassThrough(), + stdout: new PassThrough(), + stderr: new PassThrough(), + pid, + kill: vi.fn(() => { + killed = true + return true + }), + }) + Object.defineProperty(child, 'killed', { get: () => killed }) + return child as unknown as ChildProcessWithoutNullStreams +} + +function invokeServerOptions(serverOptions: ServerOptions): Promise { + expect(serverOptions).toBeTypeOf('function') + return (serverOptions as () => Promise)() +} + function matchingWorkspaceFolder(documentUri: Uri): WorkspaceFolder | undefined { if (documentUri.toString().includes('workspace-a')) return rootA if (documentUri.toString().includes('workspace-b')) return rootB @@ -63,32 +85,101 @@ function matchingWorkspaceFolder(documentUri: Uri): WorkspaceFolder | undefined } describe('LocalPrismaNextClientRegistry', () => { - test('builds exact module options for the matching workspace root', () => { + test('launches the exact CLI argv with extension-host Node and root-local streams', async () => { const entrypoint = getLocalPrismaNextEntrypoint(rootA) - const serverOptions = createLocalPrismaNextServerOptions(rootA) - const clientOptions = createLocalPrismaNextClientOptions(rootA) + const child = fakeChildProcess() + const spawnProcess = vi.fn((_executable: string, _args: string[], _options: SpawnOptionsWithoutStdio) => { + queueMicrotask(() => child.emit('spawn')) + return child + }) + const handleProcessError = vi.fn() + const serverOptions = createLocalPrismaNextServerOptions(rootA, entrypoint, { + executable: '/extension-host', + environment: { EXISTING: 'preserved' }, + spawnProcess, + handleProcessError, + }) + + const result = await invokeServerOptions(serverOptions) expect(entrypoint).toBe(path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js')) - expect(serverOptions).toEqual({ - module: entrypoint, - args: ['lsp'], - transport: 0, - options: { cwd: '/workspace-a' }, + expect(spawnProcess).toHaveBeenCalledOnce() + expect(spawnProcess).toHaveBeenCalledWith('/extension-host', [entrypoint, 'lsp'], { + cwd: '/workspace-a', + env: { + EXISTING: 'preserved', + ELECTRON_RUN_AS_NODE: '1', + ELECTRON_NO_ASAR: '1', + }, + shell: false, + stdio: ['pipe', 'pipe', 'pipe'], + }) + expect(result).toEqual({ process: child, detached: false }) + expect(result.process.stdin).toBe(child.stdin) + expect(result.process.stdout).toBe(child.stdout) + expect(result.process.stderr).toBe(child.stderr) + + const processError = new Error('process error') + child.emit('error', processError) + expect(handleProcessError).toHaveBeenCalledWith(processError) + }) + + test('preserves the environment while enabling Electron extension hosts to run as Node', () => { + expect(createExtensionHostNodeEnvironment({ EXISTING: 'preserved', ELECTRON_RUN_AS_NODE: '0' })).toEqual({ + EXISTING: 'preserved', + ELECTRON_RUN_AS_NODE: '1', + ELECTRON_NO_ASAR: '1', + }) + }) + + test('rejects early spawn errors and releases startup resources', async () => { + const child = fakeChildProcess() + const startError = new Error('spawn failed') + const spawnProcess = vi.fn(() => { + queueMicrotask(() => child.emit('error', startError)) + return child }) - expect(serverOptions).not.toHaveProperty('runtime') - expect(clientOptions).toEqual({ documentSelector: [], workspaceFolder: rootA }) + + await expect( + launchLocalPrismaNextServer({ + executable: '/extension-host', + entrypoint: '/workspace-a/node_modules/prisma/dist/prisma.js', + cwd: '/workspace-a', + environment: {}, + spawnProcess, + }), + ).rejects.toBe(startError) + + expect(child.killed).toBe(true) + expect(child.stdin.destroyed).toBe(true) + expect(child.stdout.destroyed).toBe(true) + expect(child.stderr.destroyed).toBe(true) + expect(child.listenerCount('error')).toBe(0) + expect(child.listenerCount('spawn')).toBe(0) + }) + + test('keeps local document synchronization disabled until owner middleware is attached', () => { + expect(createLocalPrismaNextClientOptions(rootA)).toEqual({ documentSelector: [], workspaceFolder: rootA }) }) test('publishes pending startup per root and starts independent clients', async () => { const discovery = deferred() const entrypointExists = vi.fn().mockReturnValue(discovery.promise) - const clients = new Map([ - [rootA.uri.toString(), fakeClient('root-a')], - [rootB.uri.toString(), fakeClient('root-b')], - ]) + const clients = new Map() + const spawnProcess = vi.fn((_executable: string, _args: string[], _options: SpawnOptionsWithoutStdio) => { + const child = fakeChildProcess() + queueMicrotask(() => child.emit('spawn')) + return child + }) const createClient = vi.fn( - (_id: string, _name: string, _serverOptions: ServerOptions, clientOptions: LanguageClientOptions) => - clients.get(clientOptions.workspaceFolder?.uri.toString() ?? '') as LanguageClient, + (_id: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions) => { + const client = fakeClient( + name, + vi.fn(() => invokeServerOptions(serverOptions).then(() => undefined)), + ) + clients.set(clientOptions.workspaceFolder?.uri.toString() ?? '', client) + return client + }, ) const registerDisposable = vi.fn() const registry = new LocalPrismaNextClientRegistry({ @@ -96,6 +187,11 @@ describe('LocalPrismaNextClientRegistry', () => { entrypointExists, createClient, registerDisposable, + launcher: { + executable: '/extension-host', + environment: {}, + spawnProcess, + }, }) const firstRootA = registry.ensureClientForDocument(document('file:///workspace-a/first.prisma')) @@ -106,13 +202,30 @@ describe('LocalPrismaNextClientRegistry', () => { expect(createClient).not.toHaveBeenCalled() discovery.resolve(true) - await expect(Promise.all([firstRootA, secondRootA, firstRootB])).resolves.toEqual([ + const results = await Promise.all([firstRootA, secondRootA, firstRootB]) + + expect(results).toEqual([ clients.get(rootA.uri.toString()), clients.get(rootA.uri.toString()), clients.get(rootB.uri.toString()), ]) expect(createClient).toHaveBeenCalledTimes(2) expect(registerDisposable).toHaveBeenCalledTimes(2) + expect(spawnProcess).toHaveBeenCalledTimes(2) + expect( + spawnProcess.mock.calls.map(([executable, args, options]) => ({ executable, args, cwd: options.cwd })), + ).toEqual([ + { + executable: '/extension-host', + args: [path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js'), 'lsp'], + cwd: '/workspace-a', + }, + { + executable: '/extension-host', + args: [path.join('/workspace-b', 'node_modules', 'prisma', 'dist', 'prisma.js'), 'lsp'], + cwd: '/workspace-b', + }, + ]) expect(registry.getTestState()).toEqual({ startedWorkspaceFolderUris: [rootA.uri.toString(), rootB.uri.toString()], }) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index 98294ce717..53bedfe986 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -1,8 +1,9 @@ import path from 'node:path' import { stat } from 'node:fs/promises' +import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process' import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' -import { TransportKind, type LanguageClient, type ServerOptions } from 'vscode-languageclient/node' +import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' const prismaCliRelativePath = ['node_modules', 'prisma', 'dist', 'prisma.js'] as const @@ -13,6 +14,19 @@ export interface LocalPrismaNextClientRegistryWorkspace { getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined } +export type SpawnLocalPrismaNextProcess = ( + executable: string, + args: string[], + options: SpawnOptionsWithoutStdio, +) => ChildProcessWithoutNullStreams + +export interface LocalPrismaNextLauncherOptions { + readonly executable?: string + readonly environment?: NodeJS.ProcessEnv + readonly spawnProcess?: SpawnLocalPrismaNextProcess + readonly handleProcessError?: (error: Error) => void +} + export interface LocalPrismaNextClientRegistryOptions { readonly workspace: LocalPrismaNextClientRegistryWorkspace readonly createClient: ( @@ -24,6 +38,7 @@ export interface LocalPrismaNextClientRegistryOptions { readonly registerDisposable: (disposable: Disposable) => void readonly entrypointExists?: (entrypoint: string) => Promise readonly handleStartError?: (workspaceFolder: WorkspaceFolder, error: unknown) => void + readonly launcher?: Omit } export interface LocalPrismaNextClientTestState { @@ -80,7 +95,10 @@ export class LocalPrismaNextClientRegistry { const client = this.options.createClient( `prisma-next:${workspaceFolderUri}`, `Prisma Next Language Server (${workspaceFolder.name})`, - createLocalPrismaNextServerOptions(workspaceFolder, entrypoint), + createLocalPrismaNextServerOptions(workspaceFolder, entrypoint, { + ...this.options.launcher, + handleProcessError: (error) => this.options.handleStartError?.(workspaceFolder, error), + }), createLocalPrismaNextClientOptions(workspaceFolder), ) this.options.registerDisposable(client.start()) @@ -101,16 +119,82 @@ export function getLocalPrismaNextEntrypoint(workspaceFolder: WorkspaceFolder): export function createLocalPrismaNextServerOptions( workspaceFolder: WorkspaceFolder, entrypoint = getLocalPrismaNextEntrypoint(workspaceFolder), + launcher: LocalPrismaNextLauncherOptions = {}, ): ServerOptions { + return () => + launchLocalPrismaNextServer({ + executable: launcher.executable ?? process.execPath, + entrypoint, + cwd: workspaceFolder.uri.fsPath, + environment: createExtensionHostNodeEnvironment(launcher.environment ?? process.env), + spawnProcess: launcher.spawnProcess ?? spawn, + handleProcessError: launcher.handleProcessError, + }) +} + +export interface LaunchLocalPrismaNextServerOptions { + readonly executable: string + readonly entrypoint: string + readonly cwd: string + readonly environment: NodeJS.ProcessEnv + readonly spawnProcess: SpawnLocalPrismaNextProcess + readonly handleProcessError?: (error: Error) => void +} + +export function launchLocalPrismaNextServer(options: LaunchLocalPrismaNextServerOptions): Promise { + return new Promise((resolve, reject) => { + const child = options.spawnProcess(options.executable, [options.entrypoint, 'lsp'], { + cwd: options.cwd, + env: options.environment, + shell: false, + stdio: ['pipe', 'pipe', 'pipe'], + }) + + const cleanupStartupListeners = (): void => { + child.removeListener('error', handleStartupError) + child.removeListener('spawn', handleSpawn) + } + const handleStartupError = (error: Error): void => { + cleanupStartupListeners() + destroyProcessStreams(child) + if (child.pid !== undefined && !child.killed) { + child.kill() + } + reject(error) + } + const handleSpawn = (): void => { + cleanupStartupListeners() + const handleProcessError = (error: Error): void => { + child.removeListener('close', handleClose) + options.handleProcessError?.(error) + } + const handleClose = (): void => { + child.removeListener('error', handleProcessError) + } + child.once('error', handleProcessError) + child.once('close', handleClose) + resolve({ process: child, detached: false }) + } + + child.once('error', handleStartupError) + child.once('spawn', handleSpawn) + }) +} + +export function createExtensionHostNodeEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return { - module: entrypoint, - args: ['lsp'], - transport: TransportKind.stdio, - // No runtime is specified: vscode-languageclient forks the module with the extension-host Node runtime. - options: { cwd: workspaceFolder.uri.fsPath }, + ...environment, + ELECTRON_RUN_AS_NODE: '1', + ELECTRON_NO_ASAR: '1', } } +function destroyProcessStreams(child: ChildProcessWithoutNullStreams): void { + child.stdin.destroy() + child.stdout.destroy() + child.stderr.destroy() +} + export function createLocalPrismaNextClientOptions(workspaceFolder: WorkspaceFolder): LanguageClientOptions { return { // Synchronization stays disabled until owner-filtered local middleware is attached. From 5a26332dc2c33c695bc00b19a7e670c9047f1118 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 10:29:31 +0000 Subject: [PATCH 10/43] feat(vscode): transfer Prisma document ownership --- packages/vscode/package.json | 2 +- .../bundledClientMiddleware.test.ts | 63 ++--- .../bundledClientMiddleware.ts | 67 +++-- .../documentRouting.test.ts | 236 ++++++++++++++++++ .../prisma-language-server/documentRouting.ts | 75 ++++++ .../plugins/prisma-language-server/index.ts | 39 +-- .../localClientMiddleware.test.ts | 184 ++++++++++++++ .../localClientMiddleware.ts | 138 ++++++++++ .../localPrismaNextClientRegistry.test.ts | 32 ++- .../localPrismaNextClientRegistry.ts | 58 ++++- 10 files changed, 799 insertions(+), 95 deletions(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/documentRouting.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 3e3f25923c..ea4bef2e73 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -47,7 +47,7 @@ "watch": "node esbuild.mjs --watch", "test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true", "test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js", - "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts", + "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/documentRouting.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts src/plugins/prisma-language-server/localClientMiddleware.test.ts src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts", "test:playwright": "playwright test", "test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright", "vscode:prepublish": "pnpm run build", diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts index 80d8851333..e59613bf88 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts @@ -51,6 +51,7 @@ function document(value: string, text: string): TextDocument & { setText(nextTex function createSubject(options: { pinned?: boolean } = {}): { middleware: BundledClientMiddleware + ownership: DocumentOwnershipCoordinator client: LanguageClient documents: Map diagnosticMessages: string[] @@ -99,6 +100,7 @@ function createSubject(options: { pinned?: boolean } = {}): { }) return { + ownership, middleware: createBundledClientMiddleware({ ownership, getClient: () => client, @@ -117,9 +119,10 @@ function createSubject(options: { pinned?: boolean } = {}): { } describe('bundled client ownership middleware', () => { - test('balances bundled lifecycle notifications without duplicate opens or closes', () => { - const { middleware, sendNotification, deleteDiagnostics } = createSubject() + test('balances bundled lifecycle notifications without duplicate opens or closes', async () => { + const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + await ownership.synchronize(schema) const didOpen = vi.fn() const didChange = vi.fn() const didClose = vi.fn() @@ -144,8 +147,9 @@ describe('bundled client ownership middleware', () => { }) test('resynchronizes an open document exactly once after the bundled client restarts', async () => { - const { middleware, client, sendNotification } = createSubject() + const { middleware, ownership, client, sendNotification } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + await ownership.synchronize(schema) const oldDidOpen = vi.fn() const oldCompletion = { label: 'id' } as CompletionItem @@ -189,9 +193,10 @@ describe('bundled client ownership middleware', () => { expect(sendNotification).not.toHaveBeenCalled() }) - test('closes and clears a bundled document immediately when it becomes marked', () => { - const { middleware, sendNotification, deleteDiagnostics } = createSubject() + test('suppresses a transition change without performing pre-commit lifecycle effects', async () => { + const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + await ownership.synchronize(schema) const didOpen = vi.fn() const didChange = vi.fn() @@ -202,25 +207,21 @@ describe('bundled client ownership middleware', () => { middleware.didChange?.(markedChange, didChange) expect(didChange).not.toHaveBeenCalled() - expect(sendNotification).toHaveBeenCalledOnce() - expect(sendNotification).toHaveBeenCalledWith('textDocument/didClose', { - textDocument: { uri: schema.uri.toString() }, - }) - expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) + expect(sendNotification).not.toHaveBeenCalled() + expect(deleteDiagnostics).not.toHaveBeenCalled() }) - test('reopens a reacquired bundled document with complete current text', () => { - const { middleware, sendNotification } = createSubject() + test('opens a coordinator-reacquired document with complete current text exactly once', async () => { + const { middleware, ownership, sendNotification } = createSubject() const schema = document('file:///workspace/schema.prisma', '// use prisma-next') - const didOpen = vi.fn() const didChange = vi.fn() - middleware.didOpen?.(schema, didOpen) schema.setText('model User {\n id Int @id\n name String\n}') + await ownership.synchronize(schema) + middleware.openDocument(schema) + middleware.openDocument(schema) const unmarkedChange = { document: schema } as unknown as TextDocumentChangeEvent - middleware.didChange?.(unmarkedChange, didChange) - expect(didOpen).not.toHaveBeenCalled() expect(didChange).not.toHaveBeenCalled() expect(sendNotification).toHaveBeenCalledOnce() expect(sendNotification).toHaveBeenCalledWith('textDocument/didOpen', { @@ -237,9 +238,10 @@ describe('bundled client ownership middleware', () => { expect(sendNotification).toHaveBeenCalledOnce() }) - test('forwards a real close for every URI still tracked by the bundled server', () => { - const { middleware, sendNotification, deleteDiagnostics } = createSubject() + test('forwards a real close for every URI still tracked by the bundled server', async () => { + const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + await ownership.synchronize(schema) const didOpen = vi.fn() const didChange = vi.fn() const didClose = vi.fn() @@ -254,22 +256,22 @@ describe('bundled client ownership middleware', () => { expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) const reopened = document('file:///workspace/reopened.prisma', 'model User { id Int @id }') + await ownership.synchronize(reopened) middleware.didOpen?.(reopened, didOpen) reopened.setText('// use prisma-next') middleware.didChange?.({ document: reopened } as unknown as TextDocumentChangeEvent, didChange) middleware.didClose?.(reopened, didClose) - expect(sendNotification).toHaveBeenCalledOnce() - expect(sendNotification).toHaveBeenCalledWith('textDocument/didClose', { - textDocument: { uri: reopened.uri.toString() }, - }) - expect(didClose).toHaveBeenCalledOnce() + expect(sendNotification).not.toHaveBeenCalled() + expect(didClose).toHaveBeenCalledTimes(2) + expect(didClose).toHaveBeenLastCalledWith(reopened) }) test('gates every advertised document request while preserving unmarked forwarding', async () => { - const { middleware } = createSubject() + const { middleware, ownership } = createSubject() const marked = document('file:///workspace/marked.prisma', '// use prisma-next') const unmarked = document('file:///workspace/unmarked.prisma', 'model User { id Int @id }') + await ownership.synchronize(unmarked) const completionItem = { label: 'id' } as CompletionItem const completionNext = vi.fn().mockReturnValue([completionItem]) @@ -311,9 +313,10 @@ describe('bundled client ownership middleware', () => { expect(middleware.provideRenameEdits?.(unmarked, position, 'Renamed', token, unmarkedNext)).toBe(forwarded) }) - test('clears diagnostics when a document loses bundled ownership', () => { - const { middleware, documents, diagnosticMessages } = createSubject() + test('clears diagnostics when a document loses bundled ownership', async () => { + const { middleware, ownership, documents, diagnosticMessages } = createSubject() const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + await ownership.synchronize(schema) documents.set(schema.uri.toString(), schema) const diagnostics = [{ message: 'bundled diagnostic' }] as Diagnostic[] const next = vi.fn() @@ -330,7 +333,7 @@ describe('bundled client ownership middleware', () => { }) test('keeps code-action conversion ownership-gated', async () => { - const { middleware, sendRequest, isSnippetEdit } = createSubject() + const { middleware, ownership, sendRequest, isSnippetEdit } = createSubject() sendRequest.mockResolvedValue([ { title: 'Insert block', @@ -339,6 +342,7 @@ describe('bundled client ownership middleware', () => { }, ] as never) const unmarked = document('file:///workspace/schema.prisma', 'model User { id Int @id }') + await ownership.synchronize(unmarked) const actions = await middleware.provideCodeActions?.(unmarked, range, codeActionContext, token, vi.fn()) @@ -361,9 +365,10 @@ describe('bundled client ownership middleware', () => { expect(sendRequest).toHaveBeenCalledOnce() }) - test('allows pinned marked documents to stay synchronized with the bundled client', () => { - const { middleware } = createSubject({ pinned: true }) + test('allows pinned marked documents to stay synchronized with the bundled client', async () => { + const { middleware, ownership } = createSubject({ pinned: true }) const schema = document('file:///workspace/schema.prisma', '// use prisma-next') + await ownership.synchronize(schema) const didOpen = vi.fn() const didChange = vi.fn() const change = { document: schema } as unknown as TextDocumentChangeEvent diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts index 3b6bf0e74f..59e88ef922 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts @@ -5,7 +5,7 @@ import type { TextDocumentIdentifier, } from 'vscode-languageclient' import type { LanguageClient, Middleware } from 'vscode-languageclient/node' -import { DocumentOwnershipCoordinator, type DocumentOwner } from './documentOwnership' +import { DocumentOwnershipCoordinator } from './documentOwnership' export interface BundledClientMiddlewareOptions { readonly ownership: DocumentOwnershipCoordinator @@ -16,6 +16,9 @@ export interface BundledClientMiddlewareOptions { } export interface BundledClientMiddleware extends Middleware { + openDocument(document: TextDocument): void + closeDocument(document: TextDocument): void + clearDiagnostics(uri: Uri): void resetClientState(): void } @@ -23,13 +26,12 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp let completionDocuments = new WeakMap() const bundledDocuments = new Set() - const ownerForDocument = (document: TextDocument): DocumentOwner => { - void options.ownership.synchronize(document) - return options.ownership.classify(document) + const isBundledDocument = (document: TextDocument): boolean => { + const committedOwner = options.ownership.getOwner(document.uri) + const currentOwner = options.ownership.classify(document) + return committedOwner.kind === 'bundled' && currentOwner.kind === 'bundled' } - const isBundledDocument = (document: TextDocument): boolean => ownerForDocument(document).kind === 'bundled' - const clearDiagnostics = (uri: Uri): void => { options.getClient().diagnostics?.delete(uri) } @@ -40,10 +42,12 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp bundledDocuments.add(documentUri) const client = options.getClient() - void client.sendNotification( - 'textDocument/didOpen', - client.code2ProtocolConverter.asOpenTextDocumentParams(document), - ) + try { + client.sendNotification('textDocument/didOpen', client.code2ProtocolConverter.asOpenTextDocumentParams(document)) + } catch (error) { + bundledDocuments.delete(documentUri) + throw error + } } const closeBundledDocument = (document: TextDocument): void => { @@ -51,41 +55,36 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp if (!bundledDocuments.delete(documentUri)) return const client = options.getClient() - void client.sendNotification( - 'textDocument/didClose', - client.code2ProtocolConverter.asCloseTextDocumentParams(document), - ) + try { + client.sendNotification( + 'textDocument/didClose', + client.code2ProtocolConverter.asCloseTextDocumentParams(document), + ) + } catch (error) { + bundledDocuments.add(documentUri) + throw error + } } const middleware: BundledClientMiddleware = { + openDocument: openBundledDocument, + closeDocument: closeBundledDocument, + clearDiagnostics, resetClientState: () => { bundledDocuments.clear() completionDocuments = new WeakMap() }, didOpen: (document, next) => { const documentUri = document.uri.toString() - if (isBundledDocument(document)) { - if (!bundledDocuments.has(documentUri)) { - bundledDocuments.add(documentUri) - next(document) - } - } else { - closeBundledDocument(document) - clearDiagnostics(document.uri) + if (isBundledDocument(document) && !bundledDocuments.has(documentUri)) { + bundledDocuments.add(documentUri) + next(document) } }, didChange: (event, next) => { const document = event.document - const documentUri = document.uri.toString() - if (isBundledDocument(document)) { - if (bundledDocuments.has(documentUri)) { - next(event) - } else { - openBundledDocument(document) - } - } else { - closeBundledDocument(document) - clearDiagnostics(document.uri) + if (isBundledDocument(document) && bundledDocuments.has(document.uri.toString())) { + next(event) } }, didClose: (document, next) => { @@ -97,8 +96,8 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp }, handleDiagnostics: (uri, diagnostics, next) => { const document = options.getDocument(uri) - const owner = document ? ownerForDocument(document) : options.ownership.getOwner(uri) - if (owner.kind !== 'bundled') { + const isOwned = document ? isBundledDocument(document) : options.ownership.getOwner(uri).kind === 'bundled' + if (!isOwned) { next(uri, []) return } diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts new file mode 100644 index 0000000000..d74f3cef84 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts @@ -0,0 +1,236 @@ +import { describe, expect, test, vi } from 'vitest' +import type { TextDocument, Uri, WorkspaceFolder } from 'vscode' +import { DocumentOwnershipCoordinator } from './documentOwnership' +import { + createPrepareDocumentRoutingCommit, + type BundledDocumentSynchronization, + type DocumentRoutingEvent, + type LocalDocumentSynchronization, +} from './documentRouting' + +const rootA = workspaceFolder('file:///workspace-a') +const rootB = workspaceFolder('file:///workspace-b') + +function uri(value: string): Uri { + return { scheme: value.slice(0, value.indexOf(':')), toString: () => value } as Uri +} + +function workspaceFolder(value: string): WorkspaceFolder { + return { uri: uri(value), name: value } as WorkspaceFolder +} + +function document(value: string, text: string): TextDocument & { setText(value: string): void } { + let currentText = text + return { + uri: uri(value), + languageId: 'prisma', + getText: () => currentText, + setText: (value) => { + currentText = value + }, + } as TextDocument & { setText(value: string): void } +} + +function deferred(): { promise: Promise; resolve(): void } { + let resolvePromise: (() => void) | undefined + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + return { promise, resolve: () => resolvePromise?.() } +} + +function createSubject(options: { localClose?: Promise; localStartup?: Promise } = {}) { + const active = new Set() + const opens: { owner: string; uri: string; text: string }[] = [] + const activeOwnerCountsAfterOpen: number[] = [] + const events: DocumentRoutingEvent[] = [] + const clearBundledDiagnostics = vi.fn() + const clearLocalDiagnostics = vi.fn() + const bundled: BundledDocumentSynchronization = { + openDocument: (schema) => { + active.add(`bundled:${schema.uri.toString()}`) + activeOwnerCountsAfterOpen.push([...active].filter((key) => key.endsWith(`:${schema.uri.toString()}`)).length) + opens.push({ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }) + }, + closeDocument: (schema) => { + active.delete(`bundled:${schema.uri.toString()}`) + }, + clearDiagnostics: clearBundledDiagnostics, + } + const ensureClientForDocument = vi.fn(async () => { + await options.localStartup + return {} + }) + const local: LocalDocumentSynchronization = { + ensureClientForDocument, + openDocument: (root, schema) => { + active.add(`local:${root}:${schema.uri.toString()}`) + activeOwnerCountsAfterOpen.push([...active].filter((key) => key.endsWith(`:${schema.uri.toString()}`)).length) + opens.push({ owner: root, uri: schema.uri.toString(), text: schema.getText() }) + return Promise.resolve() + }, + closeDocument: (root, schema) => + (options.localClose ?? Promise.resolve()).then(() => { + active.delete(`local:${root}:${schema.uri.toString()}`) + }), + clearDiagnostics: clearLocalDiagnostics, + } + const ownership: DocumentOwnershipCoordinator = new DocumentOwnershipCoordinator({ + workspace: { + isTrusted: true, + getWorkspaceFolder: (documentUri) => + documentUri.toString().includes('workspace-a') + ? rootA + : documentUri.toString().includes('workspace-b') + ? rootB + : undefined, + }, + policy: { isPinnedToPrisma6: () => false }, + prepareOwner: createPrepareDocumentRoutingCommit({ + getOwnership: (): DocumentOwnershipCoordinator => ownership, + getBundled: () => bundled, + getLocal: () => local, + observer: (event) => events.push(event), + }), + }) + return { + ownership, + bundled, + local, + ensureClientForDocument, + clearBundledDiagnostics, + clearLocalDiagnostics, + active, + activeOwnerCountsAfterOpen, + opens, + events, + } +} + +describe('document routing commits', () => { + test('transfers both directions with close-clear-open ordering and complete current text', async () => { + const subject = createSubject() + const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') + await subject.ownership.synchronize(schema) + subject.events.length = 0 + subject.opens.length = 0 + + schema.setText('// use prisma-next\nmodel User { id Int @id name String }') + await subject.ownership.synchronize(schema) + + expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) + expect(subject.opens).toEqual([ + { + owner: rootA.uri.toString(), + uri: schema.uri.toString(), + text: schema.getText(), + }, + ]) + expect(subject.active).toEqual(new Set([`local:${rootA.uri.toString()}:${schema.uri.toString()}`])) + expect(subject.activeOwnerCountsAfterOpen).toEqual([1, 1]) + expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) + + subject.events.length = 0 + subject.opens.length = 0 + schema.setText('model User { id Int @id email String }') + await subject.ownership.synchronize(schema) + + expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) + expect(subject.opens).toEqual([{ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }]) + expect(subject.active).toEqual(new Set([`bundled:${schema.uri.toString()}`])) + expect(subject.activeOwnerCountsAfterOpen).toEqual([1, 1, 1]) + expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) + }) + + test('awaits the prior close before clearing diagnostics or opening the next owner', async () => { + const close = deferred() + const subject = createSubject({ localClose: close.promise }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') + await subject.ownership.synchronize(schema) + subject.events.length = 0 + + schema.setText('model User { id Int @id }') + const transfer = subject.ownership.synchronize(schema) + await vi.waitFor(() => expect(subject.active).toContain(`local:${rootA.uri.toString()}:${schema.uri.toString()}`)) + + expect(subject.events).toEqual([]) + expect(subject.clearLocalDiagnostics).not.toHaveBeenCalled() + expect(subject.opens.filter(({ owner }) => owner === 'bundled')).toHaveLength(0) + + close.resolve() + await transfer + + expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) + }) + + test('does not open a stale local candidate when text changes during startup', async () => { + const startup = deferred() + const subject = createSubject({ localStartup: startup.promise }) + const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') + await subject.ownership.synchronize(schema) + subject.opens.length = 0 + + schema.setText('// use prisma-next\nmodel User { id Int @id }') + const staleLocal = subject.ownership.synchronize(schema) + await vi.waitFor(() => expect(subject.active.size).toBe(0)) + schema.setText('model User { id Int @id current String }') + const survivingBundled = subject.ownership.synchronize(schema) + startup.resolve() + await Promise.all([staleLocal, survivingBundled]) + + expect(subject.opens).toEqual([{ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }]) + expect(subject.ownership.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) + expect(subject.active).toEqual(new Set([`bundled:${schema.uri.toString()}`])) + }) + + test('repeated synchronization is idempotent for unchanged ownership', async () => { + const subject = createSubject() + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') + + await subject.ownership.synchronize(schema) + subject.events.length = 0 + subject.opens.length = 0 + await Promise.all([ + subject.ownership.synchronize(schema), + subject.ownership.synchronize(schema), + subject.ownership.synchronize(schema), + ]) + + expect(subject.events).toEqual([]) + expect(subject.opens).toEqual([]) + expect(subject.active).toEqual(new Set([`local:${rootA.uri.toString()}:${schema.uri.toString()}`])) + }) + + test('routes same-root schema files independently', async () => { + const subject = createSubject() + const marked = document('file:///workspace-a/marked.prisma', '// use prisma-next\nmodel A { id Int @id }') + const unmarked = document('file:///workspace-a/unmarked.prisma', 'model B { id Int @id }') + + await Promise.all([subject.ownership.synchronize(marked), subject.ownership.synchronize(unmarked)]) + unmarked.setText('// use prisma-next\nmodel B { id Int @id name String }') + await subject.ownership.synchronize(unmarked) + + expect(subject.active).toEqual( + new Set([ + `local:${rootA.uri.toString()}:${marked.uri.toString()}`, + `local:${rootA.uri.toString()}:${unmarked.uri.toString()}`, + ]), + ) + expect(subject.opens.filter(({ uri }) => uri === marked.uri.toString())).toHaveLength(1) + expect(subject.opens.filter(({ uri }) => uri === unmarked.uri.toString())).toHaveLength(2) + expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(unmarked.uri) + }) + + test('keeps local ownership isolated per document and workspace root', async () => { + const subject = createSubject() + const schemaA = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel A { id Int @id }') + const schemaB = document('file:///workspace-b/schema.prisma', '// use prisma-next\nmodel B { id Int @id }') + + await Promise.all([subject.ownership.synchronize(schemaA), subject.ownership.synchronize(schemaB)]) + + expect(subject.opens.map(({ owner, uri }) => ({ owner, uri }))).toEqual([ + { owner: rootA.uri.toString(), uri: schemaA.uri.toString() }, + { owner: rootB.uri.toString(), uri: schemaB.uri.toString() }, + ]) + }) +}) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts new file mode 100644 index 0000000000..bffd629226 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -0,0 +1,75 @@ +import type { TextDocument, Uri } from 'vscode' +import type { DocumentOwner, DocumentOwnershipCoordinator, PrepareDocumentOwnerCommit } from './documentOwnership' + +export interface BundledDocumentSynchronization { + openDocument(document: TextDocument): void + closeDocument(document: TextDocument): void + clearDiagnostics(uri: Uri): void +} + +export interface LocalDocumentSynchronization { + ensureClientForDocument(document: TextDocument): Promise + openDocument(workspaceFolderUri: string, document: TextDocument): Promise + closeDocument(workspaceFolderUri: string, document: TextDocument): Promise + clearDiagnostics(workspaceFolderUri: string, uri: Uri): Promise +} + +export type DocumentRoutingEvent = + | { readonly type: 'closed'; readonly owner: DocumentOwner; readonly documentUri: string } + | { readonly type: 'diagnosticsCleared'; readonly owner: DocumentOwner; readonly documentUri: string } + | { readonly type: 'opened'; readonly owner: DocumentOwner; readonly documentUri: string } + +export interface DocumentRoutingOptions { + readonly getOwnership: () => DocumentOwnershipCoordinator + readonly getBundled: () => BundledDocumentSynchronization + readonly getLocal: () => LocalDocumentSynchronization + readonly observer?: (event: DocumentRoutingEvent) => void +} + +export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptions): PrepareDocumentOwnerCommit { + return ({ document, previousOwner, nextOwner }) => { + if (documentOwnersEqual(previousOwner, nextOwner)) return undefined + + return async () => { + await closePreviousOwner(options, previousOwner, document) + + if (!documentOwnersEqual(options.getOwnership().classify(document), nextOwner)) return + + if (nextOwner.kind === 'bundled') { + options.getBundled().openDocument(document) + options.observer?.({ type: 'opened', owner: nextOwner, documentUri: document.uri.toString() }) + } else if (nextOwner.kind === 'local') { + const local = options.getLocal() + const client = await local.ensureClientForDocument(document) + if (client && documentOwnersEqual(options.getOwnership().classify(document), nextOwner)) { + await local.openDocument(nextOwner.workspaceFolderUri, document) + options.observer?.({ type: 'opened', owner: nextOwner, documentUri: document.uri.toString() }) + } + } + } + } +} + +async function closePreviousOwner( + options: DocumentRoutingOptions, + previousOwner: DocumentOwner, + document: TextDocument, +): Promise { + if (previousOwner.kind === 'bundled') { + options.getBundled().closeDocument(document) + options.observer?.({ type: 'closed', owner: previousOwner, documentUri: document.uri.toString() }) + options.getBundled().clearDiagnostics(document.uri) + options.observer?.({ type: 'diagnosticsCleared', owner: previousOwner, documentUri: document.uri.toString() }) + } else if (previousOwner.kind === 'local') { + const local = options.getLocal() + await local.closeDocument(previousOwner.workspaceFolderUri, document) + options.observer?.({ type: 'closed', owner: previousOwner, documentUri: document.uri.toString() }) + await local.clearDiagnostics(previousOwner.workspaceFolderUri, document.uri) + options.observer?.({ type: 'diagnosticsCleared', owner: previousOwner, documentUri: document.uri.toString() }) + } +} + +export function documentOwnersEqual(left: DocumentOwner, right: DocumentOwner): boolean { + if (left.kind !== right.kind) return false + return left.kind !== 'local' || (right.kind === 'local' && left.workspaceFolderUri === right.workspaceFolderUri) +} diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index e4222852c5..2041e13f55 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -22,7 +22,8 @@ import { CodelensProvider, generateClient } from '../../CodeLensProvider' import * as prisma6Handling from '../../prisma6Handling' import { getPackageJSON } from '../../getPackageJSON' import { DocumentOwnershipCoordinator } from './documentOwnership' -import { createBundledClientMiddleware } from './bundledClientMiddleware' +import { createBundledClientMiddleware, type BundledClientMiddleware } from './bundledClientMiddleware' +import { createPrepareDocumentRoutingCommit } from './documentRouting' import { LocalPrismaNextClientRegistry, localPrismaNextClientTestStateCommand } from './localPrismaNextClientRegistry' let client: LanguageClient @@ -106,8 +107,21 @@ const plugin: PrismaVSCodePlugin = { setGenerateWatcher(!!workspace.getConfiguration('prisma').get('fileWatcher')) + const ownership: DocumentOwnershipCoordinator = new DocumentOwnershipCoordinator({ + workspace, + policy: { + isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), + }, + prepareOwner: createPrepareDocumentRoutingCommit({ + getOwnership: (): DocumentOwnershipCoordinator => ownership, + getBundled: () => bundledClientMiddleware, + getLocal: () => localClients, + }), + }) const localClients = new LocalPrismaNextClientRegistry({ workspace, + ownership, + getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), createClient: (id, name, serverOptions, localClientOptions) => new LanguageClient(id, name, serverOptions, localClientOptions), registerDisposable: (disposable) => context.subscriptions.push(disposable), @@ -115,19 +129,8 @@ const plugin: PrismaVSCodePlugin = { console.error(`Failed to start Prisma Next Language Server for ${workspaceFolder.uri.toString()}`, error) }, }) - const ownership = new DocumentOwnershipCoordinator({ - workspace, - policy: { - isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), - }, - prepareOwner: async ({ document, nextOwner }) => { - if (nextOwner.kind === 'local') { - await localClients.ensureClientForDocument(document) - } - }, - }) - const bundledClientMiddleware = createBundledClientMiddleware({ + const bundledClientMiddleware: BundledClientMiddleware = createBundledClientMiddleware({ ownership, getClient: () => client, getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), @@ -147,8 +150,8 @@ const plugin: PrismaVSCodePlugin = { let started = false const needsLanguageServer = (doc: TextDocument): boolean => doc.languageId === 'prisma' && ownership.classify(doc).kind === 'bundled' - const prepareLocalClient = (document: TextDocument): void => { - if (document.languageId === 'prisma' && ownership.classify(document).kind === 'local') { + const synchronizeDocument = (document: TextDocument): void => { + if (document.languageId === 'prisma') { void ownership.synchronize(document) } } @@ -226,17 +229,17 @@ const plugin: PrismaVSCodePlugin = { workspace.onDidOpenTextDocument((document) => { maybeStart() - prepareLocalClient(document) + synchronizeDocument(document) }), workspace.onDidChangeTextDocument((event) => { maybeStart() - prepareLocalClient(event.document) + synchronizeDocument(event.document) }), ) maybeStart() for (const document of workspace.textDocuments) { - prepareLocalClient(document) + synchronizeDocument(document) } if (isDebugOrTest) { diff --git a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts new file mode 100644 index 0000000000..96e8f2f609 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, test, vi } from 'vitest' +import type { + CancellationToken, + CodeActionContext, + CompletionContext, + CompletionItem, + Diagnostic, + FormattingOptions, + Position, + Range, + TextDocument, + TextDocumentChangeEvent, + Uri, + WorkspaceFolder, +} from 'vscode' +import type { LanguageClient } from 'vscode-languageclient/node' +import { DocumentOwnershipCoordinator } from './documentOwnership' +import { createLocalClientMiddleware } from './localClientMiddleware' + +const rootA = workspaceFolder('file:///workspace-a') +const rootB = workspaceFolder('file:///workspace-b') +const position = {} as Position +const range = {} as Range +const token = {} as CancellationToken +const completionContext = {} as CompletionContext +const formattingOptions = {} as FormattingOptions +const codeActionContext = { diagnostics: [] } as unknown as CodeActionContext + +function uri(value: string): Uri { + return { scheme: value.slice(0, value.indexOf(':')), toString: () => value } as Uri +} + +function workspaceFolder(value: string): WorkspaceFolder { + return { uri: uri(value), name: value } as WorkspaceFolder +} + +function document(value: string, text: string): TextDocument & { setText(value: string): void } { + let currentText = text + return { + uri: uri(value), + languageId: 'prisma', + version: 7, + getText: () => currentText, + setText: (value) => { + currentText = value + }, + } as TextDocument & { setText(value: string): void } +} + +function createSubject() { + const documents = new Map() + const sendNotification = vi.fn() + const deleteDiagnostics = vi.fn() + const client = { + code2ProtocolConverter: { + asOpenTextDocumentParams: (schema: TextDocument) => ({ + textDocument: { + uri: schema.uri.toString(), + languageId: schema.languageId, + version: schema.version, + text: schema.getText(), + }, + }), + asCloseTextDocumentParams: (schema: TextDocument) => ({ textDocument: { uri: schema.uri.toString() } }), + }, + diagnostics: { delete: deleteDiagnostics }, + sendNotification, + } as unknown as LanguageClient + const ownership = new DocumentOwnershipCoordinator({ + workspace: { + isTrusted: true, + getWorkspaceFolder: (documentUri) => + documentUri.toString().includes('workspace-a') + ? rootA + : documentUri.toString().includes('workspace-b') + ? rootB + : undefined, + }, + policy: { isPinnedToPrisma6: () => false }, + }) + const middleware = createLocalClientMiddleware({ + workspaceFolderUri: rootA.uri.toString(), + ownership, + getClient: () => client, + getDocument: (documentUri) => documents.get(documentUri.toString()), + }) + return { middleware, ownership, documents, sendNotification, deleteDiagnostics } +} + +describe('local client ownership middleware', () => { + test('filters automatic initial synchronization and never sends unmarked contents', async () => { + const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') + const unmarked = document('file:///workspace-a/unmarked.prisma', 'model Secret { id Int @id }') + const automaticOpen = vi.fn() + const changeNext = vi.fn() + + middleware.didOpen?.(schema, automaticOpen) + middleware.didOpen?.(unmarked, automaticOpen) + expect(automaticOpen).not.toHaveBeenCalled() + + await ownership.synchronize(schema) + middleware.openDocument(schema) + expect(sendNotification).toHaveBeenCalledWith('textDocument/didOpen', { + textDocument: { + uri: schema.uri.toString(), + languageId: 'prisma', + version: 7, + text: schema.getText(), + }, + }) + + middleware.didChange?.({ document: schema } as unknown as TextDocumentChangeEvent, changeNext) + expect(changeNext).toHaveBeenCalledOnce() + + schema.setText('model User { id Int @id leaked String }') + middleware.didChange?.({ document: schema } as unknown as TextDocumentChangeEvent, changeNext) + middleware.closeDocument(schema) + middleware.clearDiagnostics(schema.uri) + + expect(changeNext).toHaveBeenCalledOnce() + expect(sendNotification).toHaveBeenLastCalledWith('textDocument/didClose', { + textDocument: { uri: schema.uri.toString() }, + }) + const notificationContents = sendNotification.mock.calls.flatMap(([, params]) => JSON.stringify(params)) + expect(notificationContents).not.toContain('leaked') + expect(notificationContents).not.toContain('Secret') + expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) + }) + + test('forwards every document feature only for committed ownership in the exact root', async () => { + const { middleware, ownership, documents } = createSubject() + const owned = document('file:///workspace-a/schema.prisma', '// use prisma-next') + const otherRoot = document('file:///workspace-b/schema.prisma', '// use prisma-next') + documents.set(owned.uri.toString(), owned) + await Promise.all([ownership.synchronize(owned), ownership.synchronize(otherRoot)]) + + const completion = { label: 'id' } as CompletionItem + const completionNext = vi.fn().mockReturnValue([completion]) + await expect( + middleware.provideCompletionItem?.(owned, position, completionContext, token, completionNext), + ).resolves.toEqual([completion]) + const resolveNext = vi.fn().mockReturnValue(completion) + expect(middleware.resolveCompletionItem?.(completion, token, resolveNext)).toBe(completion) + + const next = vi.fn().mockReturnValue('forwarded') + expect(middleware.provideHover?.(owned, position, token, next)).toBe('forwarded') + expect(middleware.provideDefinition?.(owned, position, token, next)).toBe('forwarded') + expect(middleware.provideReferences?.(owned, position, { includeDeclaration: true }, token, next)).toBe('forwarded') + expect(middleware.provideDocumentSymbols?.(owned, token, next)).toBe('forwarded') + expect(middleware.provideDocumentFormattingEdits?.(owned, formattingOptions, token, next)).toBe('forwarded') + expect(middleware.provideRenameEdits?.(owned, position, 'Renamed', token, next)).toBe('forwarded') + expect(middleware.provideCodeActions?.(owned, range, codeActionContext, token, next)).toBe('forwarded') + + const rejected = vi.fn() + expect(middleware.provideHover?.(otherRoot, position, token, rejected)).toBeUndefined() + owned.setText('model User { id Int @id }') + expect(middleware.provideDefinition?.(owned, position, token, rejected)).toBeUndefined() + expect(rejected).not.toHaveBeenCalled() + }) + + test('filters diagnostics outside exact committed ownership', async () => { + const { middleware, ownership, documents } = createSubject() + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') + documents.set(schema.uri.toString(), schema) + const next = vi.fn() + + const beforeOwnership = [{ message: 'before ownership' }] as Diagnostic[] + const owned = [{ message: 'owned' }] as Diagnostic[] + const stale = [{ message: 'stale' }] as Diagnostic[] + + middleware.handleDiagnostics?.(schema.uri, beforeOwnership, next) + await ownership.synchronize(schema) + middleware.handleDiagnostics?.(schema.uri, owned, next) + schema.setText('model User { id Int @id }') + middleware.handleDiagnostics?.(schema.uri, stale, next) + + expect(next.mock.calls).toEqual([ + [schema.uri, []], + [schema.uri, owned], + [schema.uri, []], + ]) + }) +}) diff --git a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts new file mode 100644 index 0000000000..aa5af3b736 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts @@ -0,0 +1,138 @@ +import type { CompletionItem, CompletionList, ProviderResult, TextDocument, Uri } from 'vscode' +import type { LanguageClient, Middleware } from 'vscode-languageclient/node' +import type { DocumentOwnershipCoordinator } from './documentOwnership' + +export interface LocalClientMiddlewareOptions { + readonly workspaceFolderUri: string + readonly ownership: DocumentOwnershipCoordinator + readonly getClient: () => LanguageClient + readonly getDocument: (uri: Uri) => TextDocument | undefined +} + +export interface LocalClientMiddleware extends Middleware { + openDocument(document: TextDocument): void + closeDocument(document: TextDocument): void + clearDiagnostics(uri: Uri): void +} + +export function createLocalClientMiddleware(options: LocalClientMiddlewareOptions): LocalClientMiddleware { + const synchronizedDocuments = new Set() + const completionDocuments = new WeakMap() + + const isOwnedDocument = (document: TextDocument): boolean => { + const committedOwner = options.ownership.getOwner(document.uri) + const currentOwner = options.ownership.classify(document) + return ( + committedOwner.kind === 'local' && + currentOwner.kind === 'local' && + committedOwner.workspaceFolderUri === options.workspaceFolderUri && + currentOwner.workspaceFolderUri === options.workspaceFolderUri + ) + } + + const clearDiagnostics = (uri: Uri): void => { + options.getClient().diagnostics?.delete(uri) + } + + const openDocument = (document: TextDocument): void => { + const documentUri = document.uri.toString() + if (synchronizedDocuments.has(documentUri)) return + + synchronizedDocuments.add(documentUri) + const client = options.getClient() + try { + client.sendNotification('textDocument/didOpen', client.code2ProtocolConverter.asOpenTextDocumentParams(document)) + } catch (error) { + synchronizedDocuments.delete(documentUri) + throw error + } + } + + const closeDocument = (document: TextDocument): void => { + const documentUri = document.uri.toString() + if (!synchronizedDocuments.delete(documentUri)) return + + const client = options.getClient() + try { + client.sendNotification( + 'textDocument/didClose', + client.code2ProtocolConverter.asCloseTextDocumentParams(document), + ) + } catch (error) { + synchronizedDocuments.add(documentUri) + throw error + } + } + + const middleware: LocalClientMiddleware = { + openDocument, + closeDocument, + clearDiagnostics, + didOpen: (document, next) => { + const documentUri = document.uri.toString() + if (isOwnedDocument(document) && !synchronizedDocuments.has(documentUri)) { + synchronizedDocuments.add(documentUri) + next(document) + } + }, + didChange: (event, next) => { + if (isOwnedDocument(event.document) && synchronizedDocuments.has(event.document.uri.toString())) { + next(event) + } + }, + didClose: (document, next) => { + if (synchronizedDocuments.delete(document.uri.toString())) { + next(document) + } + clearDiagnostics(document.uri) + }, + handleDiagnostics: (uri, diagnostics, next) => { + const document = options.getDocument(uri) + if (!document || !isOwnedDocument(document)) { + next(uri, []) + return + } + next(uri, diagnostics) + }, + provideCompletionItem: (document, position, context, token, next) => { + if (!isOwnedDocument(document)) return undefined + return mapProviderResult(next(document, position, context, token), (result) => { + for (const item of completionItems(result)) { + completionDocuments.set(item, document) + } + return result + }) + }, + resolveCompletionItem: (item, token, next) => { + const document = completionDocuments.get(item) + return document && isOwnedDocument(document) ? next(item, token) : undefined + }, + provideHover: (document, position, token, next) => + isOwnedDocument(document) ? next(document, position, token) : undefined, + provideDefinition: (document, position, token, next) => + isOwnedDocument(document) ? next(document, position, token) : undefined, + provideReferences: (document, position, context, token, next) => + isOwnedDocument(document) ? next(document, position, context, token) : undefined, + provideDocumentSymbols: (document, token, next) => (isOwnedDocument(document) ? next(document, token) : undefined), + provideDocumentFormattingEdits: (document, formattingOptions, token, next) => + isOwnedDocument(document) ? next(document, formattingOptions, token) : undefined, + provideRenameEdits: (document, position, newName, token, next) => + isOwnedDocument(document) ? next(document, position, newName, token) : undefined, + provideCodeActions: (document, range, context, token, next) => + isOwnedDocument(document) ? next(document, range, context, token) : undefined, + } + + return middleware +} + +function completionItems(result: CompletionItem[] | CompletionList | undefined | null): CompletionItem[] { + if (!result) return [] + return Array.isArray(result) ? result : result.items +} + +function mapProviderResult( + result: ProviderResult, + map: (value: T | undefined | null) => T | undefined | null, +): ProviderResult { + return Promise.resolve(result).then(map) +} diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts index 0829215681..cb916ef5dd 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts @@ -6,6 +6,8 @@ import { describe, expect, test, vi } from 'vitest' import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' +import { DocumentOwnershipCoordinator } from './documentOwnership' +import type { LocalClientMiddleware } from './localClientMiddleware' import { createExtensionHostNodeEnvironment, createLocalPrismaNextClientOptions, @@ -17,6 +19,14 @@ import { const rootA = workspaceFolder('file:///workspace-a', '/workspace-a', 'workspace-a') const rootB = workspaceFolder('file:///workspace-b', '/workspace-b', 'workspace-b') +const ownership = new DocumentOwnershipCoordinator({ + workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, + policy: { isPinnedToPrisma6: () => false }, +}) +const registryRoutingOptions = { + ownership, + getDocument: () => undefined, +} function uri(value: string, fsPath = value): Uri { return { @@ -158,8 +168,22 @@ describe('LocalPrismaNextClientRegistry', () => { expect(child.listenerCount('spawn')).toBe(0) }) - test('keeps local document synchronization disabled until owner middleware is attached', () => { - expect(createLocalPrismaNextClientOptions(rootA)).toEqual({ documentSelector: [], workspaceFolder: rootA }) + test('constrains provider registration to the matching root', () => { + const middleware = {} as LocalClientMiddleware + expect(createLocalPrismaNextClientOptions(rootA, middleware)).toEqual({ + documentSelector: [{ language: 'prisma', scheme: 'file', pattern: '/workspace-a/**/*' }], + workspaceFolder: rootA, + middleware, + }) + }) + + test('uses a relative root selector for Windows workspace paths', () => { + const windowsRoot = workspaceFolder('file:///C:/workspace-a', 'C:\\workspace-a', 'workspace-a') + const middleware = {} as LocalClientMiddleware + + expect(createLocalPrismaNextClientOptions(windowsRoot, middleware).documentSelector).toEqual([ + { language: 'prisma', scheme: 'file', pattern: 'C:/workspace-a/**/*' }, + ]) }) test('publishes pending startup per root and starts independent clients', async () => { @@ -183,6 +207,7 @@ describe('LocalPrismaNextClientRegistry', () => { ) const registerDisposable = vi.fn() const registry = new LocalPrismaNextClientRegistry({ + ...registryRoutingOptions, workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, entrypointExists, createClient, @@ -236,6 +261,7 @@ describe('LocalPrismaNextClientRegistry', () => { const createClient = vi.fn() const handleStartError = vi.fn() const registry = new LocalPrismaNextClientRegistry({ + ...registryRoutingOptions, workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, entrypointExists, createClient, @@ -266,6 +292,7 @@ describe('LocalPrismaNextClientRegistry', () => { const entrypointExists = vi.fn().mockResolvedValue(true) const createClient = vi.fn() const registry = new LocalPrismaNextClientRegistry({ + ...registryRoutingOptions, workspace: { isTrusted: trusted, getWorkspaceFolder: matchingWorkspaceFolder }, entrypointExists, createClient, @@ -284,6 +311,7 @@ describe('LocalPrismaNextClientRegistry', () => { const handleStartError = vi.fn() const createClient = vi.fn().mockReturnValue(client) const registry = new LocalPrismaNextClientRegistry({ + ...registryRoutingOptions, workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, entrypointExists: vi.fn().mockResolvedValue(true), createClient, diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index 53bedfe986..c8b32ee701 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -4,6 +4,8 @@ import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStd import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' +import type { DocumentOwnershipCoordinator } from './documentOwnership' +import { createLocalClientMiddleware, type LocalClientMiddleware } from './localClientMiddleware' const prismaCliRelativePath = ['node_modules', 'prisma', 'dist', 'prisma.js'] as const @@ -29,6 +31,8 @@ export interface LocalPrismaNextLauncherOptions { export interface LocalPrismaNextClientRegistryOptions { readonly workspace: LocalPrismaNextClientRegistryWorkspace + readonly ownership: DocumentOwnershipCoordinator + readonly getDocument: (uri: Uri) => TextDocument | undefined readonly createClient: ( id: string, name: string, @@ -45,9 +49,14 @@ export interface LocalPrismaNextClientTestState { readonly startedWorkspaceFolderUris: readonly string[] } +interface LocalPrismaNextClientEntry { + readonly client: LanguageClient + readonly middleware: LocalClientMiddleware +} + export class LocalPrismaNextClientRegistry { - private readonly clients = new Map>() - private readonly startedClients = new Map() + private readonly clients = new Map>() + private readonly startedClients = new Map() constructor(private readonly options: LocalPrismaNextClientRegistryOptions) {} @@ -61,7 +70,22 @@ export class LocalPrismaNextClientRegistry { return Promise.resolve(undefined) } - return this.ensureClient(workspaceFolder) + return this.ensureClient(workspaceFolder).then((entry) => entry?.client) + } + + async openDocument(workspaceFolderUri: string, document: TextDocument): Promise { + const entry = await this.clients.get(workspaceFolderUri) + entry?.middleware.openDocument(document) + } + + async closeDocument(workspaceFolderUri: string, document: TextDocument): Promise { + const entry = await this.clients.get(workspaceFolderUri) + entry?.middleware.closeDocument(document) + } + + async clearDiagnostics(workspaceFolderUri: string, uri: Uri): Promise { + const entry = await this.clients.get(workspaceFolderUri) + entry?.middleware.clearDiagnostics(uri) } getTestState(): LocalPrismaNextClientTestState { @@ -70,7 +94,7 @@ export class LocalPrismaNextClientRegistry { } } - private ensureClient(workspaceFolder: WorkspaceFolder): Promise { + private ensureClient(workspaceFolder: WorkspaceFolder): Promise { const workspaceFolderUri = workspaceFolder.uri.toString() const existing = this.clients.get(workspaceFolderUri) if (existing) { @@ -82,7 +106,7 @@ export class LocalPrismaNextClientRegistry { return pending } - private async discoverAndStart(workspaceFolder: WorkspaceFolder): Promise { + private async discoverAndStart(workspaceFolder: WorkspaceFolder): Promise { const entrypoint = getLocalPrismaNextEntrypoint(workspaceFolder) try { @@ -92,6 +116,12 @@ export class LocalPrismaNextClientRegistry { } const workspaceFolderUri = workspaceFolder.uri.toString() + const middleware = createLocalClientMiddleware({ + workspaceFolderUri, + ownership: this.options.ownership, + getClient: () => client, + getDocument: this.options.getDocument, + }) const client = this.options.createClient( `prisma-next:${workspaceFolderUri}`, `Prisma Next Language Server (${workspaceFolder.name})`, @@ -99,12 +129,13 @@ export class LocalPrismaNextClientRegistry { ...this.options.launcher, handleProcessError: (error) => this.options.handleStartError?.(workspaceFolder, error), }), - createLocalPrismaNextClientOptions(workspaceFolder), + createLocalPrismaNextClientOptions(workspaceFolder, middleware), ) this.options.registerDisposable(client.start()) await client.onReady() - this.startedClients.set(workspaceFolderUri, client) - return client + const entry = { client, middleware } + this.startedClients.set(workspaceFolderUri, entry) + return entry } catch (error) { this.options.handleStartError?.(workspaceFolder, error) return undefined @@ -195,11 +226,16 @@ function destroyProcessStreams(child: ChildProcessWithoutNullStreams): void { child.stderr.destroy() } -export function createLocalPrismaNextClientOptions(workspaceFolder: WorkspaceFolder): LanguageClientOptions { +export function createLocalPrismaNextClientOptions( + workspaceFolder: WorkspaceFolder, + middleware: LocalClientMiddleware, +): LanguageClientOptions { + const rootPath = workspaceFolder.uri.fsPath.split('\\').join('/') + const normalizedRoot = rootPath.endsWith('/') ? rootPath.slice(0, -1) : rootPath return { - // Synchronization stays disabled until owner-filtered local middleware is attached. - documentSelector: [], + documentSelector: [{ language: 'prisma', scheme: 'file', pattern: `${normalizedRoot}/**/*` }], workspaceFolder, + middleware, } } From a2a44d2d8d55e89829483a53f543951aba322bb1 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 10:43:26 +0000 Subject: [PATCH 11/43] fix(vscode): cancel routing for closed documents --- .../bundledClientMiddleware.test.ts | 4 + .../bundledClientMiddleware.ts | 4 +- .../documentOwnership.test.ts | 30 +++++++ .../documentOwnership.ts | 89 +++++++++++++------ .../documentRouting.test.ts | 86 ++++++++++++++++-- .../prisma-language-server/documentRouting.ts | 21 +++-- .../plugins/prisma-language-server/index.ts | 6 ++ .../localPrismaNextClientRegistry.test.ts | 26 ++++++ .../localPrismaNextClientRegistry.ts | 8 +- 9 files changed, 232 insertions(+), 42 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts index e59613bf88..f9521cf230 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts @@ -324,10 +324,14 @@ describe('bundled client ownership middleware', () => { middleware.handleDiagnostics?.(schema.uri, diagnostics, next) schema.setText('// use prisma-next') middleware.handleDiagnostics?.(schema.uri, diagnostics, next) + schema.setText('model User { id Int @id }') + documents.delete(schema.uri.toString()) + middleware.handleDiagnostics?.(schema.uri, diagnostics, next) expect(next.mock.calls).toEqual([ [schema.uri, diagnostics], [schema.uri, []], + [schema.uri, []], ]) expect(diagnosticMessages).toEqual(['bundled diagnostic']) }) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts index 59e88ef922..ee274303e5 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts @@ -88,7 +88,6 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp } }, didClose: (document, next) => { - void options.ownership.synchronize(document) if (bundledDocuments.delete(document.uri.toString())) { next(document) } @@ -96,8 +95,7 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp }, handleDiagnostics: (uri, diagnostics, next) => { const document = options.getDocument(uri) - const isOwned = document ? isBundledDocument(document) : options.ownership.getOwner(uri).kind === 'bundled' - if (!isOwned) { + if (!document || !isBundledDocument(document)) { next(uri, []) return } diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts index f623c10ace..4e1d30344d 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts @@ -263,6 +263,36 @@ describe('DocumentOwnershipCoordinator', () => { expect(maximumActiveCommits).toBe(1) }) + test('close invalidates pending preparation and serializes final unowned cleanup', async () => { + const preparation = deferred() + const committedOwners: DocumentOwner[] = [] + let blockLocalPreparation = false + const subject = coordinator({ + prepareOwner: async (transition) => { + if (blockLocalPreparation && transition.nextOwner.kind === 'local') { + await preparation.promise + } + return () => { + committedOwners.push(transition.nextOwner) + } + }, + }) + const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') + await subject.synchronize(schema) + committedOwners.length = 0 + + blockLocalPreparation = true + schema.setText('// use prisma-next') + const transfer = subject.synchronize(schema) + await Promise.resolve() + const closing = subject.close(schema) + preparation.resolve() + await Promise.all([transfer, closing]) + + expect(committedOwners).toEqual([{ kind: 'unowned' }]) + expect(subject.getOwner(schema.uri)).toEqual({ kind: 'unowned' }) + }) + test('discards stale asynchronous work after a newer transition', async () => { const gate = deferred() const events: DocumentOwnershipTestEvent[] = [] diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index 67358d19a5..a2613c2c50 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -90,16 +90,24 @@ export class DocumentOwnershipCoordinator { } synchronize(document: TextDocument): Promise { - const documentUri = document.uri.toString() - const state = this.getOrCreateState(documentUri) - const revision = ++state.revision + return this.enqueue(document, (state, revision) => this.commitCurrentOwner(document, state, revision)) + } + + close(document: TextDocument): Promise { + return this.enqueue(document, (state, revision) => this.commitClosedOwner(document, state, revision)) + } - const operation = state.pending.then(() => this.commitCurrentOwner(document, state, revision)) + private enqueue( + document: TextDocument, + commit: (state: DocumentOwnershipState, revision: number) => Promise, + ): Promise { + const state = this.getOrCreateState(document.uri.toString()) + const revision = ++state.revision + const operation = state.pending.then(() => commit(state, revision)) state.pending = operation.then( () => undefined, () => undefined, ) - return operation } @@ -118,6 +126,38 @@ export class DocumentOwnershipCoordinator { return state } + private async commitClosedOwner( + document: TextDocument, + state: DocumentOwnershipState, + revision: number, + ): Promise { + const documentUri = document.uri.toString() + if (revision !== state.revision) { + this.observeStaleTransition(documentUri, revision, state.owner) + return state.owner + } + + const commitOwner = await this.options.prepareOwner?.({ + document, + previousOwner: state.owner, + nextOwner: unownedOwner, + revision, + }) + if (revision !== state.revision) { + this.observeStaleTransition(documentUri, revision, state.owner) + return state.owner + } + + if (commitOwner) { + await commitOwner() + } + + const previousOwner = state.owner + state.owner = unownedOwner + this.observeOwnerChange(documentUri, revision, previousOwner, unownedOwner) + return unownedOwner + } + private async commitCurrentOwner( document: TextDocument, state: DocumentOwnershipState, @@ -135,12 +175,7 @@ export class DocumentOwnershipCoordinator { }) if (revision !== state.revision) { - this.options.testObserver?.({ - type: 'staleTransitionDiscarded', - documentUri, - revision, - owner: state.owner, - }) + this.observeStaleTransition(documentUri, revision, state.owner) return state.owner } @@ -155,26 +190,28 @@ export class DocumentOwnershipCoordinator { const previousOwner = state.owner state.owner = currentOwner - if (!ownersEqual(previousOwner, currentOwner)) { - this.options.testObserver?.({ - type: 'ownerChanged', - documentUri, - revision, - previousOwner, - owner: currentOwner, - }) - } + this.observeOwnerChange(documentUri, revision, previousOwner, currentOwner) return currentOwner } - this.options.testObserver?.({ - type: 'staleTransitionDiscarded', - documentUri, - revision, - owner: state.owner, - }) + this.observeStaleTransition(documentUri, revision, state.owner) return state.owner } + + private observeOwnerChange( + documentUri: string, + revision: number, + previousOwner: DocumentOwner, + owner: DocumentOwner, + ): void { + if (!ownersEqual(previousOwner, owner)) { + this.options.testObserver?.({ type: 'ownerChanged', documentUri, revision, previousOwner, owner }) + } + } + + private observeStaleTransition(documentUri: string, revision: number, owner: DocumentOwner): void { + this.options.testObserver?.({ type: 'staleTransitionDiscarded', documentUri, revision, owner }) + } } function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts index d74f3cef84..8e12079790 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts @@ -41,6 +41,8 @@ function deferred(): { promise: Promise; resolve(): void } { function createSubject(options: { localClose?: Promise; localStartup?: Promise } = {}) { const active = new Set() + const closedDocumentUris = new Set() + const protocolCloses: { owner: 'bundled' | 'local'; uri: string }[] = [] const opens: { owner: string; uri: string; text: string }[] = [] const activeOwnerCountsAfterOpen: number[] = [] const events: DocumentRoutingEvent[] = [] @@ -53,7 +55,9 @@ function createSubject(options: { localClose?: Promise; localStartup?: Pro opens.push({ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }) }, closeDocument: (schema) => { - active.delete(`bundled:${schema.uri.toString()}`) + if (active.delete(`bundled:${schema.uri.toString()}`)) { + protocolCloses.push({ owner: 'bundled', uri: schema.uri.toString() }) + } }, clearDiagnostics: clearBundledDiagnostics, } @@ -61,18 +65,22 @@ function createSubject(options: { localClose?: Promise; localStartup?: Pro await options.localStartup return {} }) + const closeLocalDocument = vi.fn((root: string, schema: TextDocument) => + (options.localClose ?? Promise.resolve()).then(() => { + if (active.delete(`local:${root}:${schema.uri.toString()}`)) { + protocolCloses.push({ owner: 'local', uri: schema.uri.toString() }) + } + }), + ) const local: LocalDocumentSynchronization = { ensureClientForDocument, openDocument: (root, schema) => { active.add(`local:${root}:${schema.uri.toString()}`) activeOwnerCountsAfterOpen.push([...active].filter((key) => key.endsWith(`:${schema.uri.toString()}`)).length) opens.push({ owner: root, uri: schema.uri.toString(), text: schema.getText() }) - return Promise.resolve() + return Promise.resolve(true) }, - closeDocument: (root, schema) => - (options.localClose ?? Promise.resolve()).then(() => { - active.delete(`local:${root}:${schema.uri.toString()}`) - }), + closeDocument: closeLocalDocument, clearDiagnostics: clearLocalDiagnostics, } const ownership: DocumentOwnershipCoordinator = new DocumentOwnershipCoordinator({ @@ -88,16 +96,34 @@ function createSubject(options: { localClose?: Promise; localStartup?: Pro policy: { isPinnedToPrisma6: () => false }, prepareOwner: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, + isDocumentOpen: (schema) => !closedDocumentUris.has(schema.uri.toString()), getBundled: () => bundled, getLocal: () => local, observer: (event) => events.push(event), }), }) + const closeEditorDocument = (schema: TextDocument): void => { + const documentUri = schema.uri.toString() + closedDocumentUris.add(documentUri) + if (active.delete(`bundled:${documentUri}`)) { + protocolCloses.push({ owner: 'bundled', uri: documentUri }) + clearBundledDiagnostics(schema.uri) + } + const root = documentUri.includes('workspace-a') ? rootA : rootB + if (active.delete(`local:${root.uri.toString()}:${documentUri}`)) { + protocolCloses.push({ owner: 'local', uri: documentUri }) + clearLocalDiagnostics(root.uri.toString(), schema.uri) + } + } + return { ownership, bundled, local, ensureClientForDocument, + closeLocalDocument, + closeEditorDocument, + protocolCloses, clearBundledDiagnostics, clearLocalDiagnostics, active, @@ -163,6 +189,54 @@ describe('document routing commits', () => { expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) }) + test('does not reopen locally when the editor closes during local startup', async () => { + const startup = deferred() + const subject = createSubject({ localStartup: startup.promise }) + const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') + await subject.ownership.synchronize(schema) + subject.opens.length = 0 + + schema.setText('// use prisma-next\nmodel User { id Int @id }') + const transfer = subject.ownership.synchronize(schema) + await vi.waitFor(() => expect(subject.ensureClientForDocument).toHaveBeenCalledOnce()) + + subject.closeEditorDocument(schema) + const closing = subject.ownership.close(schema) + startup.resolve() + await Promise.all([transfer, closing]) + + expect(subject.opens).toEqual([]) + expect(subject.active).toEqual(new Set()) + expect(subject.protocolCloses).toEqual([{ owner: 'bundled', uri: schema.uri.toString() }]) + expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) + expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) + expect(subject.ownership.getOwner(schema.uri)).toEqual({ kind: 'unowned' }) + }) + + test('does not reopen bundled when the editor closes during a delayed prior-owner close', async () => { + const close = deferred() + const subject = createSubject({ localClose: close.promise }) + const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') + await subject.ownership.synchronize(schema) + subject.opens.length = 0 + + schema.setText('model User { id Int @id }') + const transfer = subject.ownership.synchronize(schema) + await vi.waitFor(() => expect(subject.closeLocalDocument).toHaveBeenCalledOnce()) + + subject.closeEditorDocument(schema) + const closing = subject.ownership.close(schema) + close.resolve() + await Promise.all([transfer, closing]) + + expect(subject.opens).toEqual([]) + expect(subject.active).toEqual(new Set()) + expect(subject.protocolCloses).toEqual([{ owner: 'local', uri: schema.uri.toString() }]) + expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) + expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) + expect(subject.ownership.getOwner(schema.uri)).toEqual({ kind: 'unowned' }) + }) + test('does not open a stale local candidate when text changes during startup', async () => { const startup = deferred() const subject = createSubject({ localStartup: startup.promise }) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index bffd629226..cd322e0626 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -9,7 +9,7 @@ export interface BundledDocumentSynchronization { export interface LocalDocumentSynchronization { ensureClientForDocument(document: TextDocument): Promise - openDocument(workspaceFolderUri: string, document: TextDocument): Promise + openDocument(workspaceFolderUri: string, document: TextDocument): Promise closeDocument(workspaceFolderUri: string, document: TextDocument): Promise clearDiagnostics(workspaceFolderUri: string, uri: Uri): Promise } @@ -21,6 +21,7 @@ export type DocumentRoutingEvent = export interface DocumentRoutingOptions { readonly getOwnership: () => DocumentOwnershipCoordinator + readonly isDocumentOpen: (document: TextDocument) => boolean readonly getBundled: () => BundledDocumentSynchronization readonly getLocal: () => LocalDocumentSynchronization readonly observer?: (event: DocumentRoutingEvent) => void @@ -33,7 +34,7 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio return async () => { await closePreviousOwner(options, previousOwner, document) - if (!documentOwnersEqual(options.getOwnership().classify(document), nextOwner)) return + if (!isCurrentOpenCandidate(options, document, nextOwner)) return if (nextOwner.kind === 'bundled') { options.getBundled().openDocument(document) @@ -41,9 +42,11 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio } else if (nextOwner.kind === 'local') { const local = options.getLocal() const client = await local.ensureClientForDocument(document) - if (client && documentOwnersEqual(options.getOwnership().classify(document), nextOwner)) { - await local.openDocument(nextOwner.workspaceFolderUri, document) - options.observer?.({ type: 'opened', owner: nextOwner, documentUri: document.uri.toString() }) + if (client && isCurrentOpenCandidate(options, document, nextOwner)) { + const opened = await local.openDocument(nextOwner.workspaceFolderUri, document) + if (opened && isCurrentOpenCandidate(options, document, nextOwner)) { + options.observer?.({ type: 'opened', owner: nextOwner, documentUri: document.uri.toString() }) + } } } } @@ -69,6 +72,14 @@ async function closePreviousOwner( } } +function isCurrentOpenCandidate( + options: DocumentRoutingOptions, + document: TextDocument, + candidate: DocumentOwner, +): boolean { + return options.isDocumentOpen(document) && documentOwnersEqual(options.getOwnership().classify(document), candidate) +} + export function documentOwnersEqual(left: DocumentOwner, right: DocumentOwner): boolean { if (left.kind !== right.kind) return false return left.kind !== 'local' || (right.kind === 'local' && left.workspaceFolderUri === right.workspaceFolderUri) diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 2041e13f55..b396779ec4 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -114,6 +114,7 @@ const plugin: PrismaVSCodePlugin = { }, prepareOwner: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, + isDocumentOpen: (document) => workspace.textDocuments.includes(document), getBundled: () => bundledClientMiddleware, getLocal: () => localClients, }), @@ -235,6 +236,11 @@ const plugin: PrismaVSCodePlugin = { maybeStart() synchronizeDocument(event.document) }), + workspace.onDidCloseTextDocument((document) => { + if (document.languageId === 'prisma') { + void ownership.close(document) + } + }), ) maybeStart() diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts index cb916ef5dd..ea5f3a7a63 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts @@ -186,6 +186,32 @@ describe('LocalPrismaNextClientRegistry', () => { ]) }) + test('does not synchronize a document that closes while its client entry is pending', async () => { + const ready = deferred() + const schema = document('file:///workspace-a/schema.prisma') + const documents = new Map([[schema.uri.toString(), schema]]) + const client = fakeClient( + 'root-a', + vi.fn(() => ready.promise), + ) + const registry = new LocalPrismaNextClientRegistry({ + ownership, + getDocument: (documentUri) => documents.get(documentUri.toString()), + workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, + entrypointExists: vi.fn().mockResolvedValue(true), + createClient: vi.fn().mockReturnValue(client), + registerDisposable: vi.fn(), + }) + + const startup = registry.ensureClientForDocument(schema) + const open = registry.openDocument(rootA.uri.toString(), schema) + documents.delete(schema.uri.toString()) + ready.resolve(undefined) + + await expect(startup).resolves.toBe(client) + await expect(open).resolves.toBe(false) + }) + test('publishes pending startup per root and starts independent clients', async () => { const discovery = deferred() const entrypointExists = vi.fn().mockReturnValue(discovery.promise) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index c8b32ee701..062409d27c 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -73,9 +73,13 @@ export class LocalPrismaNextClientRegistry { return this.ensureClient(workspaceFolder).then((entry) => entry?.client) } - async openDocument(workspaceFolderUri: string, document: TextDocument): Promise { + async openDocument(workspaceFolderUri: string, document: TextDocument): Promise { const entry = await this.clients.get(workspaceFolderUri) - entry?.middleware.openDocument(document) + if (!entry || this.options.getDocument(document.uri) !== document) { + return false + } + entry.middleware.openDocument(document) + return true } async closeDocument(workspaceFolderUri: string, document: TextDocument): Promise { From 038dc7e1c12144f7d6581838482f75897422606b Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 11:12:17 +0000 Subject: [PATCH 12/43] test(vscode): prove local Prisma Next routing --- docs/language-server.md | 61 ++++ docs/testing.md | 19 ++ .../src/__test__/language-server/README.md | 14 +- .../vscode/src/__test__/workspace.test.ts | 296 ++++++++++++++++-- .../documentRouting.test.ts | 15 + .../prisma-language-server/documentRouting.ts | 23 +- .../plugins/prisma-language-server/index.ts | 8 + .../languageServerTestState.ts | 34 ++ .../localPrismaNextClientRegistry.test.ts | 14 +- .../localPrismaNextClientRegistry.ts | 13 +- .../integration-workspace.code-workspace | 4 + .../root-a/second.prisma | 3 + .../root-missing/schema.prisma | 3 + 13 files changed, 471 insertions(+), 36 deletions(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma diff --git a/docs/language-server.md b/docs/language-server.md index 657ae1cd43..61df1fb17b 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -33,3 +33,64 @@ for (const line of schema.iterLines()) { See [Prisma Multi-File Schema Documentation][multi-file-docs] for details. [multi-file-docs]: https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema + +## VS Code document routing + +When `prisma.pinToPrisma6` is disabled, the VS Code extension routes each open Prisma document independently: + +| Document | Owner | +| --------------------------------------------------------------------------------- | ------------------------------------------ | +| No `// use prisma-next` directive | Bundled language server | +| Directive present, trusted file workspace, matching root, and local CLI available | Prisma Next client for that workspace root | +| Directive present but local execution is ineligible or unavailable | No active language-server synchronization | + +The directive is content based and applies per file. A marked file does not opt sibling files or the rest of a multi-file schema into Prisma Next tooling. + +### Coordinator and synchronization boundary + +`DocumentOwnershipCoordinator` is the authoritative per-URI state machine. Open and change events are serialized per document. A transfer performs these operations in order: + +1. Close the prior synchronized owner. +2. Clear that owner's diagnostics for only the transferred URI. +3. Reclassify current unsaved text. +4. Lazily ensure the candidate root-local client when needed. +5. Reclassify after asynchronous startup. +6. Open the complete current document on the surviving owner. + +A close event invalidates pending revisions immediately, queues final cleanup, and leaves the URI internally unowned. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. + +Bundled and local middleware maintain ledgers of documents actually synchronized to their client. Raw editor notifications are forwarded only when committed ownership, current content classification, and (for local clients) the exact workspace root agree. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use the same ownership gate. Automatic local-client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to a local client over LSP. + +### Root-local Prisma Next launch contract + +The local-client registry is keyed by `WorkspaceFolder.uri.toString()` and coalesces concurrent startup for one root. Discovery checks only: + +```text +/node_modules/prisma/dist/prisma.js +``` + +The registry does not invoke a package manager, search parent directories, inspect package boundaries, or fall back to a global executable. Local execution requires `workspace.isTrusted` and a file-backed document in a file-backed workspace folder. + +The extension launches the module with the extension-host runtime using the exact process shape: + +```text +executable: process.execPath +argv: [/node_modules/prisma/dist/prisma.js, "lsp"] +cwd: +stdio: piped +shell: false +``` + +Electron extension hosts receive `ELECTRON_RUN_AS_NODE=1` and `ELECTRON_NO_ASAR=1`. The custom server-options launcher avoids transport arguments that `vscode-languageclient` would otherwise append. + +### Registry lifecycle contract + +The registry exposes a narrow lifecycle API used by routing and later workspace lifecycle handling: + +- `ensureClientForDocument(document)` — trust/root checks, exact discovery, and coalesced lazy startup. +- `openDocument(rootUri, document)` — verifies the document is still open before inserting it into the local middleware ledger. +- `closeDocument(rootUri, document)` — idempotently balances an actually synchronized local document. +- `clearDiagnostics(rootUri, uri)` — clears only the requested URI. +- `getTestState()` — reports successful root starts without exposing process handles; it is reachable through a command only in debug/test sessions. + +A started local client currently remains alive after its final marked document closes. Workspace-wide restart and rediscovery, runtime-failure recovery, workspace-folder removal, comprehensive deactivation, and live Prisma 6 pin transitions are separate lifecycle responsibilities that should build on this API rather than bypass the coordinator or middleware ledgers. diff --git a/docs/testing.md b/docs/testing.md index 0a152fc836..2a1737d367 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -74,3 +74,22 @@ pnpm test:e2e:vsix Both scripts use the same tests in `packages/vscode/src/__test__` with fixtures located in `packages/vscode/fixtures`. + +## VS Code Electron integration tests + +The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its roots include: + +- Two pnpm importers with the lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`. +- An additional marked-document fixture without that exact entrypoint, used to verify silent no-fallback behavior. + +Run the focused minimum-runtime workspace suite with: + +```bash +pnpm --filter prisma test:integration:workspace +``` + +This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the real Prisma CLI process; no mock language-server executable is part of the fixture. It covers lazy activation, one client per root, root reuse and independence, exclusive bundled/local ownership, complete-text unsaved directive transfers, diagnostics clearing, and missing-entrypoint behavior. + +The runner's installed `@vscode/test-electron` version always adds `--disable-workspace-trust`, so the Electron workspace is deterministically trusted. It cannot represent Restricted Mode without replacing or bypassing the runner's launch contract. Trust rejection is therefore covered at the production classifier and registry boundaries by focused unit tests; a manual Restricted Mode check remains necessary when validating trust behavior end to end. + +Routing observations are available only when `isDebugOrTestSession()` is true. The test command reports ownership/routing events and successful start counts. Complete document text and version are captured only by the optional test observer; production activation installs neither the collector nor the command, and no process handles are exposed. diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index 2d1375eca5..96e7df0688 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -3,6 +3,16 @@ Only one test per feature is done here. The goal is to check that the integration is working between the VS Code extension and the Language Server. -The integration runner opens `tests/fixtures/integration-workspace.code-workspace`, which contains two workspace roots. Each root is a pnpm workspace importer with the same lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`. +The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Two roots are pnpm workspace importers with the same lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`; a third root intentionally has no local CLI entrypoint. -Run the full minimum-and-latest integration suite with `pnpm test:integration`. To verify only the multi-root fixture substrate on the minimum supported VS Code runtime, run `pnpm test:integration:workspace`. +Run the full minimum-and-latest integration suite with `pnpm test:integration`. Run the focused real-CLI routing suite on the minimum supported VS Code runtime with: + +```bash +pnpm --filter prisma test:integration:workspace +``` + +The focused suite verifies that activation and unmarked documents start no local process, each eligible marked root starts exactly one real client, additional documents reuse their root client, roots remain independent, and the missing-entrypoint root has no fallback process. It also observes exclusive bundled/local synchronization, both unsaved directive transfer directions, complete current text/version, URI-scoped diagnostics clearing, and real local diagnostics. + +Test-only routing state is exposed through `prisma.test.languageServerRoutingState`. The command is registered only when `isDebugOrTestSession()` is true; production sessions do not install the observer or retain observed document contents. The state contains no process handles. + +`@vscode/test-electron` adds `--disable-workspace-trust` unconditionally, so this harness always runs trusted. Restricted Mode execution remains a manual check; focused classifier and registry tests cover the untrusted production boundaries. diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index 3819a60e6f..ee3418dcf4 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -1,22 +1,30 @@ import assert from 'node:assert' import { stat } from 'node:fs/promises' import vscode from 'vscode' +import type { DocumentOwner } from '../plugins/prisma-language-server/documentOwnership' +import type { DocumentRoutingEvent } from '../plugins/prisma-language-server/documentRouting' import { - localPrismaNextClientTestStateCommand, - type LocalPrismaNextClientTestState, -} from '../plugins/prisma-language-server/localPrismaNextClientRegistry' + languageServerTestStateCommand, + type LanguageServerTestState, +} from '../plugins/prisma-language-server/languageServerTestState' import { getPrismaCliEntrypoint, getWorkspaceDocUri, getWorkspaceFolder, sleep } from './helper' +const stateTimeoutMs = 30_000 +const diagnosticTimeoutMs = 20_000 + suite('Multi-root integration workspace', () => { test('resolves documents and real Prisma CLI entrypoints per workspace root', async () => { const rootA = getWorkspaceFolder('integration-root-a') const rootB = getWorkspaceFolder('integration-root-b') + const missingRoot = getWorkspaceFolder('integration-root-missing') - const documentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'schema.prisma')) - const documentB = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootB, 'schema.prisma')) + const documentAUri = getWorkspaceDocUri(rootA, 'schema.prisma') + const documentBUri = getWorkspaceDocUri(rootB, 'schema.prisma') + const missingDocumentUri = getWorkspaceDocUri(missingRoot, 'schema.prisma') - assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentA.uri), rootA) - assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentB.uri), rootB) + assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentAUri), rootA) + assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentBUri), rootB) + assert.strictEqual(vscode.workspace.getWorkspaceFolder(missingDocumentUri), missingRoot) assert.notStrictEqual(rootA.uri.toString(), rootB.uri.toString()) for (const workspaceFolder of [rootA, rootB]) { @@ -27,38 +35,268 @@ suite('Multi-root integration workspace', () => { `Missing Prisma CLI entrypoint: ${entrypoint.fsPath}`, ) } + + await assert.rejects(stat(getPrismaCliEntrypoint(missingRoot).fsPath), { code: 'ENOENT' }) }) - test('lazily starts the real root-local Prisma Next clients', async () => { + test('routes unsaved documents exclusively across real root-local clients', async () => { const rootA = getWorkspaceFolder('integration-root-a') const rootB = getWorkspaceFolder('integration-root-b') - const documentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'schema.prisma')) - const documentB = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootB, 'schema.prisma')) + const missingRoot = getWorkspaceFolder('integration-root-missing') const extension = vscode.extensions.getExtension('Prisma.prisma') assert.ok(extension) await extension.activate() + const activationState = await getTestState() + assert.deepStrictEqual(activationState.localClients.startedWorkspaceFolderUris, []) + assert.deepStrictEqual(activationState.localClients.startCountsByWorkspaceFolderUri, {}) - assert.deepStrictEqual(await getLocalClientState(), { startedWorkspaceFolderUris: [] }) - - const edit = new vscode.WorkspaceEdit() - edit.insert(documentA.uri, new vscode.Position(0, 0), '// use prisma-next\n') - edit.insert(documentB.uri, new vscode.Position(0, 0), '// use prisma-next\n') - assert.strictEqual(await vscode.workspace.applyEdit(edit), true) - - const expectedRoots = [rootA.uri.toString(), rootB.uri.toString()].sort() - for (let attempt = 0; attempt < 100; attempt += 1) { - const state = await getLocalClientState() - if (state.startedWorkspaceFolderUris.join() === expectedRoots.join()) { - assert.deepStrictEqual(state.startedWorkspaceFolderUris, expectedRoots) - return - } - await sleep(100) - } + const documentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'schema.prisma')) + const secondDocumentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'second.prisma')) + const documentB = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootB, 'schema.prisma')) + const missingDocument = await vscode.workspace.openTextDocument(getWorkspaceDocUri(missingRoot, 'schema.prisma')) + + const initialState = await waitForState( + (state) => + state.localClients.startedWorkspaceFolderUris.length === 0 && + [documentA, secondDocumentA, documentB, missingDocument].every((document) => + activeOwnerKeys(state, document.uri).has('bundled'), + ), + 'unmarked documents to be owned by the bundled client without starting local clients', + ) + assert.strictEqual(initialState.workspaceTrusted, true) + assert.deepStrictEqual(initialState.localClients.startCountsByWorkspaceFolderUri, {}) + assertExclusiveOwners(initialState) - assert.deepStrictEqual((await getLocalClientState()).startedWorkspaceFolderUris, expectedRoots) + const invalidSchema = 'model Broken {\n id\n}\n' + await replaceDocument(documentA, invalidSchema) + await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) + + const markedInvalidSchema = `// use prisma-next\n${invalidSchema}` + const addDirectiveEventIndex = initialState.routingEvents.length + await replaceDocument(documentA, markedInvalidSchema) + const localAState = await waitForState( + (state) => + state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && + lastOpenedAfter(state, documentA.uri, addDirectiveEventIndex)?.owner.kind === 'local', + 'root A local client and marked document synchronization', + ) + const localAOpen = lastOpenedAfter(localAState, documentA.uri, addDirectiveEventIndex) + assert.ok(localAOpen) + assert.strictEqual(localAOpen.documentText, markedInvalidSchema) + assert.strictEqual(localAOpen.documentVersion, documentA.version) + assert.deepStrictEqual(activeOwnerKeys(localAState, documentA.uri), new Set([localOwnerKey(rootA.uri.toString())])) + assert.strictEqual( + hasDiagnosticClearAfter(localAState, documentA.uri, 'bundled', addDirectiveEventIndex), + true, + 'expected bundled diagnostics to clear before local ownership', + ) + assertExclusiveOwners(localAState) + + await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) + + const markedSecondSchema = '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n' + const secondAEventIndex = localAState.routingEvents.length + await replaceDocument(secondDocumentA, markedSecondSchema) + const reusedAState = await waitForState( + (state) => lastOpenedAfter(state, secondDocumentA.uri, secondAEventIndex)?.owner.kind === 'local', + 'second root A document to synchronize locally', + ) + assert.strictEqual(reusedAState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) + assert.deepStrictEqual( + activeOwnerKeys(reusedAState, secondDocumentA.uri), + new Set([localOwnerKey(rootA.uri.toString())]), + ) + assertExclusiveOwners(reusedAState) + + const markedRootBSchema = '// use prisma-next\nmodel RootBRecord {\n id Int @id\n}\n' + const rootBEventIndex = reusedAState.routingEvents.length + await replaceDocument(documentB, markedRootBSchema) + const independentRootsState = await waitForState( + (state) => + state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && + state.localClients.startCountsByWorkspaceFolderUri[rootB.uri.toString()] === 1 && + lastOpenedAfter(state, documentB.uri, rootBEventIndex)?.owner.kind === 'local', + 'independent root B local client', + ) + assert.deepStrictEqual(independentRootsState.localClients.startedWorkspaceFolderUris, [ + rootA.uri.toString(), + rootB.uri.toString(), + ]) + assert.deepStrictEqual( + activeOwnerKeys(independentRootsState, documentB.uri), + new Set([localOwnerKey(rootB.uri.toString())]), + ) + assertExclusiveOwners(independentRootsState) + + const markedMissingSchema = '// use prisma-next\nmodel MissingCliRecord {\n id Int @id\n}\n' + const missingEventIndex = independentRootsState.routingEvents.length + const missingOwnershipEventIndex = independentRootsState.ownershipEvents.length + await replaceDocument(missingDocument, markedMissingSchema) + const missingState = await waitForState( + (state) => + hasLocalOwnershipAfter(state, missingDocument.uri, missingRoot.uri.toString(), missingOwnershipEventIndex) && + hasDiagnosticClearAfter(state, missingDocument.uri, 'bundled', missingEventIndex), + 'missing-entrypoint routing to settle without fallback', + ) + assert.deepStrictEqual(missingState.localClients.startedWorkspaceFolderUris, [ + rootA.uri.toString(), + rootB.uri.toString(), + ]) + assert.strictEqual(missingState.localClients.startCountsByWorkspaceFolderUri[missingRoot.uri.toString()], undefined) + assert.deepStrictEqual(activeOwnerKeys(missingState, missingDocument.uri), new Set()) + assert.strictEqual(lastOpenedAfter(missingState, missingDocument.uri, missingEventIndex), undefined) + assertExclusiveOwners(missingState) + + const restoredBundledSchema = 'model RootARestored {\n id Int @id\n}\n' + const removeDirectiveEventIndex = missingState.routingEvents.length + await replaceDocument(documentA, restoredBundledSchema) + const restoredState = await waitForState( + (state) => lastOpenedAfter(state, documentA.uri, removeDirectiveEventIndex)?.owner.kind === 'bundled', + 'root A document to return to bundled ownership', + ) + const bundledOpen = lastOpenedAfter(restoredState, documentA.uri, removeDirectiveEventIndex) + assert.ok(bundledOpen) + assert.strictEqual(bundledOpen.documentText, restoredBundledSchema) + assert.strictEqual(bundledOpen.documentVersion, documentA.version) + assert.strictEqual( + hasDiagnosticClearAfter(restoredState, documentA.uri, 'local', removeDirectiveEventIndex), + true, + 'expected local diagnostics to clear before bundled ownership', + ) + assert.deepStrictEqual(activeOwnerKeys(restoredState, documentA.uri), new Set(['bundled'])) + assert.strictEqual(restoredState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) + assertExclusiveOwners(restoredState) + await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length === 0) }) }) -async function getLocalClientState(): Promise { - return vscode.commands.executeCommand(localPrismaNextClientTestStateCommand) +async function getTestState(): Promise { + const state = await vscode.commands.executeCommand(languageServerTestStateCommand) + assert.ok(state, 'language-server test state command was not registered') + return state +} + +async function waitForState( + predicate: (state: LanguageServerTestState) => boolean, + description: string, +): Promise { + const deadline = Date.now() + stateTimeoutMs + let state = await getTestState() + while (!predicate(state) && Date.now() < deadline) { + await sleep(100) + state = await getTestState() + } + assert.ok(predicate(state), `Timed out waiting for ${description}`) + return state +} + +async function waitForDiagnostics( + uri: vscode.Uri, + predicate: (diagnostics: readonly vscode.Diagnostic[]) => boolean, +): Promise { + const deadline = Date.now() + diagnosticTimeoutMs + let diagnostics = vscode.languages.getDiagnostics(uri) + while (!predicate(diagnostics) && Date.now() < deadline) { + await sleep(100) + diagnostics = vscode.languages.getDiagnostics(uri) + } + assert.ok(predicate(diagnostics), `Timed out waiting for diagnostics for ${uri.toString()}`) + return diagnostics +} + +async function replaceDocument(document: vscode.TextDocument, text: string): Promise { + const edit = new vscode.WorkspaceEdit() + edit.replace( + document.uri, + new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length)), + text, + ) + assert.strictEqual(await vscode.workspace.applyEdit(edit), true) + assert.strictEqual(document.getText(), text) + assert.strictEqual(document.isDirty, true) +} + +function activeOwnerKeys(state: LanguageServerTestState, uri: vscode.Uri): Set { + const active = new Set() + for (const event of state.routingEvents) { + if (event.documentUri !== uri.toString()) continue + const key = ownerKey(event.owner) + if (event.type === 'opened') { + active.add(key) + } else if (event.type === 'closed') { + active.delete(key) + } + } + return active +} + +function assertExclusiveOwners(state: LanguageServerTestState): void { + const activeByDocument = new Map>() + for (const event of state.routingEvents) { + const active = activeByDocument.get(event.documentUri) ?? new Set() + activeByDocument.set(event.documentUri, active) + const key = ownerKey(event.owner) + if (event.type === 'opened') { + active.add(key) + assert.ok( + active.size <= 1, + `Document ${event.documentUri} was observed on multiple owners: ${[...active].join(', ')}`, + ) + } else if (event.type === 'closed') { + active.delete(key) + } + } +} + +function lastOpenedAfter( + state: LanguageServerTestState, + uri: vscode.Uri, + eventIndex: number, +): Extract | undefined { + return state.routingEvents + .slice(eventIndex) + .filter( + (event): event is Extract => + event.type === 'opened' && event.documentUri === uri.toString(), + ) + .at(-1) +} + +function hasDiagnosticClearAfter( + state: LanguageServerTestState, + uri: vscode.Uri, + owner: DocumentOwner['kind'], + eventIndex: number, +): boolean { + return state.routingEvents + .slice(eventIndex) + .some( + (event) => + event.type === 'diagnosticsCleared' && event.documentUri === uri.toString() && event.owner.kind === owner, + ) +} + +function hasLocalOwnershipAfter( + state: LanguageServerTestState, + uri: vscode.Uri, + workspaceFolderUri: string, + eventIndex: number, +): boolean { + return state.ownershipEvents + .slice(eventIndex) + .some( + (event) => + event.type === 'ownerChanged' && + event.documentUri === uri.toString() && + event.owner.kind === 'local' && + event.owner.workspaceFolderUri === workspaceFolderUri, + ) +} + +function ownerKey(owner: DocumentOwner): string { + return owner.kind === 'local' ? localOwnerKey(owner.workspaceFolderUri) : owner.kind +} + +function localOwnerKey(workspaceFolderUri: string): string { + return `local:${workspaceFolderUri}` } diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts index 8e12079790..42588f5ebb 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts @@ -21,12 +21,17 @@ function workspaceFolder(value: string): WorkspaceFolder { function document(value: string, text: string): TextDocument & { setText(value: string): void } { let currentText = text + let version = 1 return { uri: uri(value), languageId: 'prisma', + get version() { + return version + }, getText: () => currentText, setText: (value) => { currentText = value + version += 1 }, } as TextDocument & { setText(value: string): void } } @@ -153,6 +158,11 @@ describe('document routing commits', () => { }, ]) expect(subject.active).toEqual(new Set([`local:${rootA.uri.toString()}:${schema.uri.toString()}`])) + expect(subject.events.at(-1)).toMatchObject({ + type: 'opened', + documentText: schema.getText(), + documentVersion: schema.version, + }) expect(subject.activeOwnerCountsAfterOpen).toEqual([1, 1]) expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) @@ -164,6 +174,11 @@ describe('document routing commits', () => { expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) expect(subject.opens).toEqual([{ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }]) expect(subject.active).toEqual(new Set([`bundled:${schema.uri.toString()}`])) + expect(subject.events.at(-1)).toMatchObject({ + type: 'opened', + documentText: schema.getText(), + documentVersion: schema.version, + }) expect(subject.activeOwnerCountsAfterOpen).toEqual([1, 1, 1]) expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) }) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index cd322e0626..93cd39ee1d 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -17,7 +17,13 @@ export interface LocalDocumentSynchronization { export type DocumentRoutingEvent = | { readonly type: 'closed'; readonly owner: DocumentOwner; readonly documentUri: string } | { readonly type: 'diagnosticsCleared'; readonly owner: DocumentOwner; readonly documentUri: string } - | { readonly type: 'opened'; readonly owner: DocumentOwner; readonly documentUri: string } + | { + readonly type: 'opened' + readonly owner: DocumentOwner + readonly documentUri: string + readonly documentText: string + readonly documentVersion: number + } export interface DocumentRoutingOptions { readonly getOwnership: () => DocumentOwnershipCoordinator @@ -38,14 +44,14 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio if (nextOwner.kind === 'bundled') { options.getBundled().openDocument(document) - options.observer?.({ type: 'opened', owner: nextOwner, documentUri: document.uri.toString() }) + observeOpened(options, nextOwner, document) } else if (nextOwner.kind === 'local') { const local = options.getLocal() const client = await local.ensureClientForDocument(document) if (client && isCurrentOpenCandidate(options, document, nextOwner)) { const opened = await local.openDocument(nextOwner.workspaceFolderUri, document) if (opened && isCurrentOpenCandidate(options, document, nextOwner)) { - options.observer?.({ type: 'opened', owner: nextOwner, documentUri: document.uri.toString() }) + observeOpened(options, nextOwner, document) } } } @@ -72,6 +78,17 @@ async function closePreviousOwner( } } +function observeOpened(options: DocumentRoutingOptions, owner: DocumentOwner, document: TextDocument): void { + if (!options.observer) return + options.observer({ + type: 'opened', + owner, + documentUri: document.uri.toString(), + documentText: document.getText(), + documentVersion: document.version, + }) +} + function isCurrentOpenCandidate( options: DocumentRoutingOptions, document: TextDocument, diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index b396779ec4..4a10e46427 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -25,6 +25,7 @@ import { DocumentOwnershipCoordinator } from './documentOwnership' import { createBundledClientMiddleware, type BundledClientMiddleware } from './bundledClientMiddleware' import { createPrepareDocumentRoutingCommit } from './documentRouting' import { LocalPrismaNextClientRegistry, localPrismaNextClientTestStateCommand } from './localPrismaNextClientRegistry' +import { LanguageServerTestStateCollector, languageServerTestStateCommand } from './languageServerTestState' let client: LanguageClient let serverModule: string @@ -101,6 +102,7 @@ const plugin: PrismaVSCodePlugin = { enabled: () => true, activate: async (context) => { const isDebugOrTest = isDebugOrTestSession() + const testState = isDebugOrTest ? new LanguageServerTestStateCollector() : undefined const codelensProvider = new CodelensProvider() languages.registerCodeLensProvider('*', codelensProvider) @@ -112,11 +114,13 @@ const plugin: PrismaVSCodePlugin = { policy: { isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), }, + testObserver: testState?.observeOwnership, prepareOwner: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, isDocumentOpen: (document) => workspace.textDocuments.includes(document), getBundled: () => bundledClientMiddleware, getLocal: () => localClients, + observer: testState?.observeRouting, }), }) const localClients = new LocalPrismaNextClientRegistry({ @@ -126,6 +130,7 @@ const plugin: PrismaVSCodePlugin = { createClient: (id, name, serverOptions, localClientOptions) => new LanguageClient(id, name, serverOptions, localClientOptions), registerDisposable: (disposable) => context.subscriptions.push(disposable), + collectTestState: isDebugOrTest, handleStartError: (workspaceFolder, error) => { console.error(`Failed to start Prisma Next Language Server for ${workspaceFolder.uri.toString()}`, error) }, @@ -251,6 +256,9 @@ const plugin: PrismaVSCodePlugin = { if (isDebugOrTest) { context.subscriptions.push( commands.registerCommand(localPrismaNextClientTestStateCommand, () => localClients.getTestState()), + commands.registerCommand(languageServerTestStateCommand, () => + testState?.snapshot(workspace.isTrusted, localClients.getTestState()), + ), ) } else { const packageJSON = getPackageJSON(context) diff --git a/packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts b/packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts new file mode 100644 index 0000000000..337075331e --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts @@ -0,0 +1,34 @@ +import type { DocumentOwnershipTestEvent } from './documentOwnership' +import type { DocumentRoutingEvent } from './documentRouting' +import type { LocalPrismaNextClientTestState } from './localPrismaNextClientRegistry' + +export const languageServerTestStateCommand = 'prisma.test.languageServerRoutingState' + +export interface LanguageServerTestState { + readonly workspaceTrusted: boolean + readonly localClients: LocalPrismaNextClientTestState + readonly ownershipEvents: readonly DocumentOwnershipTestEvent[] + readonly routingEvents: readonly DocumentRoutingEvent[] +} + +export class LanguageServerTestStateCollector { + private readonly ownershipEvents: DocumentOwnershipTestEvent[] = [] + private readonly routingEvents: DocumentRoutingEvent[] = [] + + readonly observeOwnership = (event: DocumentOwnershipTestEvent): void => { + this.ownershipEvents.push(event) + } + + readonly observeRouting = (event: DocumentRoutingEvent): void => { + this.routingEvents.push(event) + } + + snapshot(workspaceTrusted: boolean, localClients: LocalPrismaNextClientTestState): LanguageServerTestState { + return { + workspaceTrusted, + localClients, + ownershipEvents: [...this.ownershipEvents], + routingEvents: [...this.routingEvents], + } + } +} diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts index ea5f3a7a63..451120d71f 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts @@ -210,6 +210,10 @@ describe('LocalPrismaNextClientRegistry', () => { await expect(startup).resolves.toBe(client) await expect(open).resolves.toBe(false) + expect(registry.getTestState()).toEqual({ + startedWorkspaceFolderUris: [rootA.uri.toString()], + startCountsByWorkspaceFolderUri: {}, + }) }) test('publishes pending startup per root and starts independent clients', async () => { @@ -238,6 +242,7 @@ describe('LocalPrismaNextClientRegistry', () => { entrypointExists, createClient, registerDisposable, + collectTestState: true, launcher: { executable: '/extension-host', environment: {}, @@ -279,6 +284,10 @@ describe('LocalPrismaNextClientRegistry', () => { ]) expect(registry.getTestState()).toEqual({ startedWorkspaceFolderUris: [rootA.uri.toString(), rootB.uri.toString()], + startCountsByWorkspaceFolderUri: { + [rootA.uri.toString()]: 1, + [rootB.uri.toString()]: 1, + }, }) }) @@ -352,6 +361,9 @@ describe('LocalPrismaNextClientRegistry', () => { expect(createClient).toHaveBeenCalledOnce() expect(handleStartError).toHaveBeenCalledOnce() expect(handleStartError).toHaveBeenCalledWith(rootA, startError) - expect(registry.getTestState()).toEqual({ startedWorkspaceFolderUris: [] }) + expect(registry.getTestState()).toEqual({ + startedWorkspaceFolderUris: [], + startCountsByWorkspaceFolderUri: {}, + }) }) }) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index 062409d27c..3f9602f2a7 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -43,10 +43,12 @@ export interface LocalPrismaNextClientRegistryOptions { readonly entrypointExists?: (entrypoint: string) => Promise readonly handleStartError?: (workspaceFolder: WorkspaceFolder, error: unknown) => void readonly launcher?: Omit + readonly collectTestState?: boolean } export interface LocalPrismaNextClientTestState { readonly startedWorkspaceFolderUris: readonly string[] + readonly startCountsByWorkspaceFolderUri: Readonly> } interface LocalPrismaNextClientEntry { @@ -57,8 +59,11 @@ interface LocalPrismaNextClientEntry { export class LocalPrismaNextClientRegistry { private readonly clients = new Map>() private readonly startedClients = new Map() + private readonly startCounts: Map | undefined - constructor(private readonly options: LocalPrismaNextClientRegistryOptions) {} + constructor(private readonly options: LocalPrismaNextClientRegistryOptions) { + this.startCounts = options.collectTestState ? new Map() : undefined + } ensureClientForDocument(document: TextDocument): Promise { if (!this.options.workspace.isTrusted || document.uri.scheme !== 'file') { @@ -95,6 +100,9 @@ export class LocalPrismaNextClientRegistry { getTestState(): LocalPrismaNextClientTestState { return { startedWorkspaceFolderUris: [...this.startedClients.keys()].sort(), + startCountsByWorkspaceFolderUri: Object.fromEntries( + [...(this.startCounts?.entries() ?? [])].sort(([left], [right]) => left.localeCompare(right)), + ), } } @@ -139,6 +147,9 @@ export class LocalPrismaNextClientRegistry { await client.onReady() const entry = { client, middleware } this.startedClients.set(workspaceFolderUri, entry) + if (this.startCounts) { + this.startCounts.set(workspaceFolderUri, (this.startCounts.get(workspaceFolderUri) ?? 0) + 1) + } return entry } catch (error) { this.options.handleStartError?.(workspaceFolder, error) diff --git a/packages/vscode/tests/fixtures/integration-workspace.code-workspace b/packages/vscode/tests/fixtures/integration-workspace.code-workspace index 5a08df1d6d..7912a21eca 100644 --- a/packages/vscode/tests/fixtures/integration-workspace.code-workspace +++ b/packages/vscode/tests/fixtures/integration-workspace.code-workspace @@ -8,5 +8,9 @@ "name": "integration-root-b", "path": "integration-workspace/root-b", }, + { + "name": "integration-root-missing", + "path": "integration-workspace/root-missing", + }, ], } diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma new file mode 100644 index 0000000000..63098ded90 --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma @@ -0,0 +1,3 @@ +model RootASecondRecord { + id Int @id +} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma new file mode 100644 index 0000000000..c981121fa0 --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma @@ -0,0 +1,3 @@ +model MissingCliRecord { + id Int @id +} From 426e6a1acd67cb18142885201d06c908b55f8cdb Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 11:22:18 +0000 Subject: [PATCH 13/43] test(vscode): restore integration fixtures --- .../vscode/src/__test__/workspace.test.ts | 315 +++++++++++------- 1 file changed, 192 insertions(+), 123 deletions(-) diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index ee3418dcf4..325f38b464 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -43,130 +43,147 @@ suite('Multi-root integration workspace', () => { const rootA = getWorkspaceFolder('integration-root-a') const rootB = getWorkspaceFolder('integration-root-b') const missingRoot = getWorkspaceFolder('integration-root-missing') - const extension = vscode.extensions.getExtension('Prisma.prisma') - assert.ok(extension) - await extension.activate() - const activationState = await getTestState() - assert.deepStrictEqual(activationState.localClients.startedWorkspaceFolderUris, []) - assert.deepStrictEqual(activationState.localClients.startCountsByWorkspaceFolderUri, {}) - - const documentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'schema.prisma')) - const secondDocumentA = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootA, 'second.prisma')) - const documentB = await vscode.workspace.openTextDocument(getWorkspaceDocUri(rootB, 'schema.prisma')) - const missingDocument = await vscode.workspace.openTextDocument(getWorkspaceDocUri(missingRoot, 'schema.prisma')) - - const initialState = await waitForState( - (state) => - state.localClients.startedWorkspaceFolderUris.length === 0 && - [documentA, secondDocumentA, documentB, missingDocument].every((document) => - activeOwnerKeys(state, document.uri).has('bundled'), - ), - 'unmarked documents to be owned by the bundled client without starting local clients', - ) - assert.strictEqual(initialState.workspaceTrusted, true) - assert.deepStrictEqual(initialState.localClients.startCountsByWorkspaceFolderUri, {}) - assertExclusiveOwners(initialState) - - const invalidSchema = 'model Broken {\n id\n}\n' - await replaceDocument(documentA, invalidSchema) - await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) - - const markedInvalidSchema = `// use prisma-next\n${invalidSchema}` - const addDirectiveEventIndex = initialState.routingEvents.length - await replaceDocument(documentA, markedInvalidSchema) - const localAState = await waitForState( - (state) => - state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && - lastOpenedAfter(state, documentA.uri, addDirectiveEventIndex)?.owner.kind === 'local', - 'root A local client and marked document synchronization', - ) - const localAOpen = lastOpenedAfter(localAState, documentA.uri, addDirectiveEventIndex) - assert.ok(localAOpen) - assert.strictEqual(localAOpen.documentText, markedInvalidSchema) - assert.strictEqual(localAOpen.documentVersion, documentA.version) - assert.deepStrictEqual(activeOwnerKeys(localAState, documentA.uri), new Set([localOwnerKey(rootA.uri.toString())])) - assert.strictEqual( - hasDiagnosticClearAfter(localAState, documentA.uri, 'bundled', addDirectiveEventIndex), - true, - 'expected bundled diagnostics to clear before local ownership', - ) - assertExclusiveOwners(localAState) + const fixtures = await snapshotFixtures([ + getWorkspaceDocUri(rootA, 'schema.prisma'), + getWorkspaceDocUri(rootA, 'second.prisma'), + getWorkspaceDocUri(rootB, 'schema.prisma'), + getWorkspaceDocUri(missingRoot, 'schema.prisma'), + ]) - await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) + try { + const extension = vscode.extensions.getExtension('Prisma.prisma') + assert.ok(extension) + await extension.activate() + const activationState = await getTestState() + assert.deepStrictEqual(activationState.localClients.startedWorkspaceFolderUris, []) + assert.deepStrictEqual(activationState.localClients.startCountsByWorkspaceFolderUri, {}) - const markedSecondSchema = '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n' - const secondAEventIndex = localAState.routingEvents.length - await replaceDocument(secondDocumentA, markedSecondSchema) - const reusedAState = await waitForState( - (state) => lastOpenedAfter(state, secondDocumentA.uri, secondAEventIndex)?.owner.kind === 'local', - 'second root A document to synchronize locally', - ) - assert.strictEqual(reusedAState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) - assert.deepStrictEqual( - activeOwnerKeys(reusedAState, secondDocumentA.uri), - new Set([localOwnerKey(rootA.uri.toString())]), - ) - assertExclusiveOwners(reusedAState) - - const markedRootBSchema = '// use prisma-next\nmodel RootBRecord {\n id Int @id\n}\n' - const rootBEventIndex = reusedAState.routingEvents.length - await replaceDocument(documentB, markedRootBSchema) - const independentRootsState = await waitForState( - (state) => - state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && - state.localClients.startCountsByWorkspaceFolderUri[rootB.uri.toString()] === 1 && - lastOpenedAfter(state, documentB.uri, rootBEventIndex)?.owner.kind === 'local', - 'independent root B local client', - ) - assert.deepStrictEqual(independentRootsState.localClients.startedWorkspaceFolderUris, [ - rootA.uri.toString(), - rootB.uri.toString(), - ]) - assert.deepStrictEqual( - activeOwnerKeys(independentRootsState, documentB.uri), - new Set([localOwnerKey(rootB.uri.toString())]), - ) - assertExclusiveOwners(independentRootsState) - - const markedMissingSchema = '// use prisma-next\nmodel MissingCliRecord {\n id Int @id\n}\n' - const missingEventIndex = independentRootsState.routingEvents.length - const missingOwnershipEventIndex = independentRootsState.ownershipEvents.length - await replaceDocument(missingDocument, markedMissingSchema) - const missingState = await waitForState( - (state) => - hasLocalOwnershipAfter(state, missingDocument.uri, missingRoot.uri.toString(), missingOwnershipEventIndex) && - hasDiagnosticClearAfter(state, missingDocument.uri, 'bundled', missingEventIndex), - 'missing-entrypoint routing to settle without fallback', - ) - assert.deepStrictEqual(missingState.localClients.startedWorkspaceFolderUris, [ - rootA.uri.toString(), - rootB.uri.toString(), - ]) - assert.strictEqual(missingState.localClients.startCountsByWorkspaceFolderUri[missingRoot.uri.toString()], undefined) - assert.deepStrictEqual(activeOwnerKeys(missingState, missingDocument.uri), new Set()) - assert.strictEqual(lastOpenedAfter(missingState, missingDocument.uri, missingEventIndex), undefined) - assertExclusiveOwners(missingState) - - const restoredBundledSchema = 'model RootARestored {\n id Int @id\n}\n' - const removeDirectiveEventIndex = missingState.routingEvents.length - await replaceDocument(documentA, restoredBundledSchema) - const restoredState = await waitForState( - (state) => lastOpenedAfter(state, documentA.uri, removeDirectiveEventIndex)?.owner.kind === 'bundled', - 'root A document to return to bundled ownership', - ) - const bundledOpen = lastOpenedAfter(restoredState, documentA.uri, removeDirectiveEventIndex) - assert.ok(bundledOpen) - assert.strictEqual(bundledOpen.documentText, restoredBundledSchema) - assert.strictEqual(bundledOpen.documentVersion, documentA.version) - assert.strictEqual( - hasDiagnosticClearAfter(restoredState, documentA.uri, 'local', removeDirectiveEventIndex), - true, - 'expected local diagnostics to clear before bundled ownership', - ) - assert.deepStrictEqual(activeOwnerKeys(restoredState, documentA.uri), new Set(['bundled'])) - assert.strictEqual(restoredState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) - assertExclusiveOwners(restoredState) - await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length === 0) + const documentA = await vscode.workspace.openTextDocument(fixtures[0].uri) + const secondDocumentA = await vscode.workspace.openTextDocument(fixtures[1].uri) + const documentB = await vscode.workspace.openTextDocument(fixtures[2].uri) + const missingDocument = await vscode.workspace.openTextDocument(fixtures[3].uri) + + const initialState = await waitForState( + (state) => + state.localClients.startedWorkspaceFolderUris.length === 0 && + [documentA, secondDocumentA, documentB, missingDocument].every((document) => + activeOwnerKeys(state, document.uri).has('bundled'), + ), + 'unmarked documents to be owned by the bundled client without starting local clients', + ) + assert.strictEqual(initialState.workspaceTrusted, true) + assert.deepStrictEqual(initialState.localClients.startCountsByWorkspaceFolderUri, {}) + assertExclusiveOwners(initialState) + + const invalidSchema = 'model Broken {\n id\n}\n' + await replaceDocument(documentA, invalidSchema) + await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) + + const markedInvalidSchema = `// use prisma-next\n${invalidSchema}` + const addDirectiveEventIndex = initialState.routingEvents.length + await replaceDocument(documentA, markedInvalidSchema) + const localAState = await waitForState( + (state) => + state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && + lastOpenedAfter(state, documentA.uri, addDirectiveEventIndex)?.owner.kind === 'local', + 'root A local client and marked document synchronization', + ) + const localAOpen = lastOpenedAfter(localAState, documentA.uri, addDirectiveEventIndex) + assert.ok(localAOpen) + assert.strictEqual(localAOpen.documentText, markedInvalidSchema) + assert.strictEqual(localAOpen.documentVersion, documentA.version) + assert.deepStrictEqual( + activeOwnerKeys(localAState, documentA.uri), + new Set([localOwnerKey(rootA.uri.toString())]), + ) + assert.strictEqual( + hasDiagnosticClearAfter(localAState, documentA.uri, 'bundled', addDirectiveEventIndex), + true, + 'expected bundled diagnostics to clear before local ownership', + ) + assertExclusiveOwners(localAState) + + await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) + + const markedSecondSchema = '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n' + const secondAEventIndex = localAState.routingEvents.length + await replaceDocument(secondDocumentA, markedSecondSchema) + const reusedAState = await waitForState( + (state) => lastOpenedAfter(state, secondDocumentA.uri, secondAEventIndex)?.owner.kind === 'local', + 'second root A document to synchronize locally', + ) + assert.strictEqual(reusedAState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) + assert.deepStrictEqual( + activeOwnerKeys(reusedAState, secondDocumentA.uri), + new Set([localOwnerKey(rootA.uri.toString())]), + ) + assertExclusiveOwners(reusedAState) + + const markedRootBSchema = '// use prisma-next\nmodel RootBRecord {\n id Int @id\n}\n' + const rootBEventIndex = reusedAState.routingEvents.length + await replaceDocument(documentB, markedRootBSchema) + const independentRootsState = await waitForState( + (state) => + state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && + state.localClients.startCountsByWorkspaceFolderUri[rootB.uri.toString()] === 1 && + lastOpenedAfter(state, documentB.uri, rootBEventIndex)?.owner.kind === 'local', + 'independent root B local client', + ) + assert.deepStrictEqual(independentRootsState.localClients.startedWorkspaceFolderUris, [ + rootA.uri.toString(), + rootB.uri.toString(), + ]) + assert.deepStrictEqual( + activeOwnerKeys(independentRootsState, documentB.uri), + new Set([localOwnerKey(rootB.uri.toString())]), + ) + assertExclusiveOwners(independentRootsState) + + const markedMissingSchema = '// use prisma-next\nmodel MissingCliRecord {\n id Int @id\n}\n' + const missingEventIndex = independentRootsState.routingEvents.length + const missingOwnershipEventIndex = independentRootsState.ownershipEvents.length + await replaceDocument(missingDocument, markedMissingSchema) + const missingState = await waitForState( + (state) => + hasLocalOwnershipAfter(state, missingDocument.uri, missingRoot.uri.toString(), missingOwnershipEventIndex) && + hasDiagnosticClearAfter(state, missingDocument.uri, 'bundled', missingEventIndex), + 'missing-entrypoint routing to settle without fallback', + ) + assert.deepStrictEqual(missingState.localClients.startedWorkspaceFolderUris, [ + rootA.uri.toString(), + rootB.uri.toString(), + ]) + assert.strictEqual( + missingState.localClients.startCountsByWorkspaceFolderUri[missingRoot.uri.toString()], + undefined, + ) + assert.deepStrictEqual(activeOwnerKeys(missingState, missingDocument.uri), new Set()) + assert.strictEqual(lastOpenedAfter(missingState, missingDocument.uri, missingEventIndex), undefined) + assertExclusiveOwners(missingState) + + const restoredBundledSchema = 'model RootARestored {\n id Int @id\n}\n' + const removeDirectiveEventIndex = missingState.routingEvents.length + await replaceDocument(documentA, restoredBundledSchema) + const restoredState = await waitForState( + (state) => lastOpenedAfter(state, documentA.uri, removeDirectiveEventIndex)?.owner.kind === 'bundled', + 'root A document to return to bundled ownership', + ) + const bundledOpen = lastOpenedAfter(restoredState, documentA.uri, removeDirectiveEventIndex) + assert.ok(bundledOpen) + assert.strictEqual(bundledOpen.documentText, restoredBundledSchema) + assert.strictEqual(bundledOpen.documentVersion, documentA.version) + assert.strictEqual( + hasDiagnosticClearAfter(restoredState, documentA.uri, 'local', removeDirectiveEventIndex), + true, + 'expected local diagnostics to clear before bundled ownership', + ) + assert.deepStrictEqual(activeOwnerKeys(restoredState, documentA.uri), new Set(['bundled'])) + assert.strictEqual(restoredState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) + assertExclusiveOwners(restoredState) + await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length === 0) + } finally { + await restoreFixtures(fixtures) + } }) }) @@ -205,6 +222,55 @@ async function waitForDiagnostics( } async function replaceDocument(document: vscode.TextDocument, text: string): Promise { + await replaceDocumentText(document, text) + assert.strictEqual(document.isDirty, true) +} + +interface FixtureSnapshot { + readonly uri: vscode.Uri + readonly bytes: Uint8Array + readonly text: string +} + +async function snapshotFixtures(uris: readonly vscode.Uri[]): Promise { + return Promise.all( + uris.map(async (uri) => { + const bytes = await vscode.workspace.fs.readFile(uri) + return { uri, bytes, text: Buffer.from(bytes).toString('utf8') } + }), + ) +} + +async function restoreFixtures(fixtures: readonly FixtureSnapshot[]): Promise { + for (const fixture of fixtures) { + const document = findOpenDocument(fixture.uri) + if (document) { + if (document.getText() !== fixture.text) { + await replaceDocumentText(document, fixture.text) + } + if (document.isDirty && !(await document.save())) { + await vscode.workspace.fs.writeFile(fixture.uri, fixture.bytes) + } + } else { + await vscode.workspace.fs.writeFile(fixture.uri, fixture.bytes) + } + } + + for (const fixture of fixtures) { + const document = findOpenDocument(fixture.uri) + if (document) { + assert.strictEqual(document.getText(), fixture.text, `Open fixture was not restored: ${fixture.uri.toString()}`) + assert.strictEqual(document.isDirty, false, `Restored fixture remains dirty: ${fixture.uri.toString()}`) + } + assert.deepStrictEqual( + await vscode.workspace.fs.readFile(fixture.uri), + fixture.bytes, + `On-disk fixture was not restored: ${fixture.uri.toString()}`, + ) + } +} + +async function replaceDocumentText(document: vscode.TextDocument, text: string): Promise { const edit = new vscode.WorkspaceEdit() edit.replace( document.uri, @@ -213,7 +279,10 @@ async function replaceDocument(document: vscode.TextDocument, text: string): Pro ) assert.strictEqual(await vscode.workspace.applyEdit(edit), true) assert.strictEqual(document.getText(), text) - assert.strictEqual(document.isDirty, true) +} + +function findOpenDocument(uri: vscode.Uri): vscode.TextDocument | undefined { + return vscode.workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()) } function activeOwnerKeys(state: LanguageServerTestState, uri: vscode.Uri): Set { From eee2aa6fe46fd3fac0f81e2daf5fa2696863d2b7 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 12:04:39 +0000 Subject: [PATCH 14/43] test(vscode): make routing e2e setup deterministic --- packages/vscode/src/__test__/helper.ts | 21 +++++++++++++++++++++ packages/vscode/src/__test__/runTest.ts | 5 ++++- packages/vscode/src/util.ts | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/vscode/src/__test__/helper.ts b/packages/vscode/src/__test__/helper.ts index 9bcd2ab290..cbda9d2a6f 100644 --- a/packages/vscode/src/__test__/helper.ts +++ b/packages/vscode/src/__test__/helper.ts @@ -1,5 +1,9 @@ import path from 'path' import vscode from 'vscode' +import { + languageServerTestStateCommand, + type LanguageServerTestState, +} from '../plugins/prisma-language-server/languageServerTestState' // Path from dist-tests/__test__/helper.js to package.json // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires @@ -29,12 +33,29 @@ export async function activate(docUri: vscode.Uri): Promise { try { doc = await vscode.workspace.openTextDocument(docUri) editor = await vscode.window.showTextDocument(doc) + await waitForBundledRouting(doc) await sleep(2500) // Wait for server activation } catch (e) { console.error(e) } } +async function waitForBundledRouting(document: vscode.TextDocument): Promise { + const documentUri = document.uri.toString() + const deadline = Date.now() + 10_000 + + while (Date.now() < deadline) { + const state = await vscode.commands.executeCommand(languageServerTestStateCommand) + const latestOpen = [...state.routingEvents] + .reverse() + .find((event) => event.type === 'opened' && event.documentUri === documentUri) + if (latestOpen?.owner.kind === 'bundled') return + await sleep(100) + } + + throw new Error(`Timed out waiting for bundled language-server routing for ${documentUri}`) +} + export function toRange(sLine: number, sChar: number, eLine: number, eChar: number): vscode.Range { const start = new vscode.Position(sLine, sChar) const end = new vscode.Position(eLine, eChar) diff --git a/packages/vscode/src/__test__/runTest.ts b/packages/vscode/src/__test__/runTest.ts index 578196c5a3..bfc22401b1 100644 --- a/packages/vscode/src/__test__/runTest.ts +++ b/packages/vscode/src/__test__/runTest.ts @@ -22,7 +22,10 @@ function test(version?: string, testPattern?: string) { version, // optional, default = latest extensionDevelopmentPath, extensionTestsPath, - extensionTestsEnv: testPattern ? { VSCODE_TEST_PATTERN: testPattern } : undefined, + extensionTestsEnv: { + PRISMA_VSCODE_TEST: '1', + ...(testPattern ? { VSCODE_TEST_PATTERN: testPattern } : {}), + }, launchArgs: [ workspacePath, // This disables all extensions except the one being testing diff --git a/packages/vscode/src/util.ts b/packages/vscode/src/util.ts index d9601e135c..56284877ce 100644 --- a/packages/vscode/src/util.ts +++ b/packages/vscode/src/util.ts @@ -16,7 +16,7 @@ import { homedir } from 'os' import { readdirSync } from 'fs' import path from 'path' export function isDebugOrTestSession(): boolean { - return env.sessionId === 'someValue.sessionId' + return env.sessionId === 'someValue.sessionId' || process.env.PRISMA_VSCODE_TEST === '1' } export { isPrismaNextSchema } From 4d2f4ff5eea5a7d59e6a577d60020e8a7940094b Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 12:13:33 +0000 Subject: [PATCH 15/43] test(vscode): opt into bundled routing readiness --- packages/vscode/src/__test__/helper.ts | 14 +++++++++++--- .../__test__/language-server/completion.test.ts | 2 +- .../src/__test__/language-server/format.test.ts | 2 +- .../src/__test__/language-server/hover.test.ts | 2 +- .../language-server/jumpToDefinition.test.ts | 2 +- .../src/__test__/language-server/linting.test.ts | 2 +- .../__test__/language-server/prismaNext.test.ts | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/vscode/src/__test__/helper.ts b/packages/vscode/src/__test__/helper.ts index cbda9d2a6f..629ced7dda 100644 --- a/packages/vscode/src/__test__/helper.ts +++ b/packages/vscode/src/__test__/helper.ts @@ -22,7 +22,11 @@ export async function sleep(ms: number): Promise { * Activates the vscode.prisma extension * @todo check readiness of the server instead of timeout */ -export async function activate(docUri: vscode.Uri): Promise { +export interface ActivateOptions { + readonly waitForBundledRouting?: boolean +} + +export async function activate(docUri: vscode.Uri, options: ActivateOptions = {}): Promise { // The extensionId is `publisher.name` from package.json const ext = vscode.extensions.getExtension(`${packageJson.publisher}.${packageJson.name}`) if (!ext) { @@ -33,11 +37,15 @@ export async function activate(docUri: vscode.Uri): Promise { try { doc = await vscode.workspace.openTextDocument(docUri) editor = await vscode.window.showTextDocument(doc) - await waitForBundledRouting(doc) - await sleep(2500) // Wait for server activation } catch (e) { console.error(e) + return + } + + if (options.waitForBundledRouting) { + await waitForBundledRouting(doc) } + await sleep(2500) // Wait for server activation } async function waitForBundledRouting(document: vscode.TextDocument): Promise { diff --git a/packages/vscode/src/__test__/language-server/completion.test.ts b/packages/vscode/src/__test__/language-server/completion.test.ts index 043574b1ae..83e95a6c3d 100644 --- a/packages/vscode/src/__test__/language-server/completion.test.ts +++ b/packages/vscode/src/__test__/language-server/completion.test.ts @@ -12,7 +12,7 @@ async function testCompletion( triggerCharacter?: string, ): Promise { if (!isActivated) { - await activate(docUri) + await activate(docUri, { waitForBundledRouting: true }) } const actualCompletions: vscode.CompletionList = await vscode.commands.executeCommand( diff --git a/packages/vscode/src/__test__/language-server/format.test.ts b/packages/vscode/src/__test__/language-server/format.test.ts index 5d337f8bd2..8aebdfcbbf 100644 --- a/packages/vscode/src/__test__/language-server/format.test.ts +++ b/packages/vscode/src/__test__/language-server/format.test.ts @@ -4,7 +4,7 @@ import { getDocUri, activate } from '../helper' import fs from 'fs' async function testAutoFormat(docUri: vscode.Uri, expectedFormatted: string): Promise { - await activate(docUri) + await activate(docUri, { waitForBundledRouting: true }) const actualFormatted = (await vscode.commands.executeCommand('vscode.executeFormatDocumentProvider', docUri, { insertSpaces: true, diff --git a/packages/vscode/src/__test__/language-server/hover.test.ts b/packages/vscode/src/__test__/language-server/hover.test.ts index 911cde2632..e2bdb7f49d 100644 --- a/packages/vscode/src/__test__/language-server/hover.test.ts +++ b/packages/vscode/src/__test__/language-server/hover.test.ts @@ -17,7 +17,7 @@ suite('Should show /// documentation comments for', () => { const expectedHover = `\`\`\`prisma\nmodel Post {\n\t...\n\tauthor User? @relation(name: "PostToUser", fields: [authorId], references: [id])\n}\n\`\`\`\n___\none-to-many\n___\nPost including an author and content.` test('model', async () => { - await activate(docUri) + await activate(docUri, { waitForBundledRouting: true }) await testHover(docUri, new vscode.Position(22, 10), expectedHover) }) }) diff --git a/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts b/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts index 8c1264e441..1cb550e097 100644 --- a/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts +++ b/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts @@ -21,7 +21,7 @@ suite('Jump-to-definition', () => { const fixturePathSqlite = getDocUri('jump-to-definition/schema.prisma') test('SQLite: from attribute to model', async function () { - await activate(fixturePathSqlite) + await activate(fixturePathSqlite, { waitForBundledRouting: true }) await testJumpToDefinition( fixturePathSqlite, diff --git a/packages/vscode/src/__test__/language-server/linting.test.ts b/packages/vscode/src/__test__/language-server/linting.test.ts index b8f182ca4b..b1bd8974fd 100644 --- a/packages/vscode/src/__test__/language-server/linting.test.ts +++ b/packages/vscode/src/__test__/language-server/linting.test.ts @@ -3,7 +3,7 @@ import * as assert from 'assert' import { getDocUri, activate, toRange } from '../helper' async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]): Promise { - await activate(docUri) + await activate(docUri, { waitForBundledRouting: true }) const actualDiagnostics = vscode.languages.getDiagnostics(docUri) diff --git a/packages/vscode/src/__test__/language-server/prismaNext.test.ts b/packages/vscode/src/__test__/language-server/prismaNext.test.ts index 8277903dac..dcbcfb74cd 100644 --- a/packages/vscode/src/__test__/language-server/prismaNext.test.ts +++ b/packages/vscode/src/__test__/language-server/prismaNext.test.ts @@ -49,7 +49,7 @@ suite('Prisma-next directive', () => { test('Sibling file without directive still gets diagnostics', async () => { const docUri = getDocUri('linting/missingArgument.prisma') - await activate(docUri) + await activate(docUri, { waitForBundledRouting: true }) const diagnostics = await waitForDiagnostics(docUri, (d) => d.length > 0) assert.ok(diagnostics.length > 0, 'expected diagnostics on regular file with errors') }) From a1d73febeb57e6a3dcb60ff10a528a622e2e3625 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 12:27:18 +0000 Subject: [PATCH 16/43] fix(vscode): await bundled client before routing --- packages/vscode/src/__test__/workspace.test.ts | 2 +- .../src/plugins/prisma-language-server/index.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index 325f38b464..d118a8fb5c 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -75,7 +75,7 @@ suite('Multi-root integration workspace', () => { assert.deepStrictEqual(initialState.localClients.startCountsByWorkspaceFolderUri, {}) assertExclusiveOwners(initialState) - const invalidSchema = 'model Broken {\n id\n}\n' + const invalidSchema = 'model Broken {\n id Missing\n}\n' await replaceDocument(documentA, invalidSchema) await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 4a10e46427..cbdedc53a9 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -34,7 +34,7 @@ let fileWatcher: FileWatcher.type | undefined const isDebugMode = () => process.env.VSCODE_DEBUG_MODE === 'true' -const activateClient = (context: ExtensionContext, clientOptions: LanguageClientOptions) => { +const activateClient = async (context: ExtensionContext, clientOptions: LanguageClientOptions): Promise => { const prismaConfig = workspace.getConfiguration('prisma') // Create the language client const serverOptions = getServerOptions(prismaConfig, context) @@ -44,6 +44,7 @@ const activateClient = (context: ExtensionContext, clientOptions: LanguageClient // Start the client. This will also launch the server context.subscriptions.push(disposable) + await client.onReady() } const onFileChange = (filepath: string) => { @@ -154,19 +155,20 @@ const plugin: PrismaVSCodePlugin = { } let started = false + let clientReady = Promise.resolve() const needsLanguageServer = (doc: TextDocument): boolean => doc.languageId === 'prisma' && ownership.classify(doc).kind === 'bundled' const synchronizeDocument = (document: TextDocument): void => { if (document.languageId === 'prisma') { - void ownership.synchronize(document) + void clientReady.then(() => ownership.synchronize(document)) } } - const maybeStart = () => { + const maybeStart = (document?: TextDocument) => { if (started) return - if (!workspace.textDocuments.some(needsLanguageServer)) return + if (document ? !needsLanguageServer(document) : !workspace.textDocuments.some(needsLanguageServer)) return started = true - activateClient(context, clientOptions) + clientReady = activateClient(context, clientOptions) } const restartLanguageServer = async () => { @@ -234,11 +236,11 @@ const plugin: PrismaVSCodePlugin = { }), workspace.onDidOpenTextDocument((document) => { - maybeStart() + maybeStart(document) synchronizeDocument(document) }), workspace.onDidChangeTextDocument((event) => { - maybeStart() + maybeStart(event.document) synchronizeDocument(event.document) }), workspace.onDidCloseTextDocument((document) => { From deba8b43692e867b3ec398bf913ad2401d3d6298 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 12:37:42 +0000 Subject: [PATCH 17/43] fix(vscode): stabilize bundled client startup --- .../bundledClientStartup.test.ts | 135 ++++++++++++++++++ .../bundledClientStartup.ts | 83 +++++++++++ .../plugins/prisma-language-server/index.ts | 30 +++- 3 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts create mode 100644 packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts new file mode 100644 index 0000000000..c15f8d81ab --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, test, vi } from 'vitest' +import { BundledClientStartup } from './bundledClientStartup' + +interface TestDocument { + readonly uri: string + text: string +} + +function deferred(): { promise: Promise; resolve(): void; reject(error: unknown): void } { + let resolvePromise: (() => void) | undefined + let rejectPromise: ((error: unknown) => void) | undefined + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve + rejectPromise = reject + }) + return { + promise, + resolve: () => resolvePromise?.(), + reject: (error) => rejectPromise?.(error), + } +} + +function createSubject() { + const currentDocuments = new Map() + const synchronized: { document: TestDocument; text: string }[] = [] + const owners = new Map() + const logError = vi.fn() + const startup = new BundledClientStartup({ + isCurrent: (document) => currentDocuments.get(document.uri) === document, + synchronize: (document) => { + synchronized.push({ document, text: document.text }) + owners.set(document, 'bundled') + return Promise.resolve() + }, + logError, + }) + return { startup, currentDocuments, synchronized, owners, logError } +} + +describe('BundledClientStartup', () => { + test('keeps readiness failure stable until a replacement is installed', async () => { + const subject = createSubject() + const readiness = deferred() + const document = { uri: 'file:///schema.prisma', text: 'model A {}' } + subject.currentDocuments.set(document.uri, document) + + subject.startup.start(() => readiness.promise) + subject.startup.schedule(document) + readiness.reject(new Error('startup failed')) + + await vi.waitFor(() => expect(subject.startup.status).toBe('failed')) + expect(subject.synchronized).toEqual([]) + expect(subject.logError).toHaveBeenCalledOnce() + + subject.startup.schedule(document) + await Promise.resolve() + expect(subject.synchronized).toEqual([]) + expect(subject.logError).toHaveBeenCalledOnce() + + subject.startup.replace(Promise.resolve()) + subject.startup.schedule(document) + await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) + expect(subject.startup.status).toBe('ready') + }) + + test('drops a closed stale instance and synchronizes one reopened replacement', async () => { + const subject = createSubject() + const readiness = deferred() + const stale = { uri: 'file:///schema.prisma', text: 'model Stale {}' } + const replacement = { uri: stale.uri, text: 'model Current {}' } + subject.currentDocuments.set(stale.uri, stale) + + subject.startup.start(() => readiness.promise) + subject.startup.schedule(stale) + subject.currentDocuments.delete(stale.uri) + subject.currentDocuments.set(replacement.uri, replacement) + subject.startup.schedule(replacement) + readiness.resolve() + + await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) + expect(subject.synchronized).toEqual([{ document: replacement, text: replacement.text }]) + expect(subject.owners.get(stale)).toBeUndefined() + expect(subject.owners.get(replacement)).toBe('bundled') + }) + + test('coalesces startup and pending events while synchronizing the latest text', async () => { + const subject = createSubject() + const readiness = deferred() + const startClient = vi.fn(() => readiness.promise) + const document = { uri: 'file:///schema.prisma', text: 'model Initial {}' } + subject.currentDocuments.set(document.uri, document) + + subject.startup.start(startClient) + subject.startup.start(startClient) + subject.startup.schedule(document) + document.text = 'model Changed {}' + subject.startup.schedule(document) + document.text = 'model Latest {}' + subject.startup.schedule(document) + readiness.resolve() + + await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) + expect(startClient).toHaveBeenCalledOnce() + expect(subject.synchronized[0]).toEqual({ document, text: 'model Latest {}' }) + }) + + test('replacement invalidates old readiness and disposal absorbs later rejection', async () => { + const subject = createSubject() + const oldReadiness = deferred() + const replacementReadiness = deferred() + const document = { uri: 'file:///schema.prisma', text: 'model Current {}' } + subject.currentDocuments.set(document.uri, document) + + subject.startup.start(() => oldReadiness.promise) + subject.startup.schedule(document) + subject.startup.replace(replacementReadiness.promise) + subject.startup.schedule(document) + oldReadiness.resolve() + replacementReadiness.resolve() + + await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) + expect(subject.startup.status).toBe('ready') + + const deactivationReadiness = deferred() + subject.startup.replace(deactivationReadiness.promise) + subject.startup.schedule(document) + subject.startup.dispose() + deactivationReadiness.reject(new Error('stopped during startup')) + await Promise.resolve() + + expect(subject.startup.status).toBe('disposed') + expect(subject.synchronized).toHaveLength(1) + expect(subject.logError).not.toHaveBeenCalled() + }) +}) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts new file mode 100644 index 0000000000..22533655f8 --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts @@ -0,0 +1,83 @@ +export type BundledClientStartupStatus = 'idle' | 'starting' | 'ready' | 'failed' | 'disposed' + +export interface BundledClientStartupOptions { + readonly isCurrent: (value: T) => boolean + readonly synchronize: (value: T) => Promise + readonly logError: (error: unknown) => void +} + +export class BundledClientStartup { + private generation = 0 + private readiness: Promise = Promise.resolve(false) + private readonly pending = new Map() + private currentStatus: BundledClientStartupStatus = 'idle' + + constructor(private readonly options: BundledClientStartupOptions) {} + + get status(): BundledClientStartupStatus { + return this.currentStatus + } + + start(startClient: () => Promise): void { + if (this.currentStatus !== 'idle') return + this.install(startClient()) + } + + replace(readiness: Promise): void { + if (this.currentStatus === 'disposed') return + this.install(readiness) + } + + schedule(value: T): void { + if (this.currentStatus === 'idle' || this.currentStatus === 'failed' || this.currentStatus === 'disposed') return + if (this.pending.has(value)) return + + const generation = this.generation + this.pending.set(value, generation) + void this.readiness + .then(async (ready) => { + if (!ready || generation !== this.generation || !this.options.isCurrent(value)) return + await this.options.synchronize(value) + }) + .catch((error: unknown) => this.report(error)) + .finally(() => { + if (this.pending.get(value) === generation) { + this.pending.delete(value) + } + }) + .catch((error: unknown) => this.report(error)) + } + + dispose(): void { + this.generation += 1 + this.currentStatus = 'disposed' + this.pending.clear() + } + + private install(readiness: Promise): void { + const generation = ++this.generation + this.currentStatus = 'starting' + this.pending.clear() + this.readiness = readiness.then( + () => { + if (generation !== this.generation) return false + this.currentStatus = 'ready' + return true + }, + (error: unknown) => { + if (generation !== this.generation) return false + this.currentStatus = 'failed' + this.report(error) + return false + }, + ) + } + + private report(error: unknown): void { + try { + this.options.logError(error) + } catch { + // Logging must never turn handled startup failures back into detached rejections. + } + } +} diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index cbdedc53a9..c7d2d1f1bc 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -26,11 +26,13 @@ import { createBundledClientMiddleware, type BundledClientMiddleware } from './b import { createPrepareDocumentRoutingCommit } from './documentRouting' import { LocalPrismaNextClientRegistry, localPrismaNextClientTestStateCommand } from './localPrismaNextClientRegistry' import { LanguageServerTestStateCollector, languageServerTestStateCommand } from './languageServerTestState' +import { BundledClientStartup } from './bundledClientStartup' let client: LanguageClient let serverModule: string let telemetry: TelemetryReporter let fileWatcher: FileWatcher.type | undefined +let bundledClientStartup: BundledClientStartup | undefined const isDebugMode = () => process.env.VSCODE_DEBUG_MODE === 'true' @@ -155,12 +157,24 @@ const plugin: PrismaVSCodePlugin = { } let started = false - let clientReady = Promise.resolve() + const logBundledClientError = (error: unknown): void => { + console.error('Bundled Prisma Language Server failed', error) + } + bundledClientStartup?.dispose() + const startup = new BundledClientStartup({ + isCurrent: (document) => workspace.textDocuments.includes(document), + synchronize: (document) => ownership.synchronize(document), + logError: logBundledClientError, + }) + bundledClientStartup = startup const needsLanguageServer = (doc: TextDocument): boolean => doc.languageId === 'prisma' && ownership.classify(doc).kind === 'bundled' const synchronizeDocument = (document: TextDocument): void => { - if (document.languageId === 'prisma') { - void clientReady.then(() => ownership.synchronize(document)) + if (document.languageId !== 'prisma') return + if (ownership.classify(document).kind === 'bundled') { + startup.schedule(document) + } else { + void ownership.synchronize(document).catch(logBundledClientError) } } @@ -168,7 +182,7 @@ const plugin: PrismaVSCodePlugin = { if (started) return if (document ? !needsLanguageServer(document) : !workspace.textDocuments.some(needsLanguageServer)) return started = true - clientReady = activateClient(context, clientOptions) + startup.start(() => activateClient(context, clientOptions)) } const restartLanguageServer = async () => { @@ -177,12 +191,14 @@ const plugin: PrismaVSCodePlugin = { return } const serverOptions = getServerOptions(workspace.getConfiguration('prisma'), context) - client = await restartClient(context, client, serverOptions, clientOptions, { + const replacement = restartClient(context, client, serverOptions, clientOptions, { onClientStopped: () => bundledClientMiddleware.resetClientState(), onClientCreated: (replacementClient) => { client = replacementClient }, }) + startup.replace(replacement.then(() => undefined)) + client = await replacement } context.subscriptions.push( @@ -245,7 +261,7 @@ const plugin: PrismaVSCodePlugin = { }), workspace.onDidCloseTextDocument((document) => { if (document.languageId === 'prisma') { - void ownership.close(document) + void ownership.close(document).catch(logBundledClientError) } }), ) @@ -281,6 +297,8 @@ const plugin: PrismaVSCodePlugin = { checkForMinimalColorTheme() }, deactivate: async () => { + bundledClientStartup?.dispose() + bundledClientStartup = undefined if (!client) { return undefined } From 379d0079f6862b6aa46b40e5c3ce5f26d68b742f Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 12:45:17 +0000 Subject: [PATCH 18/43] fix(vscode): contain bundled client stop failures --- .../bundledClientStartup.test.ts | 34 ++++++++++++++++++- .../bundledClientStartup.ts | 29 +++++++++++++--- .../plugins/prisma-language-server/index.ts | 26 +++++++------- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts index c15f8d81ab..f186db71b7 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from 'vitest' -import { BundledClientStartup } from './bundledClientStartup' +import { BundledClientStartup, deactivateBundledClient } from './bundledClientStartup' interface TestDocument { readonly uri: string @@ -132,4 +132,36 @@ describe('BundledClientStartup', () => { expect(subject.synchronized).toHaveLength(1) expect(subject.logError).not.toHaveBeenCalled() }) + + test('deactivation contains stop rejection and absorbs late startup rejection', async () => { + const subject = createSubject() + const readiness = deferred() + const document = { uri: 'file:///schema.prisma', text: 'model Current {}' } + const stopError = new Error('shutdown failed') + subject.currentDocuments.set(document.uri, document) + subject.startup.start(() => readiness.promise) + subject.startup.schedule(document) + + const deactivation = deactivateBundledClient(subject.startup, () => Promise.reject(stopError), subject.logError) + void deactivation + readiness.reject(new Error('late startup failure')) + + await expect(deactivation).resolves.toBeUndefined() + await Promise.resolve() + expect(subject.startup.status).toBe('disposed') + expect(subject.synchronized).toEqual([]) + expect(subject.logError).toHaveBeenCalledOnce() + expect(subject.logError).toHaveBeenCalledWith(stopError) + }) + + test('deactivation preserves one successful graceful stop', async () => { + const subject = createSubject() + const stop = vi.fn(() => Promise.resolve()) + + await expect(deactivateBundledClient(subject.startup, stop, subject.logError)).resolves.toBeUndefined() + + expect(stop).toHaveBeenCalledOnce() + expect(subject.startup.status).toBe('disposed') + expect(subject.logError).not.toHaveBeenCalled() + }) }) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts index 22533655f8..650e07a99b 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts +++ b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts @@ -6,6 +6,21 @@ export interface BundledClientStartupOptions { readonly logError: (error: unknown) => void } +export async function deactivateBundledClient( + startup: BundledClientStartup | undefined, + stop: (() => Promise) | undefined, + logError: (error: unknown) => void, +): Promise { + startup?.dispose() + if (!stop) return + + try { + await stop() + } catch (error) { + reportError(logError, error) + } +} + export class BundledClientStartup { private generation = 0 private readiness: Promise = Promise.resolve(false) @@ -74,10 +89,14 @@ export class BundledClientStartup { } private report(error: unknown): void { - try { - this.options.logError(error) - } catch { - // Logging must never turn handled startup failures back into detached rejections. - } + reportError(this.options.logError, error) + } +} + +function reportError(logError: (error: unknown) => void, error: unknown): void { + try { + logError(error) + } catch { + // Logging must never turn handled lifecycle failures back into detached rejections. } } diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index c7d2d1f1bc..356fbe8168 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -26,7 +26,7 @@ import { createBundledClientMiddleware, type BundledClientMiddleware } from './b import { createPrepareDocumentRoutingCommit } from './documentRouting' import { LocalPrismaNextClientRegistry, localPrismaNextClientTestStateCommand } from './localPrismaNextClientRegistry' import { LanguageServerTestStateCollector, languageServerTestStateCommand } from './languageServerTestState' -import { BundledClientStartup } from './bundledClientStartup' +import { BundledClientStartup, deactivateBundledClient } from './bundledClientStartup' let client: LanguageClient let serverModule: string @@ -35,6 +35,9 @@ let fileWatcher: FileWatcher.type | undefined let bundledClientStartup: BundledClientStartup | undefined const isDebugMode = () => process.env.VSCODE_DEBUG_MODE === 'true' +const logBundledClientError = (error: unknown): void => { + console.error('Bundled Prisma Language Server failed', error) +} const activateClient = async (context: ExtensionContext, clientOptions: LanguageClientOptions): Promise => { const prismaConfig = workspace.getConfiguration('prisma') @@ -157,9 +160,6 @@ const plugin: PrismaVSCodePlugin = { } let started = false - const logBundledClientError = (error: unknown): void => { - console.error('Bundled Prisma Language Server failed', error) - } bundledClientStartup?.dispose() const startup = new BundledClientStartup({ isCurrent: (document) => workspace.textDocuments.includes(document), @@ -296,18 +296,20 @@ const plugin: PrismaVSCodePlugin = { checkForMinimalColorTheme() }, - deactivate: async () => { - bundledClientStartup?.dispose() + deactivate: () => { + const startup = bundledClientStartup + const activeClient = client bundledClientStartup = undefined - if (!client) { - return undefined - } + const deactivation = deactivateBundledClient( + startup, + activeClient ? () => activeClient.stop() : undefined, + logBundledClientError, + ) - if (!isDebugOrTestSession()) { + if (activeClient && !isDebugOrTestSession()) { telemetry.dispose() // eslint-disable-line @typescript-eslint/no-floating-promises } - - return client.stop() + return deactivation }, } From 9c8915573e985ab76a5031d7d5a7207b39ecf84d Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 13:02:21 +0000 Subject: [PATCH 19/43] test(vscode): align routing diagnostics with local CLI --- docs/testing.md | 2 +- .../src/__test__/language-server/README.md | 2 +- .../vscode/src/__test__/workspace.test.ts | 22 ++++++++++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 2a1737d367..19a43a3fa8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -88,7 +88,7 @@ Run the focused minimum-runtime workspace suite with: pnpm --filter prisma test:integration:workspace ``` -This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the real Prisma CLI process; no mock language-server executable is part of the fixture. It covers lazy activation, one client per root, root reuse and independence, exclusive bundled/local ownership, complete-text unsaved directive transfers, diagnostics clearing, and missing-entrypoint behavior. +This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the real Prisma CLI process; no mock language-server executable is part of the fixture. It covers lazy activation, successful real-client initialization per root, root reuse and independence, exclusive bundled/local ownership, complete-text unsaved directive transfers, bundled diagnostic production and transfer-time clearing, and missing-entrypoint behavior. The current Prisma Next CLI does not publish schema diagnostics. The runner's installed `@vscode/test-electron` version always adds `--disable-workspace-trust`, so the Electron workspace is deterministically trusted. It cannot represent Restricted Mode without replacing or bypassing the runner's launch contract. Trust rejection is therefore covered at the production classifier and registry boundaries by focused unit tests; a manual Restricted Mode check remains necessary when validating trust behavior end to end. diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index 96e7df0688..ec6c45446c 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -11,7 +11,7 @@ Run the full minimum-and-latest integration suite with `pnpm test:integration`. pnpm --filter prisma test:integration:workspace ``` -The focused suite verifies that activation and unmarked documents start no local process, each eligible marked root starts exactly one real client, additional documents reuse their root client, roots remain independent, and the missing-entrypoint root has no fallback process. It also observes exclusive bundled/local synchronization, both unsaved directive transfer directions, complete current text/version, URI-scoped diagnostics clearing, and real local diagnostics. +The focused suite verifies that activation and unmarked documents start no local process, each eligible marked root completes exactly one real client initialization handshake, additional documents reuse their root client, roots remain independent, and the missing-entrypoint root has no fallback process. It also observes exclusive bundled/local synchronization, both unsaved directive transfer directions, complete current text/version, and URI-scoped diagnostics clearing. The current Prisma Next CLI does not publish schema diagnostics, so diagnostic production is asserted only while the document is bundled; routing-state observations prove that those diagnostics are cleared during ownership transfers. Test-only routing state is exposed through `prisma.test.languageServerRoutingState`. The command is registered only when `isDebugOrTestSession()` is true; production sessions do not install the observer or retain observed document contents. The state contains no process handles. diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index d118a8fb5c..9892bcd129 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -77,7 +77,11 @@ suite('Multi-root integration workspace', () => { const invalidSchema = 'model Broken {\n id Missing\n}\n' await replaceDocument(documentA, invalidSchema) - await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) + await waitForDiagnostics( + documentA.uri, + (diagnostics) => diagnostics.length > 0, + 'bundled diagnostics before adding the directive', + ) const markedInvalidSchema = `// use prisma-next\n${invalidSchema}` const addDirectiveEventIndex = initialState.routingEvents.length @@ -88,6 +92,11 @@ suite('Multi-root integration workspace', () => { lastOpenedAfter(state, documentA.uri, addDirectiveEventIndex)?.owner.kind === 'local', 'root A local client and marked document synchronization', ) + assert.strictEqual( + localAState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], + 1, + 'expected the real root A client to complete its initialization handshake', + ) const localAOpen = lastOpenedAfter(localAState, documentA.uri, addDirectiveEventIndex) assert.ok(localAOpen) assert.strictEqual(localAOpen.documentText, markedInvalidSchema) @@ -103,8 +112,6 @@ suite('Multi-root integration workspace', () => { ) assertExclusiveOwners(localAState) - await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length > 0) - const markedSecondSchema = '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n' const secondAEventIndex = localAState.routingEvents.length await replaceDocument(secondDocumentA, markedSecondSchema) @@ -180,7 +187,11 @@ suite('Multi-root integration workspace', () => { assert.deepStrictEqual(activeOwnerKeys(restoredState, documentA.uri), new Set(['bundled'])) assert.strictEqual(restoredState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) assertExclusiveOwners(restoredState) - await waitForDiagnostics(documentA.uri, (diagnostics) => diagnostics.length === 0) + await waitForDiagnostics( + documentA.uri, + (diagnostics) => diagnostics.length === 0, + 'bundled diagnostics to clear after restoring valid text', + ) } finally { await restoreFixtures(fixtures) } @@ -210,6 +221,7 @@ async function waitForState( async function waitForDiagnostics( uri: vscode.Uri, predicate: (diagnostics: readonly vscode.Diagnostic[]) => boolean, + description: string, ): Promise { const deadline = Date.now() + diagnosticTimeoutMs let diagnostics = vscode.languages.getDiagnostics(uri) @@ -217,7 +229,7 @@ async function waitForDiagnostics( await sleep(100) diagnostics = vscode.languages.getDiagnostics(uri) } - assert.ok(predicate(diagnostics), `Timed out waiting for diagnostics for ${uri.toString()}`) + assert.ok(predicate(diagnostics), `Timed out waiting for ${description} for ${uri.toString()}`) return diagnostics } From bfb0210767de818126d866818cc63667096bf021 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 13:15:10 +0000 Subject: [PATCH 20/43] test(vscode): preserve document EOL in routing E2E --- .../vscode/src/__test__/workspace.test.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index 9892bcd129..634492deea 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -75,7 +75,7 @@ suite('Multi-root integration workspace', () => { assert.deepStrictEqual(initialState.localClients.startCountsByWorkspaceFolderUri, {}) assertExclusiveOwners(initialState) - const invalidSchema = 'model Broken {\n id Missing\n}\n' + const invalidSchema = withDocumentEol(documentA, 'model Broken {\n id Missing\n}\n') await replaceDocument(documentA, invalidSchema) await waitForDiagnostics( documentA.uri, @@ -83,7 +83,7 @@ suite('Multi-root integration workspace', () => { 'bundled diagnostics before adding the directive', ) - const markedInvalidSchema = `// use prisma-next\n${invalidSchema}` + const markedInvalidSchema = withDocumentEol(documentA, `// use prisma-next\n${invalidSchema}`) const addDirectiveEventIndex = initialState.routingEvents.length await replaceDocument(documentA, markedInvalidSchema) const localAState = await waitForState( @@ -112,7 +112,10 @@ suite('Multi-root integration workspace', () => { ) assertExclusiveOwners(localAState) - const markedSecondSchema = '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n' + const markedSecondSchema = withDocumentEol( + secondDocumentA, + '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n', + ) const secondAEventIndex = localAState.routingEvents.length await replaceDocument(secondDocumentA, markedSecondSchema) const reusedAState = await waitForState( @@ -126,7 +129,7 @@ suite('Multi-root integration workspace', () => { ) assertExclusiveOwners(reusedAState) - const markedRootBSchema = '// use prisma-next\nmodel RootBRecord {\n id Int @id\n}\n' + const markedRootBSchema = withDocumentEol(documentB, '// use prisma-next\nmodel RootBRecord {\n id Int @id\n}\n') const rootBEventIndex = reusedAState.routingEvents.length await replaceDocument(documentB, markedRootBSchema) const independentRootsState = await waitForState( @@ -146,7 +149,10 @@ suite('Multi-root integration workspace', () => { ) assertExclusiveOwners(independentRootsState) - const markedMissingSchema = '// use prisma-next\nmodel MissingCliRecord {\n id Int @id\n}\n' + const markedMissingSchema = withDocumentEol( + missingDocument, + '// use prisma-next\nmodel MissingCliRecord {\n id Int @id\n}\n', + ) const missingEventIndex = independentRootsState.routingEvents.length const missingOwnershipEventIndex = independentRootsState.ownershipEvents.length await replaceDocument(missingDocument, markedMissingSchema) @@ -168,7 +174,7 @@ suite('Multi-root integration workspace', () => { assert.strictEqual(lastOpenedAfter(missingState, missingDocument.uri, missingEventIndex), undefined) assertExclusiveOwners(missingState) - const restoredBundledSchema = 'model RootARestored {\n id Int @id\n}\n' + const restoredBundledSchema = withDocumentEol(documentA, 'model RootARestored {\n id Int @id\n}\n') const removeDirectiveEventIndex = missingState.routingEvents.length await replaceDocument(documentA, restoredBundledSchema) const restoredState = await waitForState( @@ -238,6 +244,11 @@ async function replaceDocument(document: vscode.TextDocument, text: string): Pro assert.strictEqual(document.isDirty, true) } +function withDocumentEol(document: vscode.TextDocument, text: string): string { + const eol = document.eol === vscode.EndOfLine.CRLF ? '\r\n' : '\n' + return text.split('\r\n').join('\n').split('\n').join(eol) +} + interface FixtureSnapshot { readonly uri: vscode.Uri readonly bytes: Uint8Array From 6dee7f7c18fd6f167052675aacc92431bdd71f65 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 13:45:49 +0000 Subject: [PATCH 21/43] test(vscode): support renamed macOS executable --- .github/workflows/4_e2e_tests.yml | 2 +- packages/vscode/package.json | 2 +- pnpm-lock.yaml | 49 +++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/workflows/4_e2e_tests.yml b/.github/workflows/4_e2e_tests.yml index 63efe9587c..d5ffa6ad47 100644 --- a/.github/workflows/4_e2e_tests.yml +++ b/.github/workflows/4_e2e_tests.yml @@ -41,7 +41,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Install Dependencies run: pnpm install diff --git a/packages/vscode/package.json b/packages/vscode/package.json index ea4bef2e73..a09d5c11e7 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -689,7 +689,7 @@ "@types/node": "20.14.8", "@types/sinon": "^20.0.0", "@types/vscode": "1.104.0", - "@vscode/test-electron": "2.4.1", + "@vscode/test-electron": "3.1.0", "@vscode/vsce": "2.29.0", "esbuild": "^0.27.1", "glob": "8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a53744e4bd..d1c23f5d1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,8 +209,8 @@ importers: specifier: 1.104.0 version: 1.104.0 '@vscode/test-electron': - specifier: 2.4.1 - version: 2.4.1 + specifier: 3.1.0 + version: 3.1.0 '@vscode/vsce': specifier: 2.29.0 version: 2.29.0 @@ -2351,6 +2351,10 @@ packages: resolution: {integrity: sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==} engines: {node: '>=16'} + '@vscode/test-electron@3.1.0': + resolution: {integrity: sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==} + engines: {node: '>=22'} + '@vscode/vsce-sign-alpine-arm64@2.0.6': resolution: {integrity: sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==} cpu: [arm64] @@ -4047,6 +4051,10 @@ packages: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -4371,6 +4379,10 @@ packages: resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==} engines: {node: '>=16'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + os-paths@7.4.0: resolution: {integrity: sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA==} engines: {node: '>= 4.0'} @@ -4962,6 +4974,10 @@ packages: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + streamx@2.28.0: resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} @@ -7912,6 +7928,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@vscode/test-electron@3.1.0': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + jszip: 3.10.1 + ora: 8.2.0 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + '@vscode/vsce-sign-alpine-arm64@2.0.6': optional: true @@ -9787,6 +9813,11 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + log-update@6.1.0: dependencies: ansi-escapes: 7.2.0 @@ -10125,6 +10156,18 @@ snapshots: string-width: 6.1.0 strip-ansi: 7.1.2 + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + os-paths@7.4.0: optionalDependencies: fsevents: 2.3.3 @@ -10781,6 +10824,8 @@ snapshots: dependencies: bl: 5.1.0 + stdin-discarder@0.2.2: {} + streamx@2.28.0: dependencies: events-universal: 1.0.1 From fb1701c1b0f84e09ec0aadc20194aa410c4bf7bb Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 13:52:25 +0000 Subject: [PATCH 22/43] ci: run extension e2e harness on Node 22 --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 29d7dcbf85..a139943e19 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -42,7 +42,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Install Dependencies run: pnpm install From ff848273332da1f1bf6ca4147ad16f31212934e8 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 14:04:42 +0000 Subject: [PATCH 23/43] fix(vscode): make studio asset copy repeatable --- packages/vscode/esbuild.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vscode/esbuild.mjs b/packages/vscode/esbuild.mjs index 498d8f3578..b26081d3dc 100644 --- a/packages/vscode/esbuild.mjs +++ b/packages/vscode/esbuild.mjs @@ -258,7 +258,8 @@ function copyStaticAssets() { } console.log('Copying @prisma/studio-core-licensed static assets...') - // Use dereference to resolve symlinks (important for pnpm) + // Replace the previous copy so repeated builds work with pnpm's symlinked package directory. + rmSync(studioDest, { recursive: true, force: true }) cpSync(studioSrc, studioDest, { recursive: true, dereference: true }) // Copy prisma-schema-wasm WASM file to Prisma 6 language server directory From 89f6f7b94076d0d2b751ba1b2ddfbd53fd7939fa Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 14:34:44 +0000 Subject: [PATCH 24/43] test(vscode): isolate Playwright Electron attempts --- packages/vscode/playwright.config.ts | 1 + packages/vscode/tests/playwright/README.md | 4 +- .../vscode/tests/playwright/extension.spec.ts | 24 +-- .../tests/playwright/utils/page-helper.ts | 6 +- .../playwright/utils/vscode-lifecycle.ts | 140 +++++++++++++++ .../utils/vscode-lifecycle.unit.test.ts | 115 +++++++++++++ .../tests/playwright/utils/vscode-setup.ts | 161 +++++++++++++++--- 7 files changed, 409 insertions(+), 42 deletions(-) create mode 100644 packages/vscode/tests/playwright/utils/vscode-lifecycle.ts create mode 100644 packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts diff --git a/packages/vscode/playwright.config.ts b/packages/vscode/playwright.config.ts index 09577a23c0..d9dcd30044 100644 --- a/packages/vscode/playwright.config.ts +++ b/packages/vscode/playwright.config.ts @@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test' export default defineConfig({ testDir: './tests/playwright', + testIgnore: '**/*.unit.test.ts', timeout: process.env.CI ? 120000 : 60000, // Longer timeout for CI fullyParallel: false, forbidOnly: !!process.env.CI, diff --git a/packages/vscode/tests/playwright/README.md b/packages/vscode/tests/playwright/README.md index 3d23591ee9..fd1b9b3db8 100644 --- a/packages/vscode/tests/playwright/README.md +++ b/packages/vscode/tests/playwright/README.md @@ -104,14 +104,14 @@ The tests use the following VS Code launch arguments for reliable testing: - `--disable-workspace-trust`: Skips workspace trust prompts - `--skip-welcome` / `--skip-release-notes`: Skips intro screens - `--no-sandbox`, `--disable-dev-shm-usage`, `--disable-gpu`: Stability flags for headless environments -- `--user-data-dir`: Uses a temporary user data directory +- `--user-data-dir` / `--extensions-dir`: Use isolated temporary directories for every test attempt, retry, and worker - `--wait`: Prevents VS Code from exiting immediately ## Key Implementation Details ### Window Detection -The tests wait for VS Code windows to appear and use `electronApp.firstWindow()` to get the main window. +The test session waits for the first VS Code window before handing the page to test helpers. If VS Code exits first, the launch error includes its exit code or signal and bounded, redacted stdout/stderr. Teardown remains bounded when Electron has already exited and removes only the current attempt's temporary root. ### Workbench Loading diff --git a/packages/vscode/tests/playwright/extension.spec.ts b/packages/vscode/tests/playwright/extension.spec.ts index 12045a6acd..afb0c74abc 100644 --- a/packages/vscode/tests/playwright/extension.spec.ts +++ b/packages/vscode/tests/playwright/extension.spec.ts @@ -1,31 +1,33 @@ import path from 'node:path' import { test, expect } from '@playwright/test' -import type { ElectronApplication } from '@playwright/test' -import { setupVSCode } from './utils/vscode-setup' +import { setupVSCode, type VSCodeTestSession } from './utils/vscode-setup' import { VSCodePageHelper } from './utils/page-helper' import { COMMANDS, TIMEOUTS, TEST_DATA } from './utils/constants' -let electronApp: ElectronApplication +let vscodeSession: VSCodeTestSession | undefined const rootPath = path.resolve(__dirname, '../../') const testWorkspace = path.join(__dirname, '../fixtures/test-workspace') -test.beforeEach(async () => { - electronApp = await setupVSCode({ +test.beforeEach(async ({}, testInfo) => { + vscodeSession = undefined + vscodeSession = await setupVSCode({ rootPath, testWorkspace, disableExtensions: true, timeout: TIMEOUTS.VSCODE_LAUNCH, + workerIndex: testInfo.workerIndex, + retry: testInfo.retry, }) }) test('launches VS Code with Prisma extension', async () => { - await VSCodePageHelper.create(electronApp) + await VSCodePageHelper.create(vscodeSession!.page) }) test('can execute Prisma: Launch Prisma Studio command', async () => { - const helper = await VSCodePageHelper.create(electronApp) + const helper = await VSCodePageHelper.create(vscodeSession!.page) // Execute the command with a fake database URL await helper.executeCommandWithInput(COMMANDS.LAUNCH_PRISMA_STUDIO, TEST_DATA.FAKE_DATABASE_URL) @@ -44,7 +46,7 @@ test('can execute Prisma: Launch Prisma Studio command', async () => { }) test('loads Prisma schema file in workspace', async () => { - const helper = await VSCodePageHelper.create(electronApp) + const helper = await VSCodePageHelper.create(vscodeSession!.page) await helper.openFile('schema.prisma') @@ -58,7 +60,7 @@ test('loads Prisma schema file in workspace', async () => { }) test.afterEach(async () => { - if (electronApp) { - await electronApp.close() - } + const session = vscodeSession + vscodeSession = undefined + await session?.close() }) diff --git a/packages/vscode/tests/playwright/utils/page-helper.ts b/packages/vscode/tests/playwright/utils/page-helper.ts index f7bab7c42e..5181288041 100644 --- a/packages/vscode/tests/playwright/utils/page-helper.ts +++ b/packages/vscode/tests/playwright/utils/page-helper.ts @@ -1,4 +1,4 @@ -import type { Page, ElectronApplication } from '@playwright/test' +import type { Page } from '@playwright/test' import { TIMEOUTS, SELECTORS, KEYBOARD_SHORTCUTS, WAIT_TIMES } from './constants' export interface PageHelperTimeouts { @@ -22,9 +22,7 @@ export class VSCodePageHelper { } } - static async create(electronApp: ElectronApplication, timeouts?: PageHelperTimeouts): Promise { - const page = await electronApp.firstWindow() - + static async create(page: Page, timeouts?: PageHelperTimeouts): Promise { const helper = new VSCodePageHelper(page, timeouts) await helper.waitForWorkbench() diff --git a/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts b/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts new file mode 100644 index 0000000000..1a1e61f8d9 --- /dev/null +++ b/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts @@ -0,0 +1,140 @@ +import { rmSync } from 'node:fs' +import { mkdtemp, mkdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import type { ChildProcess } from 'node:child_process' + +const OUTPUT_LIMIT = 16 * 1024 + +export interface VSCodeTempDirectories { + root: string + userData: string + extensions: string +} + +export interface ProcessExitDetails { + code: number | null + signal: NodeJS.Signals | null + stdout: string + stderr: string +} + +export async function createVSCodeTempDirectories( + workerIndex: number, + retry: number, + parentDirectory = tmpdir(), +): Promise { + const root = await mkdtemp(path.join(parentDirectory, `prisma-vscode-playwright-w${workerIndex}-r${retry}-`)) + const userData = path.join(root, 'user-data') + const extensions = path.join(root, 'extensions') + + await Promise.all([mkdir(userData), mkdir(extensions)]) + + return { root, userData, extensions } +} + +export async function cleanupVSCodeTempDirectories(directories: VSCodeTempDirectories): Promise { + await rm(directories.root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }) +} + +export class VSCodeTempDirectoryLease { + private cleaned = false + private readonly cleanupOnProcessExit = () => { + try { + rmSync(this.directories.root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }) + } catch { + // The process is already exiting; normal teardown reports cleanup failures when it can. + } + } + + constructor(readonly directories: VSCodeTempDirectories) { + process.once('exit', this.cleanupOnProcessExit) + } + + async cleanup(): Promise { + if (this.cleaned) { + return + } + + await cleanupVSCodeTempDirectories(this.directories) + this.cleaned = true + process.off('exit', this.cleanupOnProcessExit) + } +} + +export function formatProcessExit(details: ProcessExitDetails): string { + const status = details.signal ? `signal ${details.signal}` : `code ${details.code ?? 'unknown'}` + const output = [ + details.stdout && `stdout (last ${OUTPUT_LIMIT} bytes):\n${sanitizeDiagnosticText(details.stdout)}`, + details.stderr && `stderr (last ${OUTPUT_LIMIT} bytes):\n${sanitizeDiagnosticText(details.stderr)}`, + ].filter(Boolean) + + return [`VS Code process exited with ${status}.`, ...output].join('\n') +} + +export class ProcessDiagnostics { + private readonly stdout = new BoundedOutput(OUTPUT_LIMIT) + private readonly stderr = new BoundedOutput(OUTPUT_LIMIT) + private code: number | null + private signal: NodeJS.Signals | null + private readonly stdoutListener = (chunk: Buffer | string) => this.stdout.append(chunk) + private readonly stderrListener = (chunk: Buffer | string) => this.stderr.append(chunk) + private readonly exitListener = (code: number | null, signal: NodeJS.Signals | null) => { + this.code = code + this.signal = signal + } + + constructor(private readonly child: ChildProcess) { + this.code = child.exitCode + this.signal = child.signalCode + child.stdout?.on('data', this.stdoutListener) + child.stderr?.on('data', this.stderrListener) + child.on('exit', this.exitListener) + } + + hasExited(): boolean { + return this.child.exitCode !== null || this.child.signalCode !== null || this.code !== null || this.signal !== null + } + + format(): string { + return formatProcessExit({ + code: this.child.exitCode ?? this.code, + signal: this.child.signalCode ?? this.signal, + stdout: this.stdout.toString(), + stderr: this.stderr.toString(), + }) + } + + dispose(): void { + this.child.stdout?.off('data', this.stdoutListener) + this.child.stderr?.off('data', this.stderrListener) + this.child.off('exit', this.exitListener) + } +} + +class BoundedOutput { + private value = Buffer.alloc(0) + + constructor(private readonly limit: number) {} + + append(chunk: Buffer | string): void { + const next = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + this.value = Buffer.concat([this.value, next]).subarray(-this.limit) + } + + toString(): string { + return this.value.toString('utf8').trim() + } +} + +export function sanitizeDiagnosticText(output: string): string { + return output + .replace(/([a-z][a-z\d+.-]*:\/\/[^\s:/@]+:)[^\s@/]+@/gi, '$1[REDACTED]@') + .replace(/\b(authorization|api[-_]?key|password|secret|token)\b(\s*[:=]\s*)([^\s,;]+)/gi, '$1$2[REDACTED]') +} + +export function terminateChildProcess(child: ChildProcess): void { + if (child.exitCode === null && child.signalCode === null) { + child.kill() + } +} diff --git a/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts b/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts new file mode 100644 index 0000000000..40cc03cbf4 --- /dev/null +++ b/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts @@ -0,0 +1,115 @@ +import type { ChildProcess } from 'node:child_process' +import { EventEmitter } from 'node:events' +import { access, mkdtemp, mkdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { PassThrough } from 'node:stream' +import { afterEach, describe, expect, test } from 'vitest' +import { + cleanupVSCodeTempDirectories, + createVSCodeTempDirectories, + formatProcessExit, + ProcessDiagnostics, + VSCodeTempDirectoryLease, +} from './vscode-lifecycle' + +const temporaryParents: string[] = [] + +afterEach(async () => { + await Promise.all(temporaryParents.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))) +}) + +describe('VS Code Playwright lifecycle', () => { + test('creates collision-free user data and extensions directories per attempt', async () => { + const parent = await createTemporaryParent() + const [first, second] = await Promise.all([ + createVSCodeTempDirectories(2, 1, parent), + createVSCodeTempDirectories(2, 1, parent), + ]) + + expect(first.root).not.toBe(second.root) + expect(first.userData).toBe(path.join(first.root, 'user-data')) + expect(first.extensions).toBe(path.join(first.root, 'extensions')) + await expect(access(first.userData)).resolves.toBeUndefined() + await expect(access(first.extensions)).resolves.toBeUndefined() + }) + + test('cleans only the isolated attempt root', async () => { + const parent = await createTemporaryParent() + const directories = await createVSCodeTempDirectories(0, 0, parent) + const sibling = path.join(parent, 'keep-me') + await mkdir(sibling) + + await cleanupVSCodeTempDirectories(directories) + + await expect(access(directories.root)).rejects.toThrow() + await expect(access(sibling)).resolves.toBeUndefined() + }) + + test('registers process-exit cleanup only for the lifetime of an attempt', async () => { + const parent = await createTemporaryParent() + const directories = await createVSCodeTempDirectories(0, 0, parent) + const initialListeners = process.listenerCount('exit') + const lease = new VSCodeTempDirectoryLease(directories) + + expect(process.listenerCount('exit')).toBe(initialListeners + 1) + await lease.cleanup() + + expect(process.listenerCount('exit')).toBe(initialListeners) + await expect(access(directories.root)).rejects.toThrow() + }) + + test('formats exit code, signal, bounded output labels, and redacts secrets', () => { + const byCode = formatProcessExit({ + code: 9, + signal: null, + stdout: 'server ready', + stderr: 'DATABASE_URL=postgres://user:password@example.test/db token=do-not-print', + }) + const bySignal = formatProcessExit({ code: null, signal: 'SIGTERM', stdout: '', stderr: '' }) + + expect(byCode).toContain('exited with code 9') + expect(byCode).toContain('stdout (last 16384 bytes):\nserver ready') + expect(byCode).toContain('postgres://user:[REDACTED]@example.test/db') + expect(byCode).toContain('token=[REDACTED]') + expect(byCode).not.toContain('do-not-print') + expect(bySignal).toBe('VS Code process exited with signal SIGTERM.') + }) + + test('bounds child output and removes all diagnostic listeners', () => { + const stdout = new PassThrough() + const stderr = new PassThrough() + const child = Object.assign(new EventEmitter(), { + stdout, + stderr, + exitCode: null, + signalCode: null, + kill: () => true, + }) as unknown as ChildProcess + const diagnostics = new ProcessDiagnostics(child) + + stdout.write('discarded-output'.repeat(2000)) + stdout.write('final-output') + child.emit('exit', 7, null) + + const formatted = diagnostics.format() + expect(formatted).toContain('exited with code 7') + expect(formatted).toContain('final-output') + expect(Buffer.byteLength(formatted)).toBeLessThan(17 * 1024) + expect(stdout.listenerCount('data')).toBe(1) + expect(stderr.listenerCount('data')).toBe(1) + expect(child.listenerCount('exit')).toBe(1) + + diagnostics.dispose() + + expect(stdout.listenerCount('data')).toBe(0) + expect(stderr.listenerCount('data')).toBe(0) + expect(child.listenerCount('exit')).toBe(0) + }) +}) + +async function createTemporaryParent(): Promise { + const parent = await mkdtemp(path.join(tmpdir(), 'prisma-vscode-lifecycle-test-')) + temporaryParents.push(parent) + return parent +} diff --git a/packages/vscode/tests/playwright/utils/vscode-setup.ts b/packages/vscode/tests/playwright/utils/vscode-setup.ts index 006ff30632..ede0bb149f 100644 --- a/packages/vscode/tests/playwright/utils/vscode-setup.ts +++ b/packages/vscode/tests/playwright/utils/vscode-setup.ts @@ -1,39 +1,150 @@ +import type { ChildProcess } from 'node:child_process' import path from 'node:path' import { downloadAndUnzipVSCode } from '@vscode/test-electron' import { _electron as electron } from '@playwright/test' -import type { ElectronApplication } from '@playwright/test' +import type { ElectronApplication, Page } from '@playwright/test' +import { + createVSCodeTempDirectories, + ProcessDiagnostics, + sanitizeDiagnosticText, + terminateChildProcess, + VSCodeTempDirectoryLease, + type VSCodeTempDirectories, +} from './vscode-lifecycle' export interface VSCodeSetupOptions { rootPath: string testWorkspace: string disableExtensions?: boolean timeout?: number + workerIndex: number + retry: number } -export async function setupVSCode(options: VSCodeSetupOptions): Promise { - const { rootPath, testWorkspace, disableExtensions = true, timeout = 30000 } = options - - const executablePath = await downloadAndUnzipVSCode() - - const args = [ - '--extensionDevelopmentPath=' + rootPath, - ...(disableExtensions ? ['--disable-extensions'] : []), - '--disable-workspace-trust', - '--skip-welcome', - '--skip-release-notes', - '--no-sandbox', - '--disable-dev-shm-usage', - '--disable-gpu', - '--user-data-dir=' + path.join(__dirname, '../tmp/user-data'), - '--wait', - testWorkspace, - ] - - const electronApp = await electron.launch({ - executablePath, - args, - timeout, +export interface VSCodeTestSession { + page: Page + close(): Promise +} + +export async function setupVSCode(options: VSCodeSetupOptions): Promise { + const { rootPath, testWorkspace, disableExtensions = true, timeout = 30000, workerIndex, retry } = options + const directories = await createVSCodeTempDirectories(workerIndex, retry) + const directoryLease = new VSCodeTempDirectoryLease(directories) + let electronApp: ElectronApplication | undefined + let diagnostics: ProcessDiagnostics | undefined + + try { + const executablePath = await downloadAndUnzipVSCode() + const args = [ + '--extensionDevelopmentPath=' + rootPath, + ...(disableExtensions ? ['--disable-extensions'] : []), + '--disable-workspace-trust', + '--skip-welcome', + '--skip-release-notes', + '--no-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--user-data-dir=' + directories.userData, + '--extensions-dir=' + directories.extensions, + '--wait', + testWorkspace, + ] + + electronApp = await electron.launch({ + executablePath, + args, + timeout, + }) + diagnostics = new ProcessDiagnostics(electronApp.process()) + + try { + const page = await electronApp.firstWindow({ timeout }) + return { + page, + close: () => closeVSCodeSession(electronApp, diagnostics, directoryLease), + } + } catch (error) { + throw createLifecycleError('waiting for its first window', error, directories, diagnostics) + } + } catch (error) { + await closeVSCodeSession(electronApp, diagnostics, directoryLease) + + if (error instanceof VSCodeLifecycleError) { + throw error + } + + throw createLifecycleError('launching', error, directories, diagnostics) + } +} + +class VSCodeLifecycleError extends Error {} + +function createLifecycleError( + stage: string, + error: unknown, + directories: VSCodeTempDirectories, + diagnostics?: ProcessDiagnostics, +): VSCodeLifecycleError { + const cause = sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)) + const processContext = diagnostics?.hasExited() ? `\n${diagnostics.format()}` : '' + + return new VSCodeLifecycleError( + `VS Code failed while ${stage} (isolated attempt ${path.basename(directories.root)}): ${cause}${processContext}`, + ) +} + +async function closeVSCodeSession( + electronApp: ElectronApplication | undefined, + diagnostics: ProcessDiagnostics | undefined, + directoryLease: VSCodeTempDirectoryLease, +): Promise { + try { + if (electronApp && !diagnostics?.hasExited()) { + const closed = await settleWithin(electronApp.close(), 5000) + if (!closed) { + await terminateAndWait(electronApp.process()) + } + } + } catch { + if (electronApp) { + await terminateAndWait(electronApp.process()) + } + } finally { + diagnostics?.dispose() + await directoryLease.cleanup() + } +} + +async function terminateAndWait(child: ChildProcess): Promise { + terminateChildProcess(child) + if (child.exitCode !== null || child.signalCode !== null) { + return + } + + let onExit: (() => void) | undefined + const exited = new Promise((resolve) => { + onExit = resolve + child.once('exit', onExit) + }) + + await settleWithin(exited, 2000) + if (onExit) { + child.off('exit', onExit) + } +} + +async function settleWithin(operation: Promise, timeout: number): Promise { + let timer: NodeJS.Timeout | undefined + const timedOut = new Promise((resolve) => { + timer = setTimeout(() => resolve(false), timeout) + timer.unref() }) - return electronApp + try { + return await Promise.race([operation.then(() => true), timedOut]) + } finally { + if (timer) { + clearTimeout(timer) + } + } } From cea546b87ebfb794b648ae18149bdfb776fcd4b9 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Mon, 24 Aug 2026 15:39:32 +0000 Subject: [PATCH 25/43] fix(vscode): shorten Playwright IPC paths on macOS --- packages/vscode/tests/playwright/README.md | 4 +- .../playwright/utils/vscode-lifecycle.ts | 71 +++++----------- .../utils/vscode-lifecycle.unit.test.ts | 82 ++++++++++--------- .../tests/playwright/utils/vscode-setup.ts | 43 ++++++---- 4 files changed, 90 insertions(+), 110 deletions(-) diff --git a/packages/vscode/tests/playwright/README.md b/packages/vscode/tests/playwright/README.md index fd1b9b3db8..80b17e75af 100644 --- a/packages/vscode/tests/playwright/README.md +++ b/packages/vscode/tests/playwright/README.md @@ -104,14 +104,14 @@ The tests use the following VS Code launch arguments for reliable testing: - `--disable-workspace-trust`: Skips workspace trust prompts - `--skip-welcome` / `--skip-release-notes`: Skips intro screens - `--no-sandbox`, `--disable-dev-shm-usage`, `--disable-gpu`: Stability flags for headless environments -- `--user-data-dir` / `--extensions-dir`: Use isolated temporary directories for every test attempt, retry, and worker +- `--user-data-dir` / `--extensions-dir`: Use isolated short temporary directories for every test attempt, retry, and worker (`/tmp/pv-XXXXXX/{u,e}` on macOS to keep VS Code IPC socket paths below the platform limit) - `--wait`: Prevents VS Code from exiting immediately ## Key Implementation Details ### Window Detection -The test session waits for the first VS Code window before handing the page to test helpers. If VS Code exits first, the launch error includes its exit code or signal and bounded, redacted stdout/stderr. Teardown remains bounded when Electron has already exited and removes only the current attempt's temporary root. +The test session waits for the first VS Code window before handing the page to test helpers. If VS Code exits first, the launch error includes only structural context: phase, in-memory attempt ID, executable basename, exit code, and signal. Teardown remains bounded when Electron has already exited and removes only the current attempt's temporary root. ### Workbench Loading diff --git a/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts b/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts index 1a1e61f8d9..3c79f3ed52 100644 --- a/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts +++ b/packages/vscode/tests/playwright/utils/vscode-lifecycle.ts @@ -1,10 +1,8 @@ +import type { ChildProcess } from 'node:child_process' import { rmSync } from 'node:fs' import { mkdtemp, mkdir, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' -import type { ChildProcess } from 'node:child_process' - -const OUTPUT_LIMIT = 16 * 1024 export interface VSCodeTempDirectories { root: string @@ -15,26 +13,34 @@ export interface VSCodeTempDirectories { export interface ProcessExitDetails { code: number | null signal: NodeJS.Signals | null - stdout: string - stderr: string +} + +export function getVSCodeTempBase(platform: NodeJS.Platform = process.platform): string { + return platform === 'darwin' ? '/tmp' : tmpdir() } export async function createVSCodeTempDirectories( - workerIndex: number, - retry: number, - parentDirectory = tmpdir(), + parentDirectory = getVSCodeTempBase(), ): Promise { - const root = await mkdtemp(path.join(parentDirectory, `prisma-vscode-playwright-w${workerIndex}-r${retry}-`)) - const userData = path.join(root, 'user-data') - const extensions = path.join(root, 'extensions') + const root = await mkdtemp(path.join(parentDirectory, 'pv-')) + const userData = path.join(root, 'u') + const extensions = path.join(root, 'e') await Promise.all([mkdir(userData), mkdir(extensions)]) return { root, userData, extensions } } -export async function cleanupVSCodeTempDirectories(directories: VSCodeTempDirectories): Promise { - await rm(directories.root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }) +type RemoveDirectory = ( + directory: string, + options: { recursive: true; force: true; maxRetries: number; retryDelay: number }, +) => Promise + +export async function cleanupVSCodeTempDirectories( + directories: VSCodeTempDirectories, + removeDirectory: RemoveDirectory = rm, +): Promise { + await removeDirectory(directories.root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }) } export class VSCodeTempDirectoryLease { @@ -63,22 +69,12 @@ export class VSCodeTempDirectoryLease { } export function formatProcessExit(details: ProcessExitDetails): string { - const status = details.signal ? `signal ${details.signal}` : `code ${details.code ?? 'unknown'}` - const output = [ - details.stdout && `stdout (last ${OUTPUT_LIMIT} bytes):\n${sanitizeDiagnosticText(details.stdout)}`, - details.stderr && `stderr (last ${OUTPUT_LIMIT} bytes):\n${sanitizeDiagnosticText(details.stderr)}`, - ].filter(Boolean) - - return [`VS Code process exited with ${status}.`, ...output].join('\n') + return `exitCode=${details.code ?? 'unknown'} signal=${details.signal ?? 'none'}` } export class ProcessDiagnostics { - private readonly stdout = new BoundedOutput(OUTPUT_LIMIT) - private readonly stderr = new BoundedOutput(OUTPUT_LIMIT) private code: number | null private signal: NodeJS.Signals | null - private readonly stdoutListener = (chunk: Buffer | string) => this.stdout.append(chunk) - private readonly stderrListener = (chunk: Buffer | string) => this.stderr.append(chunk) private readonly exitListener = (code: number | null, signal: NodeJS.Signals | null) => { this.code = code this.signal = signal @@ -87,8 +83,6 @@ export class ProcessDiagnostics { constructor(private readonly child: ChildProcess) { this.code = child.exitCode this.signal = child.signalCode - child.stdout?.on('data', this.stdoutListener) - child.stderr?.on('data', this.stderrListener) child.on('exit', this.exitListener) } @@ -100,39 +94,14 @@ export class ProcessDiagnostics { return formatProcessExit({ code: this.child.exitCode ?? this.code, signal: this.child.signalCode ?? this.signal, - stdout: this.stdout.toString(), - stderr: this.stderr.toString(), }) } dispose(): void { - this.child.stdout?.off('data', this.stdoutListener) - this.child.stderr?.off('data', this.stderrListener) this.child.off('exit', this.exitListener) } } -class BoundedOutput { - private value = Buffer.alloc(0) - - constructor(private readonly limit: number) {} - - append(chunk: Buffer | string): void { - const next = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) - this.value = Buffer.concat([this.value, next]).subarray(-this.limit) - } - - toString(): string { - return this.value.toString('utf8').trim() - } -} - -export function sanitizeDiagnosticText(output: string): string { - return output - .replace(/([a-z][a-z\d+.-]*:\/\/[^\s:/@]+:)[^\s@/]+@/gi, '$1[REDACTED]@') - .replace(/\b(authorization|api[-_]?key|password|secret|token)\b(\s*[:=]\s*)([^\s,;]+)/gi, '$1$2[REDACTED]') -} - export function terminateChildProcess(child: ChildProcess): void { if (child.exitCode === null && child.signalCode === null) { child.kill() diff --git a/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts b/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts index 40cc03cbf4..7c74d1dac8 100644 --- a/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts +++ b/packages/vscode/tests/playwright/utils/vscode-lifecycle.unit.test.ts @@ -3,12 +3,12 @@ import { EventEmitter } from 'node:events' import { access, mkdtemp, mkdir, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' -import { PassThrough } from 'node:stream' -import { afterEach, describe, expect, test } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import { cleanupVSCodeTempDirectories, createVSCodeTempDirectories, formatProcessExit, + getVSCodeTempBase, ProcessDiagnostics, VSCodeTempDirectoryLease, } from './vscode-lifecycle' @@ -20,23 +20,34 @@ afterEach(async () => { }) describe('VS Code Playwright lifecycle', () => { - test('creates collision-free user data and extensions directories per attempt', async () => { + test('creates collision-free short user data and extensions directories', async () => { const parent = await createTemporaryParent() const [first, second] = await Promise.all([ - createVSCodeTempDirectories(2, 1, parent), - createVSCodeTempDirectories(2, 1, parent), + createVSCodeTempDirectories(parent), + createVSCodeTempDirectories(parent), ]) expect(first.root).not.toBe(second.root) - expect(first.userData).toBe(path.join(first.root, 'user-data')) - expect(first.extensions).toBe(path.join(first.root, 'extensions')) + expect(path.basename(first.root)).toMatch(/^pv-.{6}$/) + expect(first.userData).toBe(path.join(first.root, 'u')) + expect(first.extensions).toBe(path.join(first.root, 'e')) await expect(access(first.userData)).resolves.toBeUndefined() await expect(access(first.extensions)).resolves.toBeUndefined() }) + test('keeps the expected macOS VS Code IPC path comfortably below the socket limit', () => { + const simulatedRoot = '/tmp/pv-XXXXXX' + const socketPath = path.posix.join(simulatedRoot, 'u', '1.13-main.sock') + + expect(getVSCodeTempBase('darwin')).toBe('/tmp') + expect(Buffer.byteLength(simulatedRoot)).toBe(14) + expect(Buffer.byteLength(socketPath)).toBe(31) + expect(Buffer.byteLength(socketPath)).toBeLessThan(104) + }) + test('cleans only the isolated attempt root', async () => { const parent = await createTemporaryParent() - const directories = await createVSCodeTempDirectories(0, 0, parent) + const directories = await createVSCodeTempDirectories(parent) const sibling = path.join(parent, 'keep-me') await mkdir(sibling) @@ -46,9 +57,24 @@ describe('VS Code Playwright lifecycle', () => { await expect(access(sibling)).resolves.toBeUndefined() }) + test('configures retries for transient locked-file cleanup', async () => { + const parent = await createTemporaryParent() + const directories = await createVSCodeTempDirectories(parent) + const remove = vi.fn().mockResolvedValue(undefined) + + await cleanupVSCodeTempDirectories(directories, remove) + + expect(remove).toHaveBeenCalledWith(directories.root, { + recursive: true, + force: true, + maxRetries: 3, + retryDelay: 100, + }) + }) + test('registers process-exit cleanup only for the lifetime of an attempt', async () => { const parent = await createTemporaryParent() - const directories = await createVSCodeTempDirectories(0, 0, parent) + const directories = await createVSCodeTempDirectories(parent) const initialListeners = process.listenerCount('exit') const lease = new VSCodeTempDirectoryLease(directories) @@ -59,57 +85,33 @@ describe('VS Code Playwright lifecycle', () => { await expect(access(directories.root)).rejects.toThrow() }) - test('formats exit code, signal, bounded output labels, and redacts secrets', () => { - const byCode = formatProcessExit({ - code: 9, - signal: null, - stdout: 'server ready', - stderr: 'DATABASE_URL=postgres://user:password@example.test/db token=do-not-print', - }) - const bySignal = formatProcessExit({ code: null, signal: 'SIGTERM', stdout: '', stderr: '' }) - - expect(byCode).toContain('exited with code 9') - expect(byCode).toContain('stdout (last 16384 bytes):\nserver ready') - expect(byCode).toContain('postgres://user:[REDACTED]@example.test/db') - expect(byCode).toContain('token=[REDACTED]') - expect(byCode).not.toContain('do-not-print') - expect(bySignal).toBe('VS Code process exited with signal SIGTERM.') + test('formats only structural process exit data', () => { + expect(formatProcessExit({ code: 9, signal: null })).toBe('exitCode=9 signal=none') + expect(formatProcessExit({ code: null, signal: 'SIGTERM' })).toBe('exitCode=unknown signal=SIGTERM') }) - test('bounds child output and removes all diagnostic listeners', () => { - const stdout = new PassThrough() - const stderr = new PassThrough() + test('tracks process exit and removes its listener', () => { const child = Object.assign(new EventEmitter(), { - stdout, - stderr, exitCode: null, signalCode: null, kill: () => true, }) as unknown as ChildProcess const diagnostics = new ProcessDiagnostics(child) - stdout.write('discarded-output'.repeat(2000)) - stdout.write('final-output') child.emit('exit', 7, null) - const formatted = diagnostics.format() - expect(formatted).toContain('exited with code 7') - expect(formatted).toContain('final-output') - expect(Buffer.byteLength(formatted)).toBeLessThan(17 * 1024) - expect(stdout.listenerCount('data')).toBe(1) - expect(stderr.listenerCount('data')).toBe(1) + expect(diagnostics.hasExited()).toBe(true) + expect(diagnostics.format()).toBe('exitCode=7 signal=none') expect(child.listenerCount('exit')).toBe(1) diagnostics.dispose() - expect(stdout.listenerCount('data')).toBe(0) - expect(stderr.listenerCount('data')).toBe(0) expect(child.listenerCount('exit')).toBe(0) }) }) async function createTemporaryParent(): Promise { - const parent = await mkdtemp(path.join(tmpdir(), 'prisma-vscode-lifecycle-test-')) + const parent = await mkdtemp(path.join(tmpdir(), 'pv-test-')) temporaryParents.push(parent) return parent } diff --git a/packages/vscode/tests/playwright/utils/vscode-setup.ts b/packages/vscode/tests/playwright/utils/vscode-setup.ts index ede0bb149f..67ce500ffe 100644 --- a/packages/vscode/tests/playwright/utils/vscode-setup.ts +++ b/packages/vscode/tests/playwright/utils/vscode-setup.ts @@ -1,4 +1,5 @@ import type { ChildProcess } from 'node:child_process' +import { randomUUID } from 'node:crypto' import path from 'node:path' import { downloadAndUnzipVSCode } from '@vscode/test-electron' import { _electron as electron } from '@playwright/test' @@ -6,10 +7,8 @@ import type { ElectronApplication, Page } from '@playwright/test' import { createVSCodeTempDirectories, ProcessDiagnostics, - sanitizeDiagnosticText, terminateChildProcess, VSCodeTempDirectoryLease, - type VSCodeTempDirectories, } from './vscode-lifecycle' export interface VSCodeSetupOptions { @@ -28,13 +27,16 @@ export interface VSCodeTestSession { export async function setupVSCode(options: VSCodeSetupOptions): Promise { const { rootPath, testWorkspace, disableExtensions = true, timeout = 30000, workerIndex, retry } = options - const directories = await createVSCodeTempDirectories(workerIndex, retry) + const directories = await createVSCodeTempDirectories() const directoryLease = new VSCodeTempDirectoryLease(directories) + const attemptId = `w${workerIndex}-r${retry}-${randomUUID()}` let electronApp: ElectronApplication | undefined let diagnostics: ProcessDiagnostics | undefined + let executableName = 'unknown' try { const executablePath = await downloadAndUnzipVSCode() + executableName = path.basename(executablePath) const args = [ '--extensionDevelopmentPath=' + rootPath, ...(disableExtensions ? ['--disable-extensions'] : []), @@ -63,33 +65,40 @@ export async function setupVSCode(options: VSCodeSetupOptions): Promise closeVSCodeSession(electronApp, diagnostics, directoryLease), } - } catch (error) { - throw createLifecycleError('waiting for its first window', error, directories, diagnostics) + } catch { + throw createLifecycleError('first-window', attemptId, executableName, diagnostics) } } catch (error) { - await closeVSCodeSession(electronApp, diagnostics, directoryLease) + const primaryError = + error instanceof VSCodeLifecycleError + ? error + : createLifecycleError('launch', attemptId, executableName, diagnostics) - if (error instanceof VSCodeLifecycleError) { - throw error + try { + await closeVSCodeSession(electronApp, diagnostics, directoryLease) + } catch { + primaryError.noteCleanupFailure() } - throw createLifecycleError('launching', error, directories, diagnostics) + throw primaryError } } -class VSCodeLifecycleError extends Error {} +class VSCodeLifecycleError extends Error { + noteCleanupFailure(): void { + this.message += ' cleanup=failed' + } +} function createLifecycleError( - stage: string, - error: unknown, - directories: VSCodeTempDirectories, + phase: 'launch' | 'first-window', + attemptId: string, + executableName: string, diagnostics?: ProcessDiagnostics, ): VSCodeLifecycleError { - const cause = sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)) - const processContext = diagnostics?.hasExited() ? `\n${diagnostics.format()}` : '' - + const processState = diagnostics?.format() ?? 'exitCode=unknown signal=none' return new VSCodeLifecycleError( - `VS Code failed while ${stage} (isolated attempt ${path.basename(directories.root)}): ${cause}${processContext}`, + `VS Code lifecycle failure: phase=${phase} attempt=${attemptId} executable=${executableName} ${processState}`, ) } From 91fb3dc3b40c04bce4fb4c6d52944b81616363f9 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 08:02:31 +0000 Subject: [PATCH 26/43] test(vscode): verify side-by-side Prisma completions --- packages/vscode/package.json | 1 - packages/vscode/src/__test__/helper.ts | 29 +- .../language-server/completion.test.ts | 2 +- .../__test__/language-server/format.test.ts | 2 +- .../__test__/language-server/hover.test.ts | 2 +- .../language-server/jumpToDefinition.test.ts | 2 +- .../__test__/language-server/linting.test.ts | 2 +- .../language-server/prismaNext.test.ts | 2 +- packages/vscode/src/__test__/runTest.ts | 5 +- .../vscode/src/__test__/workspace.test.ts | 452 ++++-------------- .../bundledClientMiddleware.test.ts | 386 --------------- .../bundledClientStartup.test.ts | 167 ------- .../documentOwnership.test.ts | 334 ------------- .../documentOwnership.ts | 42 -- .../documentRouting.test.ts | 325 ------------- .../prisma-language-server/documentRouting.ts | 33 +- .../plugins/prisma-language-server/index.ts | 16 +- .../languageServerTestState.ts | 34 -- .../localClientMiddleware.test.ts | 184 ------- .../localPrismaNextClientRegistry.test.ts | 369 -------------- .../localPrismaNextClientRegistry.ts | 30 +- packages/vscode/src/util.ts | 2 +- .../integration-workspace.code-workspace | 8 - .../root-a/bundled.prisma | 0 .../integration-workspace/root-a/next.prisma | 1 + .../integration-workspace/root-a/package.json | 5 +- .../root-a/prisma.config.ts | 5 + .../root-a/schema.prisma | 8 - .../root-a/second.prisma | 3 - .../integration-workspace/root-b/package.json | 8 - .../root-b/schema.prisma | 8 - .../root-missing/schema.prisma | 3 - pnpm-lock.yaml | 318 +++++++----- 33 files changed, 301 insertions(+), 2487 deletions(-) delete mode 100644 packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/bundled.prisma create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/next.prisma create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts delete mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma delete mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma delete mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-b/package.json delete mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma delete mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma diff --git a/packages/vscode/package.json b/packages/vscode/package.json index a09d5c11e7..cff162d8e3 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -47,7 +47,6 @@ "watch": "node esbuild.mjs --watch", "test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true", "test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js", - "test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/documentRouting.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts src/plugins/prisma-language-server/localClientMiddleware.test.ts src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts", "test:playwright": "playwright test", "test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright", "vscode:prepublish": "pnpm run build", diff --git a/packages/vscode/src/__test__/helper.ts b/packages/vscode/src/__test__/helper.ts index 629ced7dda..995e351098 100644 --- a/packages/vscode/src/__test__/helper.ts +++ b/packages/vscode/src/__test__/helper.ts @@ -1,9 +1,5 @@ import path from 'path' import vscode from 'vscode' -import { - languageServerTestStateCommand, - type LanguageServerTestState, -} from '../plugins/prisma-language-server/languageServerTestState' // Path from dist-tests/__test__/helper.js to package.json // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires @@ -22,11 +18,7 @@ export async function sleep(ms: number): Promise { * Activates the vscode.prisma extension * @todo check readiness of the server instead of timeout */ -export interface ActivateOptions { - readonly waitForBundledRouting?: boolean -} - -export async function activate(docUri: vscode.Uri, options: ActivateOptions = {}): Promise { +export async function activate(docUri: vscode.Uri): Promise { // The extensionId is `publisher.name` from package.json const ext = vscode.extensions.getExtension(`${packageJson.publisher}.${packageJson.name}`) if (!ext) { @@ -42,28 +34,9 @@ export async function activate(docUri: vscode.Uri, options: ActivateOptions = {} return } - if (options.waitForBundledRouting) { - await waitForBundledRouting(doc) - } await sleep(2500) // Wait for server activation } -async function waitForBundledRouting(document: vscode.TextDocument): Promise { - const documentUri = document.uri.toString() - const deadline = Date.now() + 10_000 - - while (Date.now() < deadline) { - const state = await vscode.commands.executeCommand(languageServerTestStateCommand) - const latestOpen = [...state.routingEvents] - .reverse() - .find((event) => event.type === 'opened' && event.documentUri === documentUri) - if (latestOpen?.owner.kind === 'bundled') return - await sleep(100) - } - - throw new Error(`Timed out waiting for bundled language-server routing for ${documentUri}`) -} - export function toRange(sLine: number, sChar: number, eLine: number, eChar: number): vscode.Range { const start = new vscode.Position(sLine, sChar) const end = new vscode.Position(eLine, eChar) diff --git a/packages/vscode/src/__test__/language-server/completion.test.ts b/packages/vscode/src/__test__/language-server/completion.test.ts index 83e95a6c3d..043574b1ae 100644 --- a/packages/vscode/src/__test__/language-server/completion.test.ts +++ b/packages/vscode/src/__test__/language-server/completion.test.ts @@ -12,7 +12,7 @@ async function testCompletion( triggerCharacter?: string, ): Promise { if (!isActivated) { - await activate(docUri, { waitForBundledRouting: true }) + await activate(docUri) } const actualCompletions: vscode.CompletionList = await vscode.commands.executeCommand( diff --git a/packages/vscode/src/__test__/language-server/format.test.ts b/packages/vscode/src/__test__/language-server/format.test.ts index 8aebdfcbbf..5d337f8bd2 100644 --- a/packages/vscode/src/__test__/language-server/format.test.ts +++ b/packages/vscode/src/__test__/language-server/format.test.ts @@ -4,7 +4,7 @@ import { getDocUri, activate } from '../helper' import fs from 'fs' async function testAutoFormat(docUri: vscode.Uri, expectedFormatted: string): Promise { - await activate(docUri, { waitForBundledRouting: true }) + await activate(docUri) const actualFormatted = (await vscode.commands.executeCommand('vscode.executeFormatDocumentProvider', docUri, { insertSpaces: true, diff --git a/packages/vscode/src/__test__/language-server/hover.test.ts b/packages/vscode/src/__test__/language-server/hover.test.ts index e2bdb7f49d..911cde2632 100644 --- a/packages/vscode/src/__test__/language-server/hover.test.ts +++ b/packages/vscode/src/__test__/language-server/hover.test.ts @@ -17,7 +17,7 @@ suite('Should show /// documentation comments for', () => { const expectedHover = `\`\`\`prisma\nmodel Post {\n\t...\n\tauthor User? @relation(name: "PostToUser", fields: [authorId], references: [id])\n}\n\`\`\`\n___\none-to-many\n___\nPost including an author and content.` test('model', async () => { - await activate(docUri, { waitForBundledRouting: true }) + await activate(docUri) await testHover(docUri, new vscode.Position(22, 10), expectedHover) }) }) diff --git a/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts b/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts index 1cb550e097..8c1264e441 100644 --- a/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts +++ b/packages/vscode/src/__test__/language-server/jumpToDefinition.test.ts @@ -21,7 +21,7 @@ suite('Jump-to-definition', () => { const fixturePathSqlite = getDocUri('jump-to-definition/schema.prisma') test('SQLite: from attribute to model', async function () { - await activate(fixturePathSqlite, { waitForBundledRouting: true }) + await activate(fixturePathSqlite) await testJumpToDefinition( fixturePathSqlite, diff --git a/packages/vscode/src/__test__/language-server/linting.test.ts b/packages/vscode/src/__test__/language-server/linting.test.ts index b1bd8974fd..b8f182ca4b 100644 --- a/packages/vscode/src/__test__/language-server/linting.test.ts +++ b/packages/vscode/src/__test__/language-server/linting.test.ts @@ -3,7 +3,7 @@ import * as assert from 'assert' import { getDocUri, activate, toRange } from '../helper' async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]): Promise { - await activate(docUri, { waitForBundledRouting: true }) + await activate(docUri) const actualDiagnostics = vscode.languages.getDiagnostics(docUri) diff --git a/packages/vscode/src/__test__/language-server/prismaNext.test.ts b/packages/vscode/src/__test__/language-server/prismaNext.test.ts index dcbcfb74cd..8277903dac 100644 --- a/packages/vscode/src/__test__/language-server/prismaNext.test.ts +++ b/packages/vscode/src/__test__/language-server/prismaNext.test.ts @@ -49,7 +49,7 @@ suite('Prisma-next directive', () => { test('Sibling file without directive still gets diagnostics', async () => { const docUri = getDocUri('linting/missingArgument.prisma') - await activate(docUri, { waitForBundledRouting: true }) + await activate(docUri) const diagnostics = await waitForDiagnostics(docUri, (d) => d.length > 0) assert.ok(diagnostics.length > 0, 'expected diagnostics on regular file with errors') }) diff --git a/packages/vscode/src/__test__/runTest.ts b/packages/vscode/src/__test__/runTest.ts index bfc22401b1..578196c5a3 100644 --- a/packages/vscode/src/__test__/runTest.ts +++ b/packages/vscode/src/__test__/runTest.ts @@ -22,10 +22,7 @@ function test(version?: string, testPattern?: string) { version, // optional, default = latest extensionDevelopmentPath, extensionTestsPath, - extensionTestsEnv: { - PRISMA_VSCODE_TEST: '1', - ...(testPattern ? { VSCODE_TEST_PATTERN: testPattern } : {}), - }, + extensionTestsEnv: testPattern ? { VSCODE_TEST_PATTERN: testPattern } : undefined, launchArgs: [ workspacePath, // This disables all extensions except the one being testing diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index 634492deea..5ff600769f 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -1,394 +1,102 @@ import assert from 'node:assert' -import { stat } from 'node:fs/promises' import vscode from 'vscode' -import type { DocumentOwner } from '../plugins/prisma-language-server/documentOwnership' -import type { DocumentRoutingEvent } from '../plugins/prisma-language-server/documentRouting' -import { - languageServerTestStateCommand, - type LanguageServerTestState, -} from '../plugins/prisma-language-server/languageServerTestState' -import { getPrismaCliEntrypoint, getWorkspaceDocUri, getWorkspaceFolder, sleep } from './helper' -const stateTimeoutMs = 30_000 -const diagnosticTimeoutMs = 20_000 - -suite('Multi-root integration workspace', () => { - test('resolves documents and real Prisma CLI entrypoints per workspace root', async () => { - const rootA = getWorkspaceFolder('integration-root-a') - const rootB = getWorkspaceFolder('integration-root-b') - const missingRoot = getWorkspaceFolder('integration-root-missing') - - const documentAUri = getWorkspaceDocUri(rootA, 'schema.prisma') - const documentBUri = getWorkspaceDocUri(rootB, 'schema.prisma') - const missingDocumentUri = getWorkspaceDocUri(missingRoot, 'schema.prisma') - - assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentAUri), rootA) - assert.strictEqual(vscode.workspace.getWorkspaceFolder(documentBUri), rootB) - assert.strictEqual(vscode.workspace.getWorkspaceFolder(missingDocumentUri), missingRoot) - assert.notStrictEqual(rootA.uri.toString(), rootB.uri.toString()) - - for (const workspaceFolder of [rootA, rootB]) { - const entrypoint = getPrismaCliEntrypoint(workspaceFolder) - assert.strictEqual( - (await stat(entrypoint.fsPath)).isFile(), - true, - `Missing Prisma CLI entrypoint: ${entrypoint.fsPath}`, - ) - } - - await assert.rejects(stat(getPrismaCliEntrypoint(missingRoot).fsPath), { code: 'ENOENT' }) - }) - - test('routes unsaved documents exclusively across real root-local clients', async () => { - const rootA = getWorkspaceFolder('integration-root-a') - const rootB = getWorkspaceFolder('integration-root-b') - const missingRoot = getWorkspaceFolder('integration-root-missing') - const fixtures = await snapshotFixtures([ - getWorkspaceDocUri(rootA, 'schema.prisma'), - getWorkspaceDocUri(rootA, 'second.prisma'), - getWorkspaceDocUri(rootB, 'schema.prisma'), - getWorkspaceDocUri(missingRoot, 'schema.prisma'), - ]) - - try { - const extension = vscode.extensions.getExtension('Prisma.prisma') - assert.ok(extension) - await extension.activate() - const activationState = await getTestState() - assert.deepStrictEqual(activationState.localClients.startedWorkspaceFolderUris, []) - assert.deepStrictEqual(activationState.localClients.startCountsByWorkspaceFolderUri, {}) - - const documentA = await vscode.workspace.openTextDocument(fixtures[0].uri) - const secondDocumentA = await vscode.workspace.openTextDocument(fixtures[1].uri) - const documentB = await vscode.workspace.openTextDocument(fixtures[2].uri) - const missingDocument = await vscode.workspace.openTextDocument(fixtures[3].uri) - - const initialState = await waitForState( - (state) => - state.localClients.startedWorkspaceFolderUris.length === 0 && - [documentA, secondDocumentA, documentB, missingDocument].every((document) => - activeOwnerKeys(state, document.uri).has('bundled'), - ), - 'unmarked documents to be owned by the bundled client without starting local clients', - ) - assert.strictEqual(initialState.workspaceTrusted, true) - assert.deepStrictEqual(initialState.localClients.startCountsByWorkspaceFolderUri, {}) - assertExclusiveOwners(initialState) - - const invalidSchema = withDocumentEol(documentA, 'model Broken {\n id Missing\n}\n') - await replaceDocument(documentA, invalidSchema) - await waitForDiagnostics( - documentA.uri, - (diagnostics) => diagnostics.length > 0, - 'bundled diagnostics before adding the directive', - ) - - const markedInvalidSchema = withDocumentEol(documentA, `// use prisma-next\n${invalidSchema}`) - const addDirectiveEventIndex = initialState.routingEvents.length - await replaceDocument(documentA, markedInvalidSchema) - const localAState = await waitForState( - (state) => - state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && - lastOpenedAfter(state, documentA.uri, addDirectiveEventIndex)?.owner.kind === 'local', - 'root A local client and marked document synchronization', - ) - assert.strictEqual( - localAState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], - 1, - 'expected the real root A client to complete its initialization handshake', - ) - const localAOpen = lastOpenedAfter(localAState, documentA.uri, addDirectiveEventIndex) - assert.ok(localAOpen) - assert.strictEqual(localAOpen.documentText, markedInvalidSchema) - assert.strictEqual(localAOpen.documentVersion, documentA.version) - assert.deepStrictEqual( - activeOwnerKeys(localAState, documentA.uri), - new Set([localOwnerKey(rootA.uri.toString())]), - ) - assert.strictEqual( - hasDiagnosticClearAfter(localAState, documentA.uri, 'bundled', addDirectiveEventIndex), - true, - 'expected bundled diagnostics to clear before local ownership', - ) - assertExclusiveOwners(localAState) - - const markedSecondSchema = withDocumentEol( - secondDocumentA, - '// use prisma-next\nmodel RootASecondRecord {\n id Int @id\n}\n', - ) - const secondAEventIndex = localAState.routingEvents.length - await replaceDocument(secondDocumentA, markedSecondSchema) - const reusedAState = await waitForState( - (state) => lastOpenedAfter(state, secondDocumentA.uri, secondAEventIndex)?.owner.kind === 'local', - 'second root A document to synchronize locally', - ) - assert.strictEqual(reusedAState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) - assert.deepStrictEqual( - activeOwnerKeys(reusedAState, secondDocumentA.uri), - new Set([localOwnerKey(rootA.uri.toString())]), - ) - assertExclusiveOwners(reusedAState) - - const markedRootBSchema = withDocumentEol(documentB, '// use prisma-next\nmodel RootBRecord {\n id Int @id\n}\n') - const rootBEventIndex = reusedAState.routingEvents.length - await replaceDocument(documentB, markedRootBSchema) - const independentRootsState = await waitForState( - (state) => - state.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()] === 1 && - state.localClients.startCountsByWorkspaceFolderUri[rootB.uri.toString()] === 1 && - lastOpenedAfter(state, documentB.uri, rootBEventIndex)?.owner.kind === 'local', - 'independent root B local client', - ) - assert.deepStrictEqual(independentRootsState.localClients.startedWorkspaceFolderUris, [ - rootA.uri.toString(), - rootB.uri.toString(), - ]) - assert.deepStrictEqual( - activeOwnerKeys(independentRootsState, documentB.uri), - new Set([localOwnerKey(rootB.uri.toString())]), - ) - assertExclusiveOwners(independentRootsState) - - const markedMissingSchema = withDocumentEol( - missingDocument, - '// use prisma-next\nmodel MissingCliRecord {\n id Int @id\n}\n', - ) - const missingEventIndex = independentRootsState.routingEvents.length - const missingOwnershipEventIndex = independentRootsState.ownershipEvents.length - await replaceDocument(missingDocument, markedMissingSchema) - const missingState = await waitForState( - (state) => - hasLocalOwnershipAfter(state, missingDocument.uri, missingRoot.uri.toString(), missingOwnershipEventIndex) && - hasDiagnosticClearAfter(state, missingDocument.uri, 'bundled', missingEventIndex), - 'missing-entrypoint routing to settle without fallback', - ) - assert.deepStrictEqual(missingState.localClients.startedWorkspaceFolderUris, [ - rootA.uri.toString(), - rootB.uri.toString(), - ]) - assert.strictEqual( - missingState.localClients.startCountsByWorkspaceFolderUri[missingRoot.uri.toString()], - undefined, - ) - assert.deepStrictEqual(activeOwnerKeys(missingState, missingDocument.uri), new Set()) - assert.strictEqual(lastOpenedAfter(missingState, missingDocument.uri, missingEventIndex), undefined) - assertExclusiveOwners(missingState) - - const restoredBundledSchema = withDocumentEol(documentA, 'model RootARestored {\n id Int @id\n}\n') - const removeDirectiveEventIndex = missingState.routingEvents.length - await replaceDocument(documentA, restoredBundledSchema) - const restoredState = await waitForState( - (state) => lastOpenedAfter(state, documentA.uri, removeDirectiveEventIndex)?.owner.kind === 'bundled', - 'root A document to return to bundled ownership', - ) - const bundledOpen = lastOpenedAfter(restoredState, documentA.uri, removeDirectiveEventIndex) - assert.ok(bundledOpen) - assert.strictEqual(bundledOpen.documentText, restoredBundledSchema) - assert.strictEqual(bundledOpen.documentVersion, documentA.version) - assert.strictEqual( - hasDiagnosticClearAfter(restoredState, documentA.uri, 'local', removeDirectiveEventIndex), - true, - 'expected local diagnostics to clear before bundled ownership', - ) - assert.deepStrictEqual(activeOwnerKeys(restoredState, documentA.uri), new Set(['bundled'])) - assert.strictEqual(restoredState.localClients.startCountsByWorkspaceFolderUri[rootA.uri.toString()], 1) - assertExclusiveOwners(restoredState) - await waitForDiagnostics( - documentA.uri, - (diagnostics) => diagnostics.length === 0, - 'bundled diagnostics to clear after restoring valid text', - ) - } finally { - await restoreFixtures(fixtures) - } +const completionTimeoutMs = 30_000 +const completionPollIntervalMs = 100 + +suite('Prisma language server routing', () => { + test('provides bundled Prisma 7 and workspace-local Prisma 8 completions side by side', async () => { + const workspaceFolders = vscode.workspace.workspaceFolders + assert.ok(workspaceFolders) + assert.strictEqual(workspaceFolders.length, 1) + const root = workspaceFolders[0] + + const bundledUri = vscode.Uri.joinPath(root.uri, 'bundled.prisma') + const nextUri = vscode.Uri.joinPath(root.uri, 'next.prisma') + const bundledDocument = await vscode.workspace.openTextDocument(bundledUri) + await vscode.window.showTextDocument(bundledDocument, { viewColumn: vscode.ViewColumn.One }) + const nextDocument = await vscode.workspace.openTextDocument(nextUri) + await vscode.window.showTextDocument(nextDocument, { viewColumn: vscode.ViewColumn.Two }) + + const extension = vscode.extensions.getExtension('Prisma.prisma') + assert.ok(extension) + await extension.activate() + + const bundledCompletions = await waitForCompletions( + bundledUri, + new vscode.Position(0, 0), + (completions) => + ['datasource', 'generator', 'model'].every((label) => hasLabel(completions, label)) && + findCompletion(completions, 'datasource')?.kind === vscode.CompletionItemKind.Class, + 'bundled Prisma 7 declaration completions', + ) + const bundledDatasource = findCompletion(bundledCompletions, 'datasource') + assert.ok(bundledDatasource) + assert.strictEqual(bundledDatasource.kind, vscode.CompletionItemKind.Class) + assert.ok(hasLabel(bundledCompletions, 'generator')) + assert.ok(hasLabel(bundledCompletions, 'model')) + assert.ok(!hasLabel(bundledCompletions, 'namespace')) + + const nextCompletions = await waitForCompletions( + nextUri, + new vscode.Position(1, 0), + (completions) => { + const namespace = findCompletion(completions, 'namespace') + return namespace?.kind === vscode.CompletionItemKind.Keyword && namespace.detail === 'PSL declaration keyword' + }, + 'workspace-local Prisma 8 declaration completions', + ) + const namespace = findCompletion(nextCompletions, 'namespace') + assert.ok(namespace) + assert.strictEqual(namespace.kind, vscode.CompletionItemKind.Keyword) + assert.strictEqual(namespace.detail, 'PSL declaration keyword') + assert.ok(!hasLabel(nextCompletions, 'datasource')) }) }) -async function getTestState(): Promise { - const state = await vscode.commands.executeCommand(languageServerTestStateCommand) - assert.ok(state, 'language-server test state command was not registered') - return state -} - -async function waitForState( - predicate: (state: LanguageServerTestState) => boolean, - description: string, -): Promise { - const deadline = Date.now() + stateTimeoutMs - let state = await getTestState() - while (!predicate(state) && Date.now() < deadline) { - await sleep(100) - state = await getTestState() - } - assert.ok(predicate(state), `Timed out waiting for ${description}`) - return state -} - -async function waitForDiagnostics( +async function waitForCompletions( uri: vscode.Uri, - predicate: (diagnostics: readonly vscode.Diagnostic[]) => boolean, + position: vscode.Position, + predicate: (completions: vscode.CompletionList) => boolean, description: string, -): Promise { - const deadline = Date.now() + diagnosticTimeoutMs - let diagnostics = vscode.languages.getDiagnostics(uri) - while (!predicate(diagnostics) && Date.now() < deadline) { - await sleep(100) - diagnostics = vscode.languages.getDiagnostics(uri) - } - assert.ok(predicate(diagnostics), `Timed out waiting for ${description} for ${uri.toString()}`) - return diagnostics -} +): Promise { + const deadline = Date.now() + completionTimeoutMs + let lastCompletions: vscode.CompletionList | undefined + let lastError: unknown -async function replaceDocument(document: vscode.TextDocument, text: string): Promise { - await replaceDocumentText(document, text) - assert.strictEqual(document.isDirty, true) -} - -function withDocumentEol(document: vscode.TextDocument, text: string): string { - const eol = document.eol === vscode.EndOfLine.CRLF ? '\r\n' : '\n' - return text.split('\r\n').join('\n').split('\n').join(eol) -} - -interface FixtureSnapshot { - readonly uri: vscode.Uri - readonly bytes: Uint8Array - readonly text: string -} - -async function snapshotFixtures(uris: readonly vscode.Uri[]): Promise { - return Promise.all( - uris.map(async (uri) => { - const bytes = await vscode.workspace.fs.readFile(uri) - return { uri, bytes, text: Buffer.from(bytes).toString('utf8') } - }), - ) -} - -async function restoreFixtures(fixtures: readonly FixtureSnapshot[]): Promise { - for (const fixture of fixtures) { - const document = findOpenDocument(fixture.uri) - if (document) { - if (document.getText() !== fixture.text) { - await replaceDocumentText(document, fixture.text) - } - if (document.isDirty && !(await document.save())) { - await vscode.workspace.fs.writeFile(fixture.uri, fixture.bytes) - } - } else { - await vscode.workspace.fs.writeFile(fixture.uri, fixture.bytes) - } - } - - for (const fixture of fixtures) { - const document = findOpenDocument(fixture.uri) - if (document) { - assert.strictEqual(document.getText(), fixture.text, `Open fixture was not restored: ${fixture.uri.toString()}`) - assert.strictEqual(document.isDirty, false, `Restored fixture remains dirty: ${fixture.uri.toString()}`) - } - assert.deepStrictEqual( - await vscode.workspace.fs.readFile(fixture.uri), - fixture.bytes, - `On-disk fixture was not restored: ${fixture.uri.toString()}`, - ) - } -} - -async function replaceDocumentText(document: vscode.TextDocument, text: string): Promise { - const edit = new vscode.WorkspaceEdit() - edit.replace( - document.uri, - new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length)), - text, - ) - assert.strictEqual(await vscode.workspace.applyEdit(edit), true) - assert.strictEqual(document.getText(), text) -} - -function findOpenDocument(uri: vscode.Uri): vscode.TextDocument | undefined { - return vscode.workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()) -} - -function activeOwnerKeys(state: LanguageServerTestState, uri: vscode.Uri): Set { - const active = new Set() - for (const event of state.routingEvents) { - if (event.documentUri !== uri.toString()) continue - const key = ownerKey(event.owner) - if (event.type === 'opened') { - active.add(key) - } else if (event.type === 'closed') { - active.delete(key) - } - } - return active -} - -function assertExclusiveOwners(state: LanguageServerTestState): void { - const activeByDocument = new Map>() - for (const event of state.routingEvents) { - const active = activeByDocument.get(event.documentUri) ?? new Set() - activeByDocument.set(event.documentUri, active) - const key = ownerKey(event.owner) - if (event.type === 'opened') { - active.add(key) - assert.ok( - active.size <= 1, - `Document ${event.documentUri} was observed on multiple owners: ${[...active].join(', ')}`, + while (Date.now() < deadline) { + try { + lastCompletions = await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uri, + position, ) - } else if (event.type === 'closed') { - active.delete(key) + if (lastCompletions && predicate(lastCompletions)) { + return lastCompletions + } + } catch (error) { + lastError = error } + await sleep(completionPollIntervalMs) } -} -function lastOpenedAfter( - state: LanguageServerTestState, - uri: vscode.Uri, - eventIndex: number, -): Extract | undefined { - return state.routingEvents - .slice(eventIndex) - .filter( - (event): event is Extract => - event.type === 'opened' && event.documentUri === uri.toString(), - ) - .at(-1) + const labels = lastCompletions?.items.map(completionLabel).join(', ') ?? '' + const error = lastError instanceof Error ? ` Last error: ${lastError.message}` : '' + throw new Error(`Timed out waiting for ${description}. Last completion labels: ${labels}.${error}`) } -function hasDiagnosticClearAfter( - state: LanguageServerTestState, - uri: vscode.Uri, - owner: DocumentOwner['kind'], - eventIndex: number, -): boolean { - return state.routingEvents - .slice(eventIndex) - .some( - (event) => - event.type === 'diagnosticsCleared' && event.documentUri === uri.toString() && event.owner.kind === owner, - ) +function findCompletion(completions: vscode.CompletionList, label: string): vscode.CompletionItem | undefined { + return completions.items.find((item) => completionLabel(item) === label) } -function hasLocalOwnershipAfter( - state: LanguageServerTestState, - uri: vscode.Uri, - workspaceFolderUri: string, - eventIndex: number, -): boolean { - return state.ownershipEvents - .slice(eventIndex) - .some( - (event) => - event.type === 'ownerChanged' && - event.documentUri === uri.toString() && - event.owner.kind === 'local' && - event.owner.workspaceFolderUri === workspaceFolderUri, - ) +function hasLabel(completions: vscode.CompletionList, label: string): boolean { + return findCompletion(completions, label) !== undefined } -function ownerKey(owner: DocumentOwner): string { - return owner.kind === 'local' ? localOwnerKey(owner.workspaceFolderUri) : owner.kind +function completionLabel(item: vscode.CompletionItem): string { + return typeof item.label === 'string' ? item.label : item.label.label } -function localOwnerKey(workspaceFolderUri: string): string { - return `local:${workspaceFolderUri}` +async function sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts deleted file mode 100644 index f9521cf230..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import type { - CancellationToken, - CodeAction, - CodeActionContext, - CompletionContext, - CompletionItem, - Diagnostic, - FormattingOptions, - Position, - Range, - TextDocument, - TextDocumentChangeEvent, - Uri, - WorkspaceFolder, -} from 'vscode' -import type { LanguageClient } from 'vscode-languageclient/node' -import { createBundledClientMiddleware, type BundledClientMiddleware } from './bundledClientMiddleware' -import { DocumentOwnershipCoordinator } from './documentOwnership' - -const position = {} as Position -const range = {} as Range -const token = {} as CancellationToken -const completionContext = {} as CompletionContext -const formattingOptions = {} as FormattingOptions -const codeActionContext = { diagnostics: [] } as unknown as CodeActionContext -const root = workspaceFolder('file:///workspace') - -function uri(value: string): Uri { - return { - scheme: value.slice(0, value.indexOf(':')), - toString: () => value, - } as Uri -} - -function workspaceFolder(value: string): WorkspaceFolder { - return { uri: uri(value) } as WorkspaceFolder -} - -function document(value: string, text: string): TextDocument & { setText(nextText: string): void } { - let currentText = text - return { - uri: uri(value), - languageId: 'prisma', - getText: () => currentText, - setText: (nextText: string) => { - currentText = nextText - }, - } as TextDocument & { setText(nextText: string): void } -} - -function createSubject(options: { pinned?: boolean } = {}): { - middleware: BundledClientMiddleware - ownership: DocumentOwnershipCoordinator - client: LanguageClient - documents: Map - diagnosticMessages: string[] - isSnippetEdit: ReturnType - sendRequest: ReturnType - sendNotification: ReturnType - deleteDiagnostics: ReturnType -} { - const documents = new Map() - const diagnosticMessages: string[] = [] - const isSnippetEdit = vi.fn().mockReturnValue(true) - const sendRequest = vi.fn() - const sendNotification = vi.fn().mockResolvedValue(undefined) - const deleteDiagnostics = vi.fn() - const client = { - code2ProtocolConverter: { - asTextDocumentIdentifier: (textDocument: TextDocument) => ({ uri: textDocument.uri.toString() }), - asOpenTextDocumentParams: (textDocument: TextDocument) => ({ - textDocument: { - uri: textDocument.uri.toString(), - languageId: textDocument.languageId, - version: 1, - text: textDocument.getText(), - }, - }), - asCloseTextDocumentParams: (textDocument: TextDocument) => ({ - textDocument: { uri: textDocument.uri.toString() }, - }), - asRange: () => ({ start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }), - asCodeActionContext: () => ({ diagnostics: [] }), - }, - protocol2CodeConverter: { - asCodeAction: (action: { title: string }) => ({ title: action.title, edit: { changes: {} } }), - asCommand: (command: { title: string; command: string }) => command, - }, - diagnostics: { delete: deleteDiagnostics }, - sendNotification, - sendRequest, - } as unknown as LanguageClient - const ownership = new DocumentOwnershipCoordinator({ - workspace: { - isTrusted: true, - getWorkspaceFolder: (documentUri) => (documentUri.toString().startsWith(root.uri.toString()) ? root : undefined), - }, - policy: { isPinnedToPrisma6: () => options.pinned ?? false }, - }) - - return { - ownership, - middleware: createBundledClientMiddleware({ - ownership, - getClient: () => client, - getDocument: (documentUri) => documents.get(documentUri.toString()), - handleDiagnosticMessage: (message) => diagnosticMessages.push(message), - isSnippetEdit, - }), - client, - documents, - diagnosticMessages, - isSnippetEdit, - sendRequest, - sendNotification, - deleteDiagnostics, - } -} - -describe('bundled client ownership middleware', () => { - test('balances bundled lifecycle notifications without duplicate opens or closes', async () => { - const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() - const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') - await ownership.synchronize(schema) - const didOpen = vi.fn() - const didChange = vi.fn() - const didClose = vi.fn() - const change = { document: schema } as unknown as TextDocumentChangeEvent - - middleware.didOpen?.(schema, didOpen) - middleware.didOpen?.(schema, didOpen) - middleware.didChange?.(change, didChange) - middleware.didClose?.(schema, didClose) - middleware.didClose?.(schema, didClose) - middleware.didOpen?.(schema, didOpen) - middleware.didClose?.(schema, didClose) - - expect(didOpen).toHaveBeenCalledTimes(2) - expect(didOpen).toHaveBeenCalledWith(schema) - expect(didChange).toHaveBeenCalledOnce() - expect(didChange).toHaveBeenCalledWith(change) - expect(didClose).toHaveBeenCalledTimes(2) - expect(didClose).toHaveBeenCalledWith(schema) - expect(sendNotification).not.toHaveBeenCalled() - expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) - }) - - test('resynchronizes an open document exactly once after the bundled client restarts', async () => { - const { middleware, ownership, client, sendNotification } = createSubject() - const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') - await ownership.synchronize(schema) - const oldDidOpen = vi.fn() - const oldCompletion = { label: 'id' } as CompletionItem - - middleware.didOpen?.(schema, oldDidOpen) - await middleware.provideCompletionItem?.(schema, position, completionContext, token, () => [oldCompletion]) - schema.setText('model User {\n id Int @id\n name String\n}') - middleware.resetClientState() - - expect(middleware.resolveCompletionItem?.(oldCompletion, token, vi.fn())).toBeUndefined() - - const replacementOpenParams: unknown[] = [] - const replacementDidOpen = vi.fn((textDocument: TextDocument) => { - replacementOpenParams.push(client.code2ProtocolConverter.asOpenTextDocumentParams(textDocument)) - }) - const replacementDidChange = vi.fn() - const replacementDidClose = vi.fn() - const change = { document: schema } as unknown as TextDocumentChangeEvent - - middleware.didOpen?.(schema, replacementDidOpen) - middleware.didOpen?.(schema, replacementDidOpen) - middleware.didChange?.(change, replacementDidChange) - middleware.didClose?.(schema, replacementDidClose) - middleware.didClose?.(schema, replacementDidClose) - - expect(oldDidOpen).toHaveBeenCalledOnce() - expect(replacementDidOpen).toHaveBeenCalledOnce() - expect(replacementOpenParams).toEqual([ - { - textDocument: { - uri: schema.uri.toString(), - languageId: 'prisma', - version: 1, - text: schema.getText(), - }, - }, - ]) - expect(replacementDidChange).toHaveBeenCalledOnce() - expect(replacementDidChange).toHaveBeenCalledWith(change) - expect(replacementDidClose).toHaveBeenCalledOnce() - expect(replacementDidClose).toHaveBeenCalledWith(schema) - expect(sendNotification).not.toHaveBeenCalled() - }) - - test('suppresses a transition change without performing pre-commit lifecycle effects', async () => { - const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() - const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') - await ownership.synchronize(schema) - const didOpen = vi.fn() - const didChange = vi.fn() - - middleware.didOpen?.(schema, didOpen) - schema.setText('// use prisma-next\nmodel User { id Int @id }') - const markedChange = { document: schema } as unknown as TextDocumentChangeEvent - middleware.didChange?.(markedChange, didChange) - middleware.didChange?.(markedChange, didChange) - - expect(didChange).not.toHaveBeenCalled() - expect(sendNotification).not.toHaveBeenCalled() - expect(deleteDiagnostics).not.toHaveBeenCalled() - }) - - test('opens a coordinator-reacquired document with complete current text exactly once', async () => { - const { middleware, ownership, sendNotification } = createSubject() - const schema = document('file:///workspace/schema.prisma', '// use prisma-next') - const didChange = vi.fn() - - schema.setText('model User {\n id Int @id\n name String\n}') - await ownership.synchronize(schema) - middleware.openDocument(schema) - middleware.openDocument(schema) - const unmarkedChange = { document: schema } as unknown as TextDocumentChangeEvent - - expect(didChange).not.toHaveBeenCalled() - expect(sendNotification).toHaveBeenCalledOnce() - expect(sendNotification).toHaveBeenCalledWith('textDocument/didOpen', { - textDocument: { - uri: schema.uri.toString(), - languageId: 'prisma', - version: 1, - text: schema.getText(), - }, - }) - - middleware.didChange?.(unmarkedChange, didChange) - expect(didChange).toHaveBeenCalledOnce() - expect(sendNotification).toHaveBeenCalledOnce() - }) - - test('forwards a real close for every URI still tracked by the bundled server', async () => { - const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() - const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') - await ownership.synchronize(schema) - const didOpen = vi.fn() - const didChange = vi.fn() - const didClose = vi.fn() - - middleware.didOpen?.(schema, didOpen) - schema.setText('// use prisma-next') - middleware.didClose?.(schema, didClose) - - expect(didClose).toHaveBeenCalledOnce() - expect(didClose).toHaveBeenCalledWith(schema) - expect(sendNotification).not.toHaveBeenCalled() - expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) - - const reopened = document('file:///workspace/reopened.prisma', 'model User { id Int @id }') - await ownership.synchronize(reopened) - middleware.didOpen?.(reopened, didOpen) - reopened.setText('// use prisma-next') - middleware.didChange?.({ document: reopened } as unknown as TextDocumentChangeEvent, didChange) - middleware.didClose?.(reopened, didClose) - - expect(sendNotification).not.toHaveBeenCalled() - expect(didClose).toHaveBeenCalledTimes(2) - expect(didClose).toHaveBeenLastCalledWith(reopened) - }) - - test('gates every advertised document request while preserving unmarked forwarding', async () => { - const { middleware, ownership } = createSubject() - const marked = document('file:///workspace/marked.prisma', '// use prisma-next') - const unmarked = document('file:///workspace/unmarked.prisma', 'model User { id Int @id }') - await ownership.synchronize(unmarked) - - const completionItem = { label: 'id' } as CompletionItem - const completionNext = vi.fn().mockReturnValue([completionItem]) - expect( - middleware.provideCompletionItem?.(marked, position, completionContext, token, completionNext), - ).toBeUndefined() - await expect( - middleware.provideCompletionItem?.(unmarked, position, completionContext, token, completionNext), - ).resolves.toEqual([{ label: 'id' }]) - - const resolveNext = vi.fn().mockReturnValue(completionItem) - expect(middleware.resolveCompletionItem?.(completionItem, token, resolveNext)).toBe(completionItem) - unmarked.setText('// use prisma-next') - expect(middleware.resolveCompletionItem?.(completionItem, token, resolveNext)).toBeUndefined() - - const markedNext = vi.fn() - expect(middleware.provideHover?.(marked, position, token, markedNext)).toBeUndefined() - expect(middleware.provideDefinition?.(marked, position, token, markedNext)).toBeUndefined() - expect( - middleware.provideReferences?.(marked, position, { includeDeclaration: true }, token, markedNext), - ).toBeUndefined() - expect(middleware.provideDocumentSymbols?.(marked, token, markedNext)).toBeUndefined() - expect(middleware.provideDocumentFormattingEdits?.(marked, formattingOptions, token, markedNext)).toBeUndefined() - expect(middleware.provideRenameEdits?.(marked, position, 'Renamed', token, markedNext)).toBeUndefined() - expect(markedNext).not.toHaveBeenCalled() - - const forwarded = Symbol('forwarded') - const unmarkedNext = vi.fn().mockReturnValue(forwarded) - unmarked.setText('model User { id Int @id }') - expect(middleware.provideHover?.(unmarked, position, token, unmarkedNext)).toBe(forwarded) - expect(middleware.provideDefinition?.(unmarked, position, token, unmarkedNext)).toBe(forwarded) - expect(middleware.provideReferences?.(unmarked, position, { includeDeclaration: true }, token, unmarkedNext)).toBe( - forwarded, - ) - expect(middleware.provideDocumentSymbols?.(unmarked, token, unmarkedNext)).toBe(forwarded) - expect(middleware.provideDocumentFormattingEdits?.(unmarked, formattingOptions, token, unmarkedNext)).toBe( - forwarded, - ) - expect(middleware.provideRenameEdits?.(unmarked, position, 'Renamed', token, unmarkedNext)).toBe(forwarded) - }) - - test('clears diagnostics when a document loses bundled ownership', async () => { - const { middleware, ownership, documents, diagnosticMessages } = createSubject() - const schema = document('file:///workspace/schema.prisma', 'model User { id Int @id }') - await ownership.synchronize(schema) - documents.set(schema.uri.toString(), schema) - const diagnostics = [{ message: 'bundled diagnostic' }] as Diagnostic[] - const next = vi.fn() - - middleware.handleDiagnostics?.(schema.uri, diagnostics, next) - schema.setText('// use prisma-next') - middleware.handleDiagnostics?.(schema.uri, diagnostics, next) - schema.setText('model User { id Int @id }') - documents.delete(schema.uri.toString()) - middleware.handleDiagnostics?.(schema.uri, diagnostics, next) - - expect(next.mock.calls).toEqual([ - [schema.uri, diagnostics], - [schema.uri, []], - [schema.uri, []], - ]) - expect(diagnosticMessages).toEqual(['bundled diagnostic']) - }) - - test('keeps code-action conversion ownership-gated', async () => { - const { middleware, ownership, sendRequest, isSnippetEdit } = createSubject() - sendRequest.mockResolvedValue([ - { - title: 'Insert block', - kind: 'quickfix', - edit: { changes: { 'file:///workspace/schema.prisma': [] } }, - }, - ] as never) - const unmarked = document('file:///workspace/schema.prisma', 'model User { id Int @id }') - await ownership.synchronize(unmarked) - - const actions = await middleware.provideCodeActions?.(unmarked, range, codeActionContext, token, vi.fn()) - - expect(sendRequest).toHaveBeenCalledOnce() - expect(isSnippetEdit).toHaveBeenCalledOnce() - expect(actions).toEqual([ - { - title: 'Insert block', - command: { - command: 'prisma.applySnippetWorkspaceEdit', - title: '', - arguments: [{ changes: {} }], - }, - edit: undefined, - } satisfies CodeAction, - ]) - - unmarked.setText('// use prisma-next') - expect(await middleware.provideCodeActions?.(unmarked, range, codeActionContext, token, vi.fn())).toBeUndefined() - expect(sendRequest).toHaveBeenCalledOnce() - }) - - test('allows pinned marked documents to stay synchronized with the bundled client', async () => { - const { middleware, ownership } = createSubject({ pinned: true }) - const schema = document('file:///workspace/schema.prisma', '// use prisma-next') - await ownership.synchronize(schema) - const didOpen = vi.fn() - const didChange = vi.fn() - const change = { document: schema } as unknown as TextDocumentChangeEvent - - middleware.didOpen?.(schema, didOpen) - middleware.didChange?.(change, didChange) - - expect(didOpen).toHaveBeenCalledWith(schema) - expect(didChange).toHaveBeenCalledWith(change) - }) -}) diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts b/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts deleted file mode 100644 index f186db71b7..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import { BundledClientStartup, deactivateBundledClient } from './bundledClientStartup' - -interface TestDocument { - readonly uri: string - text: string -} - -function deferred(): { promise: Promise; resolve(): void; reject(error: unknown): void } { - let resolvePromise: (() => void) | undefined - let rejectPromise: ((error: unknown) => void) | undefined - const promise = new Promise((resolve, reject) => { - resolvePromise = resolve - rejectPromise = reject - }) - return { - promise, - resolve: () => resolvePromise?.(), - reject: (error) => rejectPromise?.(error), - } -} - -function createSubject() { - const currentDocuments = new Map() - const synchronized: { document: TestDocument; text: string }[] = [] - const owners = new Map() - const logError = vi.fn() - const startup = new BundledClientStartup({ - isCurrent: (document) => currentDocuments.get(document.uri) === document, - synchronize: (document) => { - synchronized.push({ document, text: document.text }) - owners.set(document, 'bundled') - return Promise.resolve() - }, - logError, - }) - return { startup, currentDocuments, synchronized, owners, logError } -} - -describe('BundledClientStartup', () => { - test('keeps readiness failure stable until a replacement is installed', async () => { - const subject = createSubject() - const readiness = deferred() - const document = { uri: 'file:///schema.prisma', text: 'model A {}' } - subject.currentDocuments.set(document.uri, document) - - subject.startup.start(() => readiness.promise) - subject.startup.schedule(document) - readiness.reject(new Error('startup failed')) - - await vi.waitFor(() => expect(subject.startup.status).toBe('failed')) - expect(subject.synchronized).toEqual([]) - expect(subject.logError).toHaveBeenCalledOnce() - - subject.startup.schedule(document) - await Promise.resolve() - expect(subject.synchronized).toEqual([]) - expect(subject.logError).toHaveBeenCalledOnce() - - subject.startup.replace(Promise.resolve()) - subject.startup.schedule(document) - await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) - expect(subject.startup.status).toBe('ready') - }) - - test('drops a closed stale instance and synchronizes one reopened replacement', async () => { - const subject = createSubject() - const readiness = deferred() - const stale = { uri: 'file:///schema.prisma', text: 'model Stale {}' } - const replacement = { uri: stale.uri, text: 'model Current {}' } - subject.currentDocuments.set(stale.uri, stale) - - subject.startup.start(() => readiness.promise) - subject.startup.schedule(stale) - subject.currentDocuments.delete(stale.uri) - subject.currentDocuments.set(replacement.uri, replacement) - subject.startup.schedule(replacement) - readiness.resolve() - - await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) - expect(subject.synchronized).toEqual([{ document: replacement, text: replacement.text }]) - expect(subject.owners.get(stale)).toBeUndefined() - expect(subject.owners.get(replacement)).toBe('bundled') - }) - - test('coalesces startup and pending events while synchronizing the latest text', async () => { - const subject = createSubject() - const readiness = deferred() - const startClient = vi.fn(() => readiness.promise) - const document = { uri: 'file:///schema.prisma', text: 'model Initial {}' } - subject.currentDocuments.set(document.uri, document) - - subject.startup.start(startClient) - subject.startup.start(startClient) - subject.startup.schedule(document) - document.text = 'model Changed {}' - subject.startup.schedule(document) - document.text = 'model Latest {}' - subject.startup.schedule(document) - readiness.resolve() - - await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) - expect(startClient).toHaveBeenCalledOnce() - expect(subject.synchronized[0]).toEqual({ document, text: 'model Latest {}' }) - }) - - test('replacement invalidates old readiness and disposal absorbs later rejection', async () => { - const subject = createSubject() - const oldReadiness = deferred() - const replacementReadiness = deferred() - const document = { uri: 'file:///schema.prisma', text: 'model Current {}' } - subject.currentDocuments.set(document.uri, document) - - subject.startup.start(() => oldReadiness.promise) - subject.startup.schedule(document) - subject.startup.replace(replacementReadiness.promise) - subject.startup.schedule(document) - oldReadiness.resolve() - replacementReadiness.resolve() - - await vi.waitFor(() => expect(subject.synchronized).toHaveLength(1)) - expect(subject.startup.status).toBe('ready') - - const deactivationReadiness = deferred() - subject.startup.replace(deactivationReadiness.promise) - subject.startup.schedule(document) - subject.startup.dispose() - deactivationReadiness.reject(new Error('stopped during startup')) - await Promise.resolve() - - expect(subject.startup.status).toBe('disposed') - expect(subject.synchronized).toHaveLength(1) - expect(subject.logError).not.toHaveBeenCalled() - }) - - test('deactivation contains stop rejection and absorbs late startup rejection', async () => { - const subject = createSubject() - const readiness = deferred() - const document = { uri: 'file:///schema.prisma', text: 'model Current {}' } - const stopError = new Error('shutdown failed') - subject.currentDocuments.set(document.uri, document) - subject.startup.start(() => readiness.promise) - subject.startup.schedule(document) - - const deactivation = deactivateBundledClient(subject.startup, () => Promise.reject(stopError), subject.logError) - void deactivation - readiness.reject(new Error('late startup failure')) - - await expect(deactivation).resolves.toBeUndefined() - await Promise.resolve() - expect(subject.startup.status).toBe('disposed') - expect(subject.synchronized).toEqual([]) - expect(subject.logError).toHaveBeenCalledOnce() - expect(subject.logError).toHaveBeenCalledWith(stopError) - }) - - test('deactivation preserves one successful graceful stop', async () => { - const subject = createSubject() - const stop = vi.fn(() => Promise.resolve()) - - await expect(deactivateBundledClient(subject.startup, stop, subject.logError)).resolves.toBeUndefined() - - expect(stop).toHaveBeenCalledOnce() - expect(subject.startup.status).toBe('disposed') - expect(subject.logError).not.toHaveBeenCalled() - }) -}) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts deleted file mode 100644 index 4e1d30344d..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import type { TextDocument, Uri, WorkspaceFolder } from 'vscode' -import { - DocumentOwnershipCoordinator, - type DocumentOwner, - type DocumentOwnershipCoordinatorOptions, - type DocumentOwnershipTestEvent, -} from './documentOwnership' - -const rootA = workspaceFolder('file:///workspace-a') -const rootB = workspaceFolder('file:///workspace-b') - -function uri(value: string): Uri { - const scheme = value.slice(0, value.indexOf(':')) - return { scheme, toString: () => value } as Uri -} - -function workspaceFolder(value: string): WorkspaceFolder { - return { uri: uri(value) } as WorkspaceFolder -} - -function document(value: string, text: string): TextDocument & { setText(nextText: string): void } { - let currentText = text - return { - uri: uri(value), - languageId: 'prisma', - getText: () => currentText, - setText: (nextText: string) => { - currentText = nextText - }, - } as TextDocument & { setText(nextText: string): void } -} - -function coordinator(overrides: Partial = {}): DocumentOwnershipCoordinator { - return new DocumentOwnershipCoordinator({ - workspace: { - isTrusted: true, - getWorkspaceFolder: (documentUri) => - documentUri.toString().includes('workspace-a') - ? rootA - : documentUri.toString().includes('workspace-b') - ? rootB - : undefined, - }, - policy: { isPinnedToPrisma6: () => false }, - ...overrides, - }) -} - -function deferred(): { promise: Promise; resolve(): void } { - let resolvePromise: (() => void) | undefined - const promise = new Promise((resolve) => { - resolvePromise = resolve - }) - return { - promise, - resolve: () => resolvePromise?.(), - } -} - -describe('DocumentOwnershipCoordinator', () => { - test.each([ - '// use prisma-next\nmodel User { id Int @id }', - '\n\t//use prisma-next\nmodel User { id Int @id }', - '// use prisma-next \nmodel User { id Int @id }', - ])('preserves canonical Prisma Next directive matching for %j', (text) => { - const subject = coordinator() - - expect(subject.classify(document('file:///workspace-a/schema.prisma', text))).toEqual({ - kind: 'local', - workspaceFolderUri: rootA.uri.toString(), - }) - }) - - test('keeps unsupported directive spellings with the bundled owner', () => { - const subject = coordinator() - - expect(subject.classify(document('file:///workspace-a/schema.prisma', '// use prisma next'))).toEqual({ - kind: 'bundled', - }) - }) - - test('classifies marked files independently by matching workspace folder', async () => { - const subject = coordinator() - const first = document('file:///workspace-a/first.prisma', '// use prisma-next') - const second = document('file:///workspace-b/second.prisma', 'model User { id Int @id }') - - await Promise.all([subject.synchronize(first), subject.synchronize(second)]) - - expect(subject.getOwner(first.uri)).toEqual({ kind: 'local', workspaceFolderUri: rootA.uri.toString() }) - expect(subject.getOwner(second.uri)).toEqual({ kind: 'bundled' }) - }) - - test.each([ - { name: 'untrusted workspace', value: 'file:///workspace-a/schema.prisma', trusted: false }, - { name: 'non-file document', value: 'untitled:Untitled-1', trusted: true }, - { name: 'unmatched workspace', value: 'file:///outside/schema.prisma', trusted: true }, - ])('leaves a marked document unowned in an $name', ({ value, trusted }) => { - const subject = coordinator({ - workspace: { - isTrusted: trusted, - getWorkspaceFolder: (documentUri) => (documentUri.toString().includes('workspace-a') ? rootA : undefined), - }, - }) - - expect(subject.classify(document(value, '// use prisma-next'))).toEqual({ kind: 'unowned' }) - }) - - test('lets pin policy force bundled ownership', () => { - const subject = coordinator({ policy: { isPinnedToPrisma6: () => true } }) - - expect(subject.classify(document('file:///workspace-a/schema.prisma', '// use prisma-next'))).toEqual({ - kind: 'bundled', - }) - }) - - test('serializes transitions for each document URI', async () => { - let activeTransitions = 0 - let maximumActiveTransitions = 0 - const firstGate = deferred() - const secondGate = deferred() - const gates = [firstGate, secondGate] - const subject = coordinator({ - prepareOwner: async () => { - const gate = gates.shift() - activeTransitions += 1 - maximumActiveTransitions = Math.max(maximumActiveTransitions, activeTransitions) - await gate?.promise - activeTransitions -= 1 - }, - }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - - const first = subject.synchronize(schema) - await vi.waitFor(() => expect(activeTransitions).toBe(1)) - schema.setText('model User { id Int @id }') - const second = subject.synchronize(schema) - - firstGate.resolve() - await vi.waitFor(() => expect(activeTransitions).toBe(1)) - secondGate.resolve() - await Promise.all([first, second]) - - expect(maximumActiveTransitions).toBe(1) - expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) - }) - - test('reclassifies current text after asynchronous work', async () => { - const gate = deferred() - let calls = 0 - const subject = coordinator({ - prepareOwner: async () => { - calls += 1 - if (calls === 1) { - await gate.promise - } - }, - }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - - const transition = subject.synchronize(schema) - await vi.waitFor(() => expect(calls).toBe(1)) - schema.setText('model User { id Int @id }') - gate.resolve() - - await expect(transition).resolves.toEqual({ kind: 'bundled' }) - expect(calls).toBe(2) - expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) - }) - - test('reclassifies pin policy after asynchronous work', async () => { - const gate = deferred() - let pinnedToPrisma6 = false - let calls = 0 - const subject = coordinator({ - policy: { isPinnedToPrisma6: () => pinnedToPrisma6 }, - prepareOwner: async () => { - calls += 1 - if (calls === 1) { - await gate.promise - } - }, - }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - - const transition = subject.synchronize(schema) - await vi.waitFor(() => expect(calls).toBe(1)) - pinnedToPrisma6 = true - gate.resolve() - - await expect(transition).resolves.toEqual({ kind: 'bundled' }) - expect(calls).toBe(2) - expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) - }) - - test('guards ownership side effects from superseded transitions', async () => { - const firstPreparation = deferred() - const committedOwners: DocumentOwner[] = [] - let activeCommits = 0 - let maximumActiveCommits = 0 - const subject = coordinator({ - prepareOwner: async (transition) => { - if (transition.revision === 1) { - await firstPreparation.promise - } - return async () => { - activeCommits += 1 - maximumActiveCommits = Math.max(maximumActiveCommits, activeCommits) - committedOwners.push(transition.nextOwner) - await Promise.resolve() - activeCommits -= 1 - } - }, - }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - - const supersededTransition = subject.synchronize(schema) - await Promise.resolve() - schema.setText('model User { id Int @id }') - const survivingTransition = subject.synchronize(schema) - firstPreparation.resolve() - - await Promise.all([supersededTransition, survivingTransition]) - - expect(committedOwners).toEqual([{ kind: 'bundled' }]) - expect(maximumActiveCommits).toBe(1) - expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) - }) - - test('records a completed commit before the queued successor transitions', async () => { - const firstCommitStarted = deferred() - const releaseFirstCommit = deferred() - const previousOwners: DocumentOwner[] = [] - let externalOwner: DocumentOwner = { kind: 'unowned' } - let activeCommits = 0 - let maximumActiveCommits = 0 - const subject = coordinator({ - prepareOwner: (transition) => async () => { - activeCommits += 1 - maximumActiveCommits = Math.max(maximumActiveCommits, activeCommits) - previousOwners.push(transition.previousOwner) - if (transition.revision === 1) { - firstCommitStarted.resolve() - await releaseFirstCommit.promise - } - externalOwner = transition.nextOwner - activeCommits -= 1 - }, - }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - - const firstTransition = subject.synchronize(schema) - await firstCommitStarted.promise - schema.setText('model User { id Int @id }') - const survivingTransition = subject.synchronize(schema) - releaseFirstCommit.resolve() - - await Promise.all([firstTransition, survivingTransition]) - - expect(previousOwners).toEqual([{ kind: 'unowned' }, { kind: 'local', workspaceFolderUri: rootA.uri.toString() }]) - expect(externalOwner).toEqual({ kind: 'bundled' }) - expect(subject.getOwner(schema.uri)).toEqual(externalOwner) - expect(maximumActiveCommits).toBe(1) - }) - - test('close invalidates pending preparation and serializes final unowned cleanup', async () => { - const preparation = deferred() - const committedOwners: DocumentOwner[] = [] - let blockLocalPreparation = false - const subject = coordinator({ - prepareOwner: async (transition) => { - if (blockLocalPreparation && transition.nextOwner.kind === 'local') { - await preparation.promise - } - return () => { - committedOwners.push(transition.nextOwner) - } - }, - }) - const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') - await subject.synchronize(schema) - committedOwners.length = 0 - - blockLocalPreparation = true - schema.setText('// use prisma-next') - const transfer = subject.synchronize(schema) - await Promise.resolve() - const closing = subject.close(schema) - preparation.resolve() - await Promise.all([transfer, closing]) - - expect(committedOwners).toEqual([{ kind: 'unowned' }]) - expect(subject.getOwner(schema.uri)).toEqual({ kind: 'unowned' }) - }) - - test('discards stale asynchronous work after a newer transition', async () => { - const gate = deferred() - const events: DocumentOwnershipTestEvent[] = [] - let calls = 0 - const subject = coordinator({ - prepareOwner: async () => { - calls += 1 - if (calls === 1) { - await gate.promise - } - }, - testObserver: (event) => events.push(event), - }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - - const staleTransition = subject.synchronize(schema) - await vi.waitFor(() => expect(calls).toBe(1)) - schema.setText('model User { id Int @id }') - const currentTransition = subject.synchronize(schema) - gate.resolve() - - await Promise.all([staleTransition, currentTransition]) - - expect(subject.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) - expect(events).toContainEqual({ - type: 'staleTransitionDiscarded', - documentUri: schema.uri.toString(), - revision: 1, - owner: { kind: 'unowned' } satisfies DocumentOwner, - }) - expect(events.at(-1)).toEqual({ - type: 'ownerChanged', - documentUri: schema.uri.toString(), - revision: 2, - previousOwner: { kind: 'unowned' }, - owner: { kind: 'bundled' }, - }) - }) -}) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index a2613c2c50..9b542e98a4 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -28,26 +28,10 @@ export type PrepareDocumentOwnerCommit = ( transition: DocumentOwnershipTransition, ) => Promise | PreparedDocumentOwnerCommit | void -export type DocumentOwnershipTestEvent = - | { - readonly type: 'ownerChanged' - readonly documentUri: string - readonly revision: number - readonly previousOwner: DocumentOwner - readonly owner: DocumentOwner - } - | { - readonly type: 'staleTransitionDiscarded' - readonly documentUri: string - readonly revision: number - readonly owner: DocumentOwner - } - export interface DocumentOwnershipCoordinatorOptions { readonly workspace: DocumentOwnershipWorkspace readonly policy: DocumentOwnershipPolicy readonly prepareOwner?: PrepareDocumentOwnerCommit - readonly testObserver?: (event: DocumentOwnershipTestEvent) => void } interface DocumentOwnershipState { @@ -131,9 +115,7 @@ export class DocumentOwnershipCoordinator { state: DocumentOwnershipState, revision: number, ): Promise { - const documentUri = document.uri.toString() if (revision !== state.revision) { - this.observeStaleTransition(documentUri, revision, state.owner) return state.owner } @@ -144,7 +126,6 @@ export class DocumentOwnershipCoordinator { revision, }) if (revision !== state.revision) { - this.observeStaleTransition(documentUri, revision, state.owner) return state.owner } @@ -152,9 +133,7 @@ export class DocumentOwnershipCoordinator { await commitOwner() } - const previousOwner = state.owner state.owner = unownedOwner - this.observeOwnerChange(documentUri, revision, previousOwner, unownedOwner) return unownedOwner } @@ -163,8 +142,6 @@ export class DocumentOwnershipCoordinator { state: DocumentOwnershipState, revision: number, ): Promise { - const documentUri = document.uri.toString() - while (revision === state.revision) { const nextOwner = this.classify(document) const commitOwner = await this.options.prepareOwner?.({ @@ -175,7 +152,6 @@ export class DocumentOwnershipCoordinator { }) if (revision !== state.revision) { - this.observeStaleTransition(documentUri, revision, state.owner) return state.owner } @@ -188,30 +164,12 @@ export class DocumentOwnershipCoordinator { await commitOwner() } - const previousOwner = state.owner state.owner = currentOwner - this.observeOwnerChange(documentUri, revision, previousOwner, currentOwner) return currentOwner } - this.observeStaleTransition(documentUri, revision, state.owner) return state.owner } - - private observeOwnerChange( - documentUri: string, - revision: number, - previousOwner: DocumentOwner, - owner: DocumentOwner, - ): void { - if (!ownersEqual(previousOwner, owner)) { - this.options.testObserver?.({ type: 'ownerChanged', documentUri, revision, previousOwner, owner }) - } - } - - private observeStaleTransition(documentUri: string, revision: number, owner: DocumentOwner): void { - this.options.testObserver?.({ type: 'staleTransitionDiscarded', documentUri, revision, owner }) - } } function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts deleted file mode 100644 index 42588f5ebb..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import type { TextDocument, Uri, WorkspaceFolder } from 'vscode' -import { DocumentOwnershipCoordinator } from './documentOwnership' -import { - createPrepareDocumentRoutingCommit, - type BundledDocumentSynchronization, - type DocumentRoutingEvent, - type LocalDocumentSynchronization, -} from './documentRouting' - -const rootA = workspaceFolder('file:///workspace-a') -const rootB = workspaceFolder('file:///workspace-b') - -function uri(value: string): Uri { - return { scheme: value.slice(0, value.indexOf(':')), toString: () => value } as Uri -} - -function workspaceFolder(value: string): WorkspaceFolder { - return { uri: uri(value), name: value } as WorkspaceFolder -} - -function document(value: string, text: string): TextDocument & { setText(value: string): void } { - let currentText = text - let version = 1 - return { - uri: uri(value), - languageId: 'prisma', - get version() { - return version - }, - getText: () => currentText, - setText: (value) => { - currentText = value - version += 1 - }, - } as TextDocument & { setText(value: string): void } -} - -function deferred(): { promise: Promise; resolve(): void } { - let resolvePromise: (() => void) | undefined - const promise = new Promise((resolve) => { - resolvePromise = resolve - }) - return { promise, resolve: () => resolvePromise?.() } -} - -function createSubject(options: { localClose?: Promise; localStartup?: Promise } = {}) { - const active = new Set() - const closedDocumentUris = new Set() - const protocolCloses: { owner: 'bundled' | 'local'; uri: string }[] = [] - const opens: { owner: string; uri: string; text: string }[] = [] - const activeOwnerCountsAfterOpen: number[] = [] - const events: DocumentRoutingEvent[] = [] - const clearBundledDiagnostics = vi.fn() - const clearLocalDiagnostics = vi.fn() - const bundled: BundledDocumentSynchronization = { - openDocument: (schema) => { - active.add(`bundled:${schema.uri.toString()}`) - activeOwnerCountsAfterOpen.push([...active].filter((key) => key.endsWith(`:${schema.uri.toString()}`)).length) - opens.push({ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }) - }, - closeDocument: (schema) => { - if (active.delete(`bundled:${schema.uri.toString()}`)) { - protocolCloses.push({ owner: 'bundled', uri: schema.uri.toString() }) - } - }, - clearDiagnostics: clearBundledDiagnostics, - } - const ensureClientForDocument = vi.fn(async () => { - await options.localStartup - return {} - }) - const closeLocalDocument = vi.fn((root: string, schema: TextDocument) => - (options.localClose ?? Promise.resolve()).then(() => { - if (active.delete(`local:${root}:${schema.uri.toString()}`)) { - protocolCloses.push({ owner: 'local', uri: schema.uri.toString() }) - } - }), - ) - const local: LocalDocumentSynchronization = { - ensureClientForDocument, - openDocument: (root, schema) => { - active.add(`local:${root}:${schema.uri.toString()}`) - activeOwnerCountsAfterOpen.push([...active].filter((key) => key.endsWith(`:${schema.uri.toString()}`)).length) - opens.push({ owner: root, uri: schema.uri.toString(), text: schema.getText() }) - return Promise.resolve(true) - }, - closeDocument: closeLocalDocument, - clearDiagnostics: clearLocalDiagnostics, - } - const ownership: DocumentOwnershipCoordinator = new DocumentOwnershipCoordinator({ - workspace: { - isTrusted: true, - getWorkspaceFolder: (documentUri) => - documentUri.toString().includes('workspace-a') - ? rootA - : documentUri.toString().includes('workspace-b') - ? rootB - : undefined, - }, - policy: { isPinnedToPrisma6: () => false }, - prepareOwner: createPrepareDocumentRoutingCommit({ - getOwnership: (): DocumentOwnershipCoordinator => ownership, - isDocumentOpen: (schema) => !closedDocumentUris.has(schema.uri.toString()), - getBundled: () => bundled, - getLocal: () => local, - observer: (event) => events.push(event), - }), - }) - const closeEditorDocument = (schema: TextDocument): void => { - const documentUri = schema.uri.toString() - closedDocumentUris.add(documentUri) - if (active.delete(`bundled:${documentUri}`)) { - protocolCloses.push({ owner: 'bundled', uri: documentUri }) - clearBundledDiagnostics(schema.uri) - } - const root = documentUri.includes('workspace-a') ? rootA : rootB - if (active.delete(`local:${root.uri.toString()}:${documentUri}`)) { - protocolCloses.push({ owner: 'local', uri: documentUri }) - clearLocalDiagnostics(root.uri.toString(), schema.uri) - } - } - - return { - ownership, - bundled, - local, - ensureClientForDocument, - closeLocalDocument, - closeEditorDocument, - protocolCloses, - clearBundledDiagnostics, - clearLocalDiagnostics, - active, - activeOwnerCountsAfterOpen, - opens, - events, - } -} - -describe('document routing commits', () => { - test('transfers both directions with close-clear-open ordering and complete current text', async () => { - const subject = createSubject() - const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') - await subject.ownership.synchronize(schema) - subject.events.length = 0 - subject.opens.length = 0 - - schema.setText('// use prisma-next\nmodel User { id Int @id name String }') - await subject.ownership.synchronize(schema) - - expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) - expect(subject.opens).toEqual([ - { - owner: rootA.uri.toString(), - uri: schema.uri.toString(), - text: schema.getText(), - }, - ]) - expect(subject.active).toEqual(new Set([`local:${rootA.uri.toString()}:${schema.uri.toString()}`])) - expect(subject.events.at(-1)).toMatchObject({ - type: 'opened', - documentText: schema.getText(), - documentVersion: schema.version, - }) - expect(subject.activeOwnerCountsAfterOpen).toEqual([1, 1]) - expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) - - subject.events.length = 0 - subject.opens.length = 0 - schema.setText('model User { id Int @id email String }') - await subject.ownership.synchronize(schema) - - expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) - expect(subject.opens).toEqual([{ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }]) - expect(subject.active).toEqual(new Set([`bundled:${schema.uri.toString()}`])) - expect(subject.events.at(-1)).toMatchObject({ - type: 'opened', - documentText: schema.getText(), - documentVersion: schema.version, - }) - expect(subject.activeOwnerCountsAfterOpen).toEqual([1, 1, 1]) - expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) - }) - - test('awaits the prior close before clearing diagnostics or opening the next owner', async () => { - const close = deferred() - const subject = createSubject({ localClose: close.promise }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') - await subject.ownership.synchronize(schema) - subject.events.length = 0 - - schema.setText('model User { id Int @id }') - const transfer = subject.ownership.synchronize(schema) - await vi.waitFor(() => expect(subject.active).toContain(`local:${rootA.uri.toString()}:${schema.uri.toString()}`)) - - expect(subject.events).toEqual([]) - expect(subject.clearLocalDiagnostics).not.toHaveBeenCalled() - expect(subject.opens.filter(({ owner }) => owner === 'bundled')).toHaveLength(0) - - close.resolve() - await transfer - - expect(subject.events.map((event) => event.type)).toEqual(['closed', 'diagnosticsCleared', 'opened']) - }) - - test('does not reopen locally when the editor closes during local startup', async () => { - const startup = deferred() - const subject = createSubject({ localStartup: startup.promise }) - const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') - await subject.ownership.synchronize(schema) - subject.opens.length = 0 - - schema.setText('// use prisma-next\nmodel User { id Int @id }') - const transfer = subject.ownership.synchronize(schema) - await vi.waitFor(() => expect(subject.ensureClientForDocument).toHaveBeenCalledOnce()) - - subject.closeEditorDocument(schema) - const closing = subject.ownership.close(schema) - startup.resolve() - await Promise.all([transfer, closing]) - - expect(subject.opens).toEqual([]) - expect(subject.active).toEqual(new Set()) - expect(subject.protocolCloses).toEqual([{ owner: 'bundled', uri: schema.uri.toString() }]) - expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) - expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) - expect(subject.ownership.getOwner(schema.uri)).toEqual({ kind: 'unowned' }) - }) - - test('does not reopen bundled when the editor closes during a delayed prior-owner close', async () => { - const close = deferred() - const subject = createSubject({ localClose: close.promise }) - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') - await subject.ownership.synchronize(schema) - subject.opens.length = 0 - - schema.setText('model User { id Int @id }') - const transfer = subject.ownership.synchronize(schema) - await vi.waitFor(() => expect(subject.closeLocalDocument).toHaveBeenCalledOnce()) - - subject.closeEditorDocument(schema) - const closing = subject.ownership.close(schema) - close.resolve() - await Promise.all([transfer, closing]) - - expect(subject.opens).toEqual([]) - expect(subject.active).toEqual(new Set()) - expect(subject.protocolCloses).toEqual([{ owner: 'local', uri: schema.uri.toString() }]) - expect(subject.clearLocalDiagnostics).toHaveBeenCalledWith(rootA.uri.toString(), schema.uri) - expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(schema.uri) - expect(subject.ownership.getOwner(schema.uri)).toEqual({ kind: 'unowned' }) - }) - - test('does not open a stale local candidate when text changes during startup', async () => { - const startup = deferred() - const subject = createSubject({ localStartup: startup.promise }) - const schema = document('file:///workspace-a/schema.prisma', 'model User { id Int @id }') - await subject.ownership.synchronize(schema) - subject.opens.length = 0 - - schema.setText('// use prisma-next\nmodel User { id Int @id }') - const staleLocal = subject.ownership.synchronize(schema) - await vi.waitFor(() => expect(subject.active.size).toBe(0)) - schema.setText('model User { id Int @id current String }') - const survivingBundled = subject.ownership.synchronize(schema) - startup.resolve() - await Promise.all([staleLocal, survivingBundled]) - - expect(subject.opens).toEqual([{ owner: 'bundled', uri: schema.uri.toString(), text: schema.getText() }]) - expect(subject.ownership.getOwner(schema.uri)).toEqual({ kind: 'bundled' }) - expect(subject.active).toEqual(new Set([`bundled:${schema.uri.toString()}`])) - }) - - test('repeated synchronization is idempotent for unchanged ownership', async () => { - const subject = createSubject() - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') - - await subject.ownership.synchronize(schema) - subject.events.length = 0 - subject.opens.length = 0 - await Promise.all([ - subject.ownership.synchronize(schema), - subject.ownership.synchronize(schema), - subject.ownership.synchronize(schema), - ]) - - expect(subject.events).toEqual([]) - expect(subject.opens).toEqual([]) - expect(subject.active).toEqual(new Set([`local:${rootA.uri.toString()}:${schema.uri.toString()}`])) - }) - - test('routes same-root schema files independently', async () => { - const subject = createSubject() - const marked = document('file:///workspace-a/marked.prisma', '// use prisma-next\nmodel A { id Int @id }') - const unmarked = document('file:///workspace-a/unmarked.prisma', 'model B { id Int @id }') - - await Promise.all([subject.ownership.synchronize(marked), subject.ownership.synchronize(unmarked)]) - unmarked.setText('// use prisma-next\nmodel B { id Int @id name String }') - await subject.ownership.synchronize(unmarked) - - expect(subject.active).toEqual( - new Set([ - `local:${rootA.uri.toString()}:${marked.uri.toString()}`, - `local:${rootA.uri.toString()}:${unmarked.uri.toString()}`, - ]), - ) - expect(subject.opens.filter(({ uri }) => uri === marked.uri.toString())).toHaveLength(1) - expect(subject.opens.filter(({ uri }) => uri === unmarked.uri.toString())).toHaveLength(2) - expect(subject.clearBundledDiagnostics).toHaveBeenCalledWith(unmarked.uri) - }) - - test('keeps local ownership isolated per document and workspace root', async () => { - const subject = createSubject() - const schemaA = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel A { id Int @id }') - const schemaB = document('file:///workspace-b/schema.prisma', '// use prisma-next\nmodel B { id Int @id }') - - await Promise.all([subject.ownership.synchronize(schemaA), subject.ownership.synchronize(schemaB)]) - - expect(subject.opens.map(({ owner, uri }) => ({ owner, uri }))).toEqual([ - { owner: rootA.uri.toString(), uri: schemaA.uri.toString() }, - { owner: rootB.uri.toString(), uri: schemaB.uri.toString() }, - ]) - }) -}) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index 93cd39ee1d..e6d18fe7af 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -14,23 +14,11 @@ export interface LocalDocumentSynchronization { clearDiagnostics(workspaceFolderUri: string, uri: Uri): Promise } -export type DocumentRoutingEvent = - | { readonly type: 'closed'; readonly owner: DocumentOwner; readonly documentUri: string } - | { readonly type: 'diagnosticsCleared'; readonly owner: DocumentOwner; readonly documentUri: string } - | { - readonly type: 'opened' - readonly owner: DocumentOwner - readonly documentUri: string - readonly documentText: string - readonly documentVersion: number - } - export interface DocumentRoutingOptions { readonly getOwnership: () => DocumentOwnershipCoordinator readonly isDocumentOpen: (document: TextDocument) => boolean readonly getBundled: () => BundledDocumentSynchronization readonly getLocal: () => LocalDocumentSynchronization - readonly observer?: (event: DocumentRoutingEvent) => void } export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptions): PrepareDocumentOwnerCommit { @@ -44,15 +32,11 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio if (nextOwner.kind === 'bundled') { options.getBundled().openDocument(document) - observeOpened(options, nextOwner, document) } else if (nextOwner.kind === 'local') { const local = options.getLocal() const client = await local.ensureClientForDocument(document) if (client && isCurrentOpenCandidate(options, document, nextOwner)) { - const opened = await local.openDocument(nextOwner.workspaceFolderUri, document) - if (opened && isCurrentOpenCandidate(options, document, nextOwner)) { - observeOpened(options, nextOwner, document) - } + await local.openDocument(nextOwner.workspaceFolderUri, document) } } } @@ -66,29 +50,14 @@ async function closePreviousOwner( ): Promise { if (previousOwner.kind === 'bundled') { options.getBundled().closeDocument(document) - options.observer?.({ type: 'closed', owner: previousOwner, documentUri: document.uri.toString() }) options.getBundled().clearDiagnostics(document.uri) - options.observer?.({ type: 'diagnosticsCleared', owner: previousOwner, documentUri: document.uri.toString() }) } else if (previousOwner.kind === 'local') { const local = options.getLocal() await local.closeDocument(previousOwner.workspaceFolderUri, document) - options.observer?.({ type: 'closed', owner: previousOwner, documentUri: document.uri.toString() }) await local.clearDiagnostics(previousOwner.workspaceFolderUri, document.uri) - options.observer?.({ type: 'diagnosticsCleared', owner: previousOwner, documentUri: document.uri.toString() }) } } -function observeOpened(options: DocumentRoutingOptions, owner: DocumentOwner, document: TextDocument): void { - if (!options.observer) return - options.observer({ - type: 'opened', - owner, - documentUri: document.uri.toString(), - documentText: document.getText(), - documentVersion: document.version, - }) -} - function isCurrentOpenCandidate( options: DocumentRoutingOptions, document: TextDocument, diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 356fbe8168..71ff6ddcc0 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -24,8 +24,7 @@ import { getPackageJSON } from '../../getPackageJSON' import { DocumentOwnershipCoordinator } from './documentOwnership' import { createBundledClientMiddleware, type BundledClientMiddleware } from './bundledClientMiddleware' import { createPrepareDocumentRoutingCommit } from './documentRouting' -import { LocalPrismaNextClientRegistry, localPrismaNextClientTestStateCommand } from './localPrismaNextClientRegistry' -import { LanguageServerTestStateCollector, languageServerTestStateCommand } from './languageServerTestState' +import { LocalPrismaNextClientRegistry } from './localPrismaNextClientRegistry' import { BundledClientStartup, deactivateBundledClient } from './bundledClientStartup' let client: LanguageClient @@ -108,7 +107,6 @@ const plugin: PrismaVSCodePlugin = { enabled: () => true, activate: async (context) => { const isDebugOrTest = isDebugOrTestSession() - const testState = isDebugOrTest ? new LanguageServerTestStateCollector() : undefined const codelensProvider = new CodelensProvider() languages.registerCodeLensProvider('*', codelensProvider) @@ -120,13 +118,11 @@ const plugin: PrismaVSCodePlugin = { policy: { isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), }, - testObserver: testState?.observeOwnership, prepareOwner: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, isDocumentOpen: (document) => workspace.textDocuments.includes(document), getBundled: () => bundledClientMiddleware, getLocal: () => localClients, - observer: testState?.observeRouting, }), }) const localClients = new LocalPrismaNextClientRegistry({ @@ -136,7 +132,6 @@ const plugin: PrismaVSCodePlugin = { createClient: (id, name, serverOptions, localClientOptions) => new LanguageClient(id, name, serverOptions, localClientOptions), registerDisposable: (disposable) => context.subscriptions.push(disposable), - collectTestState: isDebugOrTest, handleStartError: (workspaceFolder, error) => { console.error(`Failed to start Prisma Next Language Server for ${workspaceFolder.uri.toString()}`, error) }, @@ -271,14 +266,7 @@ const plugin: PrismaVSCodePlugin = { synchronizeDocument(document) } - if (isDebugOrTest) { - context.subscriptions.push( - commands.registerCommand(localPrismaNextClientTestStateCommand, () => localClients.getTestState()), - commands.registerCommand(languageServerTestStateCommand, () => - testState?.snapshot(workspace.isTrusted, localClients.getTestState()), - ), - ) - } else { + if (!isDebugOrTest) { const packageJSON = getPackageJSON(context) const extensionId = 'prisma.' + packageJSON.name const extensionVersion = packageJSON.version ?? 'unknown' diff --git a/packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts b/packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts deleted file mode 100644 index 337075331e..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/languageServerTestState.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { DocumentOwnershipTestEvent } from './documentOwnership' -import type { DocumentRoutingEvent } from './documentRouting' -import type { LocalPrismaNextClientTestState } from './localPrismaNextClientRegistry' - -export const languageServerTestStateCommand = 'prisma.test.languageServerRoutingState' - -export interface LanguageServerTestState { - readonly workspaceTrusted: boolean - readonly localClients: LocalPrismaNextClientTestState - readonly ownershipEvents: readonly DocumentOwnershipTestEvent[] - readonly routingEvents: readonly DocumentRoutingEvent[] -} - -export class LanguageServerTestStateCollector { - private readonly ownershipEvents: DocumentOwnershipTestEvent[] = [] - private readonly routingEvents: DocumentRoutingEvent[] = [] - - readonly observeOwnership = (event: DocumentOwnershipTestEvent): void => { - this.ownershipEvents.push(event) - } - - readonly observeRouting = (event: DocumentRoutingEvent): void => { - this.routingEvents.push(event) - } - - snapshot(workspaceTrusted: boolean, localClients: LocalPrismaNextClientTestState): LanguageServerTestState { - return { - workspaceTrusted, - localClients, - ownershipEvents: [...this.ownershipEvents], - routingEvents: [...this.routingEvents], - } - } -} diff --git a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts deleted file mode 100644 index 96e8f2f609..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import type { - CancellationToken, - CodeActionContext, - CompletionContext, - CompletionItem, - Diagnostic, - FormattingOptions, - Position, - Range, - TextDocument, - TextDocumentChangeEvent, - Uri, - WorkspaceFolder, -} from 'vscode' -import type { LanguageClient } from 'vscode-languageclient/node' -import { DocumentOwnershipCoordinator } from './documentOwnership' -import { createLocalClientMiddleware } from './localClientMiddleware' - -const rootA = workspaceFolder('file:///workspace-a') -const rootB = workspaceFolder('file:///workspace-b') -const position = {} as Position -const range = {} as Range -const token = {} as CancellationToken -const completionContext = {} as CompletionContext -const formattingOptions = {} as FormattingOptions -const codeActionContext = { diagnostics: [] } as unknown as CodeActionContext - -function uri(value: string): Uri { - return { scheme: value.slice(0, value.indexOf(':')), toString: () => value } as Uri -} - -function workspaceFolder(value: string): WorkspaceFolder { - return { uri: uri(value), name: value } as WorkspaceFolder -} - -function document(value: string, text: string): TextDocument & { setText(value: string): void } { - let currentText = text - return { - uri: uri(value), - languageId: 'prisma', - version: 7, - getText: () => currentText, - setText: (value) => { - currentText = value - }, - } as TextDocument & { setText(value: string): void } -} - -function createSubject() { - const documents = new Map() - const sendNotification = vi.fn() - const deleteDiagnostics = vi.fn() - const client = { - code2ProtocolConverter: { - asOpenTextDocumentParams: (schema: TextDocument) => ({ - textDocument: { - uri: schema.uri.toString(), - languageId: schema.languageId, - version: schema.version, - text: schema.getText(), - }, - }), - asCloseTextDocumentParams: (schema: TextDocument) => ({ textDocument: { uri: schema.uri.toString() } }), - }, - diagnostics: { delete: deleteDiagnostics }, - sendNotification, - } as unknown as LanguageClient - const ownership = new DocumentOwnershipCoordinator({ - workspace: { - isTrusted: true, - getWorkspaceFolder: (documentUri) => - documentUri.toString().includes('workspace-a') - ? rootA - : documentUri.toString().includes('workspace-b') - ? rootB - : undefined, - }, - policy: { isPinnedToPrisma6: () => false }, - }) - const middleware = createLocalClientMiddleware({ - workspaceFolderUri: rootA.uri.toString(), - ownership, - getClient: () => client, - getDocument: (documentUri) => documents.get(documentUri.toString()), - }) - return { middleware, ownership, documents, sendNotification, deleteDiagnostics } -} - -describe('local client ownership middleware', () => { - test('filters automatic initial synchronization and never sends unmarked contents', async () => { - const { middleware, ownership, sendNotification, deleteDiagnostics } = createSubject() - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next\nmodel User { id Int @id }') - const unmarked = document('file:///workspace-a/unmarked.prisma', 'model Secret { id Int @id }') - const automaticOpen = vi.fn() - const changeNext = vi.fn() - - middleware.didOpen?.(schema, automaticOpen) - middleware.didOpen?.(unmarked, automaticOpen) - expect(automaticOpen).not.toHaveBeenCalled() - - await ownership.synchronize(schema) - middleware.openDocument(schema) - expect(sendNotification).toHaveBeenCalledWith('textDocument/didOpen', { - textDocument: { - uri: schema.uri.toString(), - languageId: 'prisma', - version: 7, - text: schema.getText(), - }, - }) - - middleware.didChange?.({ document: schema } as unknown as TextDocumentChangeEvent, changeNext) - expect(changeNext).toHaveBeenCalledOnce() - - schema.setText('model User { id Int @id leaked String }') - middleware.didChange?.({ document: schema } as unknown as TextDocumentChangeEvent, changeNext) - middleware.closeDocument(schema) - middleware.clearDiagnostics(schema.uri) - - expect(changeNext).toHaveBeenCalledOnce() - expect(sendNotification).toHaveBeenLastCalledWith('textDocument/didClose', { - textDocument: { uri: schema.uri.toString() }, - }) - const notificationContents = sendNotification.mock.calls.flatMap(([, params]) => JSON.stringify(params)) - expect(notificationContents).not.toContain('leaked') - expect(notificationContents).not.toContain('Secret') - expect(deleteDiagnostics).toHaveBeenCalledWith(schema.uri) - }) - - test('forwards every document feature only for committed ownership in the exact root', async () => { - const { middleware, ownership, documents } = createSubject() - const owned = document('file:///workspace-a/schema.prisma', '// use prisma-next') - const otherRoot = document('file:///workspace-b/schema.prisma', '// use prisma-next') - documents.set(owned.uri.toString(), owned) - await Promise.all([ownership.synchronize(owned), ownership.synchronize(otherRoot)]) - - const completion = { label: 'id' } as CompletionItem - const completionNext = vi.fn().mockReturnValue([completion]) - await expect( - middleware.provideCompletionItem?.(owned, position, completionContext, token, completionNext), - ).resolves.toEqual([completion]) - const resolveNext = vi.fn().mockReturnValue(completion) - expect(middleware.resolveCompletionItem?.(completion, token, resolveNext)).toBe(completion) - - const next = vi.fn().mockReturnValue('forwarded') - expect(middleware.provideHover?.(owned, position, token, next)).toBe('forwarded') - expect(middleware.provideDefinition?.(owned, position, token, next)).toBe('forwarded') - expect(middleware.provideReferences?.(owned, position, { includeDeclaration: true }, token, next)).toBe('forwarded') - expect(middleware.provideDocumentSymbols?.(owned, token, next)).toBe('forwarded') - expect(middleware.provideDocumentFormattingEdits?.(owned, formattingOptions, token, next)).toBe('forwarded') - expect(middleware.provideRenameEdits?.(owned, position, 'Renamed', token, next)).toBe('forwarded') - expect(middleware.provideCodeActions?.(owned, range, codeActionContext, token, next)).toBe('forwarded') - - const rejected = vi.fn() - expect(middleware.provideHover?.(otherRoot, position, token, rejected)).toBeUndefined() - owned.setText('model User { id Int @id }') - expect(middleware.provideDefinition?.(owned, position, token, rejected)).toBeUndefined() - expect(rejected).not.toHaveBeenCalled() - }) - - test('filters diagnostics outside exact committed ownership', async () => { - const { middleware, ownership, documents } = createSubject() - const schema = document('file:///workspace-a/schema.prisma', '// use prisma-next') - documents.set(schema.uri.toString(), schema) - const next = vi.fn() - - const beforeOwnership = [{ message: 'before ownership' }] as Diagnostic[] - const owned = [{ message: 'owned' }] as Diagnostic[] - const stale = [{ message: 'stale' }] as Diagnostic[] - - middleware.handleDiagnostics?.(schema.uri, beforeOwnership, next) - await ownership.synchronize(schema) - middleware.handleDiagnostics?.(schema.uri, owned, next) - schema.setText('model User { id Int @id }') - middleware.handleDiagnostics?.(schema.uri, stale, next) - - expect(next.mock.calls).toEqual([ - [schema.uri, []], - [schema.uri, owned], - [schema.uri, []], - ]) - }) -}) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts deleted file mode 100644 index 451120d71f..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import path from 'node:path' -import { EventEmitter } from 'node:events' -import { PassThrough } from 'node:stream' -import type { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'node:child_process' -import { describe, expect, test, vi } from 'vitest' -import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' -import type { LanguageClientOptions } from 'vscode-languageclient' -import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' -import { DocumentOwnershipCoordinator } from './documentOwnership' -import type { LocalClientMiddleware } from './localClientMiddleware' -import { - createExtensionHostNodeEnvironment, - createLocalPrismaNextClientOptions, - createLocalPrismaNextServerOptions, - getLocalPrismaNextEntrypoint, - launchLocalPrismaNextServer, - LocalPrismaNextClientRegistry, -} from './localPrismaNextClientRegistry' - -const rootA = workspaceFolder('file:///workspace-a', '/workspace-a', 'workspace-a') -const rootB = workspaceFolder('file:///workspace-b', '/workspace-b', 'workspace-b') -const ownership = new DocumentOwnershipCoordinator({ - workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, - policy: { isPinnedToPrisma6: () => false }, -}) -const registryRoutingOptions = { - ownership, - getDocument: () => undefined, -} - -function uri(value: string, fsPath = value): Uri { - return { - scheme: value.slice(0, value.indexOf(':')), - fsPath, - toString: () => value, - } as Uri -} - -function workspaceFolder(value: string, fsPath: string, name: string): WorkspaceFolder { - return { uri: uri(value, fsPath), name } as WorkspaceFolder -} - -function document(value: string): TextDocument { - return { - uri: uri(value), - languageId: 'prisma', - getText: () => '// use prisma-next', - } as TextDocument -} - -function deferred(): { promise: Promise; resolve(value: T): void } { - let resolvePromise: ((value: T) => void) | undefined - const promise = new Promise((resolve) => { - resolvePromise = resolve - }) - return { - promise, - resolve: (value) => resolvePromise?.(value), - } -} - -function fakeClient(name: string, onReady = vi.fn().mockResolvedValue(undefined)): LanguageClient { - return { - name, - start: vi.fn().mockReturnValue({ dispose: vi.fn() } satisfies Disposable), - onReady, - } as unknown as LanguageClient -} - -function fakeChildProcess(pid = 123): ChildProcessWithoutNullStreams { - let killed = false - const child = Object.assign(new EventEmitter(), { - stdin: new PassThrough(), - stdout: new PassThrough(), - stderr: new PassThrough(), - pid, - kill: vi.fn(() => { - killed = true - return true - }), - }) - Object.defineProperty(child, 'killed', { get: () => killed }) - return child as unknown as ChildProcessWithoutNullStreams -} - -function invokeServerOptions(serverOptions: ServerOptions): Promise { - expect(serverOptions).toBeTypeOf('function') - return (serverOptions as () => Promise)() -} - -function matchingWorkspaceFolder(documentUri: Uri): WorkspaceFolder | undefined { - if (documentUri.toString().includes('workspace-a')) return rootA - if (documentUri.toString().includes('workspace-b')) return rootB - return undefined -} - -describe('LocalPrismaNextClientRegistry', () => { - test('launches the exact CLI argv with extension-host Node and root-local streams', async () => { - const entrypoint = getLocalPrismaNextEntrypoint(rootA) - const child = fakeChildProcess() - const spawnProcess = vi.fn((_executable: string, _args: string[], _options: SpawnOptionsWithoutStdio) => { - queueMicrotask(() => child.emit('spawn')) - return child - }) - const handleProcessError = vi.fn() - const serverOptions = createLocalPrismaNextServerOptions(rootA, entrypoint, { - executable: '/extension-host', - environment: { EXISTING: 'preserved' }, - spawnProcess, - handleProcessError, - }) - - const result = await invokeServerOptions(serverOptions) - - expect(entrypoint).toBe(path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js')) - expect(spawnProcess).toHaveBeenCalledOnce() - expect(spawnProcess).toHaveBeenCalledWith('/extension-host', [entrypoint, 'lsp'], { - cwd: '/workspace-a', - env: { - EXISTING: 'preserved', - ELECTRON_RUN_AS_NODE: '1', - ELECTRON_NO_ASAR: '1', - }, - shell: false, - stdio: ['pipe', 'pipe', 'pipe'], - }) - expect(result).toEqual({ process: child, detached: false }) - expect(result.process.stdin).toBe(child.stdin) - expect(result.process.stdout).toBe(child.stdout) - expect(result.process.stderr).toBe(child.stderr) - - const processError = new Error('process error') - child.emit('error', processError) - expect(handleProcessError).toHaveBeenCalledWith(processError) - }) - - test('preserves the environment while enabling Electron extension hosts to run as Node', () => { - expect(createExtensionHostNodeEnvironment({ EXISTING: 'preserved', ELECTRON_RUN_AS_NODE: '0' })).toEqual({ - EXISTING: 'preserved', - ELECTRON_RUN_AS_NODE: '1', - ELECTRON_NO_ASAR: '1', - }) - }) - - test('rejects early spawn errors and releases startup resources', async () => { - const child = fakeChildProcess() - const startError = new Error('spawn failed') - const spawnProcess = vi.fn(() => { - queueMicrotask(() => child.emit('error', startError)) - return child - }) - - await expect( - launchLocalPrismaNextServer({ - executable: '/extension-host', - entrypoint: '/workspace-a/node_modules/prisma/dist/prisma.js', - cwd: '/workspace-a', - environment: {}, - spawnProcess, - }), - ).rejects.toBe(startError) - - expect(child.killed).toBe(true) - expect(child.stdin.destroyed).toBe(true) - expect(child.stdout.destroyed).toBe(true) - expect(child.stderr.destroyed).toBe(true) - expect(child.listenerCount('error')).toBe(0) - expect(child.listenerCount('spawn')).toBe(0) - }) - - test('constrains provider registration to the matching root', () => { - const middleware = {} as LocalClientMiddleware - expect(createLocalPrismaNextClientOptions(rootA, middleware)).toEqual({ - documentSelector: [{ language: 'prisma', scheme: 'file', pattern: '/workspace-a/**/*' }], - workspaceFolder: rootA, - middleware, - }) - }) - - test('uses a relative root selector for Windows workspace paths', () => { - const windowsRoot = workspaceFolder('file:///C:/workspace-a', 'C:\\workspace-a', 'workspace-a') - const middleware = {} as LocalClientMiddleware - - expect(createLocalPrismaNextClientOptions(windowsRoot, middleware).documentSelector).toEqual([ - { language: 'prisma', scheme: 'file', pattern: 'C:/workspace-a/**/*' }, - ]) - }) - - test('does not synchronize a document that closes while its client entry is pending', async () => { - const ready = deferred() - const schema = document('file:///workspace-a/schema.prisma') - const documents = new Map([[schema.uri.toString(), schema]]) - const client = fakeClient( - 'root-a', - vi.fn(() => ready.promise), - ) - const registry = new LocalPrismaNextClientRegistry({ - ownership, - getDocument: (documentUri) => documents.get(documentUri.toString()), - workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, - entrypointExists: vi.fn().mockResolvedValue(true), - createClient: vi.fn().mockReturnValue(client), - registerDisposable: vi.fn(), - }) - - const startup = registry.ensureClientForDocument(schema) - const open = registry.openDocument(rootA.uri.toString(), schema) - documents.delete(schema.uri.toString()) - ready.resolve(undefined) - - await expect(startup).resolves.toBe(client) - await expect(open).resolves.toBe(false) - expect(registry.getTestState()).toEqual({ - startedWorkspaceFolderUris: [rootA.uri.toString()], - startCountsByWorkspaceFolderUri: {}, - }) - }) - - test('publishes pending startup per root and starts independent clients', async () => { - const discovery = deferred() - const entrypointExists = vi.fn().mockReturnValue(discovery.promise) - const clients = new Map() - const spawnProcess = vi.fn((_executable: string, _args: string[], _options: SpawnOptionsWithoutStdio) => { - const child = fakeChildProcess() - queueMicrotask(() => child.emit('spawn')) - return child - }) - const createClient = vi.fn( - (_id: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions) => { - const client = fakeClient( - name, - vi.fn(() => invokeServerOptions(serverOptions).then(() => undefined)), - ) - clients.set(clientOptions.workspaceFolder?.uri.toString() ?? '', client) - return client - }, - ) - const registerDisposable = vi.fn() - const registry = new LocalPrismaNextClientRegistry({ - ...registryRoutingOptions, - workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, - entrypointExists, - createClient, - registerDisposable, - collectTestState: true, - launcher: { - executable: '/extension-host', - environment: {}, - spawnProcess, - }, - }) - - const firstRootA = registry.ensureClientForDocument(document('file:///workspace-a/first.prisma')) - const secondRootA = registry.ensureClientForDocument(document('file:///workspace-a/second.prisma')) - const firstRootB = registry.ensureClientForDocument(document('file:///workspace-b/schema.prisma')) - - await vi.waitFor(() => expect(entrypointExists).toHaveBeenCalledTimes(2)) - expect(createClient).not.toHaveBeenCalled() - discovery.resolve(true) - - const results = await Promise.all([firstRootA, secondRootA, firstRootB]) - - expect(results).toEqual([ - clients.get(rootA.uri.toString()), - clients.get(rootA.uri.toString()), - clients.get(rootB.uri.toString()), - ]) - expect(createClient).toHaveBeenCalledTimes(2) - expect(registerDisposable).toHaveBeenCalledTimes(2) - expect(spawnProcess).toHaveBeenCalledTimes(2) - expect( - spawnProcess.mock.calls.map(([executable, args, options]) => ({ executable, args, cwd: options.cwd })), - ).toEqual([ - { - executable: '/extension-host', - args: [path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js'), 'lsp'], - cwd: '/workspace-a', - }, - { - executable: '/extension-host', - args: [path.join('/workspace-b', 'node_modules', 'prisma', 'dist', 'prisma.js'), 'lsp'], - cwd: '/workspace-b', - }, - ]) - expect(registry.getTestState()).toEqual({ - startedWorkspaceFolderUris: [rootA.uri.toString(), rootB.uri.toString()], - startCountsByWorkspaceFolderUri: { - [rootA.uri.toString()]: 1, - [rootB.uri.toString()]: 1, - }, - }) - }) - - test('does no discovery until an eligible document requests a client', async () => { - const entrypointExists = vi.fn().mockResolvedValue(false) - const createClient = vi.fn() - const handleStartError = vi.fn() - const registry = new LocalPrismaNextClientRegistry({ - ...registryRoutingOptions, - workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, - entrypointExists, - createClient, - registerDisposable: vi.fn(), - handleStartError, - }) - - expect(entrypointExists).not.toHaveBeenCalled() - expect(createClient).not.toHaveBeenCalled() - - const schema = document('file:///workspace-a/schema.prisma') - await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() - await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() - - expect(entrypointExists).toHaveBeenCalledOnce() - expect(entrypointExists).toHaveBeenCalledWith( - path.join('/workspace-a', 'node_modules', 'prisma', 'dist', 'prisma.js'), - ) - expect(createClient).not.toHaveBeenCalled() - expect(handleStartError).not.toHaveBeenCalled() - }) - - test.each([ - { name: 'untrusted workspace', trusted: false, documentUri: 'file:///workspace-a/schema.prisma' }, - { name: 'non-file document', trusted: true, documentUri: 'untitled:Untitled-1' }, - { name: 'unmatched workspace', trusted: true, documentUri: 'file:///outside/schema.prisma' }, - ])('does not discover or start for an $name', async ({ trusted, documentUri }) => { - const entrypointExists = vi.fn().mockResolvedValue(true) - const createClient = vi.fn() - const registry = new LocalPrismaNextClientRegistry({ - ...registryRoutingOptions, - workspace: { isTrusted: trusted, getWorkspaceFolder: matchingWorkspaceFolder }, - entrypointExists, - createClient, - registerDisposable: vi.fn(), - }) - - await expect(registry.ensureClientForDocument(document(documentUri))).resolves.toBeUndefined() - - expect(entrypointExists).not.toHaveBeenCalled() - expect(createClient).not.toHaveBeenCalled() - }) - - test('reports a real startup failure once without retrying automatically', async () => { - const startError = new Error('startup failed') - const client = fakeClient('root-a', vi.fn().mockRejectedValue(startError)) - const handleStartError = vi.fn() - const createClient = vi.fn().mockReturnValue(client) - const registry = new LocalPrismaNextClientRegistry({ - ...registryRoutingOptions, - workspace: { isTrusted: true, getWorkspaceFolder: matchingWorkspaceFolder }, - entrypointExists: vi.fn().mockResolvedValue(true), - createClient, - registerDisposable: vi.fn(), - handleStartError, - }) - const schema = document('file:///workspace-a/schema.prisma') - - await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() - await expect(registry.ensureClientForDocument(schema)).resolves.toBeUndefined() - - expect(createClient).toHaveBeenCalledOnce() - expect(handleStartError).toHaveBeenCalledOnce() - expect(handleStartError).toHaveBeenCalledWith(rootA, startError) - expect(registry.getTestState()).toEqual({ - startedWorkspaceFolderUris: [], - startCountsByWorkspaceFolderUri: {}, - }) - }) -}) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index 3f9602f2a7..6f6fba57fb 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -9,8 +9,6 @@ import { createLocalClientMiddleware, type LocalClientMiddleware } from './local const prismaCliRelativePath = ['node_modules', 'prisma', 'dist', 'prisma.js'] as const -export const localPrismaNextClientTestStateCommand = 'prisma.test.localPrismaNextClientState' - export interface LocalPrismaNextClientRegistryWorkspace { readonly isTrusted: boolean getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined @@ -43,12 +41,6 @@ export interface LocalPrismaNextClientRegistryOptions { readonly entrypointExists?: (entrypoint: string) => Promise readonly handleStartError?: (workspaceFolder: WorkspaceFolder, error: unknown) => void readonly launcher?: Omit - readonly collectTestState?: boolean -} - -export interface LocalPrismaNextClientTestState { - readonly startedWorkspaceFolderUris: readonly string[] - readonly startCountsByWorkspaceFolderUri: Readonly> } interface LocalPrismaNextClientEntry { @@ -58,12 +50,8 @@ interface LocalPrismaNextClientEntry { export class LocalPrismaNextClientRegistry { private readonly clients = new Map>() - private readonly startedClients = new Map() - private readonly startCounts: Map | undefined - constructor(private readonly options: LocalPrismaNextClientRegistryOptions) { - this.startCounts = options.collectTestState ? new Map() : undefined - } + constructor(private readonly options: LocalPrismaNextClientRegistryOptions) {} ensureClientForDocument(document: TextDocument): Promise { if (!this.options.workspace.isTrusted || document.uri.scheme !== 'file') { @@ -97,15 +85,6 @@ export class LocalPrismaNextClientRegistry { entry?.middleware.clearDiagnostics(uri) } - getTestState(): LocalPrismaNextClientTestState { - return { - startedWorkspaceFolderUris: [...this.startedClients.keys()].sort(), - startCountsByWorkspaceFolderUri: Object.fromEntries( - [...(this.startCounts?.entries() ?? [])].sort(([left], [right]) => left.localeCompare(right)), - ), - } - } - private ensureClient(workspaceFolder: WorkspaceFolder): Promise { const workspaceFolderUri = workspaceFolder.uri.toString() const existing = this.clients.get(workspaceFolderUri) @@ -145,12 +124,7 @@ export class LocalPrismaNextClientRegistry { ) this.options.registerDisposable(client.start()) await client.onReady() - const entry = { client, middleware } - this.startedClients.set(workspaceFolderUri, entry) - if (this.startCounts) { - this.startCounts.set(workspaceFolderUri, (this.startCounts.get(workspaceFolderUri) ?? 0) + 1) - } - return entry + return { client, middleware } } catch (error) { this.options.handleStartError?.(workspaceFolder, error) return undefined diff --git a/packages/vscode/src/util.ts b/packages/vscode/src/util.ts index 56284877ce..d9601e135c 100644 --- a/packages/vscode/src/util.ts +++ b/packages/vscode/src/util.ts @@ -16,7 +16,7 @@ import { homedir } from 'os' import { readdirSync } from 'fs' import path from 'path' export function isDebugOrTestSession(): boolean { - return env.sessionId === 'someValue.sessionId' || process.env.PRISMA_VSCODE_TEST === '1' + return env.sessionId === 'someValue.sessionId' } export { isPrismaNextSchema } diff --git a/packages/vscode/tests/fixtures/integration-workspace.code-workspace b/packages/vscode/tests/fixtures/integration-workspace.code-workspace index 7912a21eca..4d853945d9 100644 --- a/packages/vscode/tests/fixtures/integration-workspace.code-workspace +++ b/packages/vscode/tests/fixtures/integration-workspace.code-workspace @@ -4,13 +4,5 @@ "name": "integration-root-a", "path": "integration-workspace/root-a", }, - { - "name": "integration-root-b", - "path": "integration-workspace/root-b", - }, - { - "name": "integration-root-missing", - "path": "integration-workspace/root-missing", - }, ], } diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/bundled.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/bundled.prisma new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/next.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/next.prisma new file mode 100644 index 0000000000..066ed51cbd --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/next.prisma @@ -0,0 +1 @@ +// use prisma-next diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json index c16769f044..fd72bdd170 100644 --- a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "devDependencies": { - "prisma": "8.0.0-rc.7" + "@prisma/cli-engine": "0.2.0", + "@prisma/orm-postgres": "8.0.0-rc.4", + "prisma": "8.0.0-rc.7", + "typescript": "5.9.3" } } diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts b/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts new file mode 100644 index 0000000000..28b47fc41a --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@prisma/orm-postgres/config' + +export default defineConfig({ + contract: './next.prisma', +}) diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma deleted file mode 100644 index f8d6d7e53b..0000000000 --- a/packages/vscode/tests/fixtures/integration-workspace/root-a/schema.prisma +++ /dev/null @@ -1,8 +0,0 @@ -datasource db { - provider = "sqlite" - url = "file:./root-a.db" -} - -model RootARecord { - id Int @id -} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma deleted file mode 100644 index 63098ded90..0000000000 --- a/packages/vscode/tests/fixtures/integration-workspace/root-a/second.prisma +++ /dev/null @@ -1,3 +0,0 @@ -model RootASecondRecord { - id Int @id -} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-b/package.json b/packages/vscode/tests/fixtures/integration-workspace/root-b/package.json deleted file mode 100644 index aa71609e45..0000000000 --- a/packages/vscode/tests/fixtures/integration-workspace/root-b/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "prisma-vscode-integration-root-b", - "version": "1.0.0", - "private": true, - "devDependencies": { - "prisma": "8.0.0-rc.7" - } -} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma deleted file mode 100644 index 7292af1bef..0000000000 --- a/packages/vscode/tests/fixtures/integration-workspace/root-b/schema.prisma +++ /dev/null @@ -1,8 +0,0 @@ -datasource db { - provider = "sqlite" - url = "file:./root-b.db" -} - -model RootBRecord { - id Int @id -} diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma deleted file mode 100644 index c981121fa0..0000000000 --- a/packages/vscode/tests/fixtures/integration-workspace/root-missing/schema.prisma +++ /dev/null @@ -1,3 +0,0 @@ -model MissingCliRecord { - id Int @id -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1c23f5d1d..58c0725b69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,15 +244,18 @@ importers: packages/vscode/tests/fixtures/integration-workspace/root-a: devDependencies: + '@prisma/cli-engine': + specifier: 0.2.0 + version: 0.2.0(magicast@0.5.4) + '@prisma/orm-postgres': + specifier: 8.0.0-rc.4 + version: 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) prisma: specifier: 8.0.0-rc.7 - version: 8.0.0-rc.7(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) - - packages/vscode/tests/fixtures/integration-workspace/root-b: - devDependencies: - prisma: - specifier: 8.0.0-rc.7 - version: 8.0.0-rc.7(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + version: 8.0.0-rc.7(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) + typescript: + specifier: 5.9.3 + version: 5.9.3 packages: @@ -1732,6 +1735,14 @@ packages: '@prisma/management-api-sdk@1.67.0': resolution: {integrity: sha512-lgiCR2XD+xHXb5gIKcYmgjgEjIcM35TCBW6cAyWOLcKTlq88YfddqURaALC3TxDFo+Zp+3tXrf1Cr0aiXZgKPg==} + '@prisma/orm-family-sql@8.0.0-rc.4': + resolution: {integrity: sha512-C42YYbFtHB2lbSsBEHzap1txc+GundXAzweEQRR/qMLADhJj313MJ9utQauDWslaRwniexal5fZ5KNUtlpgJag==} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + '@prisma/orm-framework@8.0.0-rc.4': resolution: {integrity: sha512-vEMX1h5UF5zOyIT5TVKeWR9c8TcghJWggaFTVp3uXuHFyRUOANPomMMocXTFt2bhvdp/Ny7KxJ3KDJXyc1wCmw==} peerDependencies: @@ -1740,6 +1751,22 @@ packages: typescript: optional: true + '@prisma/orm-postgres@8.0.0-rc.4': + resolution: {integrity: sha512-1sxBwpMYnQFKei3uyM43DZJoQ+b0SwNz+8/sEXp0cccLlPTJU1ws+WYyqUNrvD2mO5tw0bsJge6xFrFX72QWyg==} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma/orm-target-postgres@8.0.0-rc.4': + resolution: {integrity: sha512-3JPTCdfrBOYp+vWrwtvkDioUi2yFpi5+mSP0wjo0bVN6WuK0/EA/YU19Zx5Hk/Y4T0fcP9kBCEVU0ZaHC71rmg==} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + '@prisma/orm-toolchain@8.0.0-rc.4': resolution: {integrity: sha512-YufxTbj0jB8f6iSCbo/KEgWwPP6Y1C2XFEg5N6YnVLHV7nP92NfR51qlUJrtvj153mltPZjKpGdAdtxvf0Besw==} peerDependencies: @@ -2170,6 +2197,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/pg@8.20.4': + resolution: {integrity: sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg==} + '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} @@ -2261,6 +2291,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vercel/detect-agent@1.2.5': resolution: {integrity: sha512-0krENrjuitlW8s6TJu0MlqCevyCU7K7JK63jZAf7xZ6n17tx+vUEwzHT3sTxawtwZxaW21hu+oFUpoOrm49FsQ==} @@ -3482,6 +3513,7 @@ packages: git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true github-from-package@0.0.0: @@ -3497,11 +3529,13 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.6: @@ -3510,12 +3544,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -4526,6 +4560,11 @@ packages: pg-connection-string@2.14.0: resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + pg-cursor@2.22.0: + resolution: {integrity: sha512-knzXLKqarTjOvb3qDSW0JiGsazmxwEKXrqHfWRte7XUsOYccQRafn3BLnQobWwInkzFJSyOej8y8cQRh2z3kGw==} + peerDependencies: + pg: ^8 + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -4542,6 +4581,15 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pg@8.23.0: resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} engines: {node: '>= 16.0.0'} @@ -4632,6 +4680,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -5199,6 +5248,9 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} + ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5278,6 +5330,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} @@ -5343,6 +5400,7 @@ packages: uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true valibot@1.2.0: @@ -5559,6 +5617,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -6264,14 +6323,14 @@ snapshots: '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) effect: 4.0.0-beta.103 - '@distilled.cloud/cloudflare-rolldown-plugin@0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': + '@distilled.cloud/cloudflare-rolldown-plugin@0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': dependencies: '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260704.1) magic-string: 0.30.21 unenv: 2.0.0-rc.24 optionalDependencies: rolldown: 1.1.5 - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - workerd @@ -6282,13 +6341,13 @@ snapshots: effect: 4.0.0-beta.103 workerd: 1.20260704.1 - '@distilled.cloud/cloudflare-vite-plugin@0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': + '@distilled.cloud/cloudflare-vite-plugin@0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': dependencies: '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) + '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) '@distilled.cloud/cloudflare-runtime': 0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103) effect: 4.0.0-beta.103 - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - rolldown - workerd @@ -6317,10 +6376,10 @@ snapshots: '@cloudflare/workers-types': 5.20260822.1 effect: 4.0.0-beta.103 - '@effect/vitest@4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))': + '@effect/vitest@4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@2.1.9(@types/node@14.18.63))': dependencies: effect: 4.0.0-beta.103 - vitest: 3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + vitest: 2.1.9(@types/node@14.18.63) '@electric-sql/pglite-socket@0.0.19(@electric-sql/pglite@0.3.14)': dependencies: @@ -6720,7 +6779,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -7028,11 +7087,11 @@ snapshots: transitivePeerDependencies: - magicast - '@prisma/composer-cli@0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3)': + '@prisma/composer-cli@0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3)': dependencies: '@prisma/cli-engine': 0.2.0(magicast@0.5.4) - '@prisma/composer': 0.11.0(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) - alchemy: 2.0.0-beta.67(@types/node@20.14.8)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + '@prisma/composer': 0.11.0(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) + alchemy: 2.0.0-beta.67(@types/node@14.18.63)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) c12: 3.3.4(magicast@0.5.4) effect: 4.0.0-beta.103 esbuild: 0.28.2 @@ -7065,11 +7124,11 @@ snapshots: - workerd - ws - '@prisma/composer@0.11.0(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3)': + '@prisma/composer@0.11.0(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3)': dependencies: '@prisma/management-api-sdk': 1.67.0 '@standard-schema/spec': 1.1.0 - alchemy: 2.0.0-beta.67(@types/node@20.14.8)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + alchemy: 2.0.0-beta.67(@types/node@14.18.63)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) arktype: 2.2.3 c12: 3.3.4(magicast@0.5.4) effect: 4.0.0-beta.103 @@ -7177,7 +7236,7 @@ snapshots: transitivePeerDependencies: - typescript - '@prisma/dev@0.20.0(typescript@5.7.3)': + '@prisma/dev@0.20.0(typescript@5.9.3)': dependencies: '@electric-sql/pglite': 0.3.15 '@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15) @@ -7194,7 +7253,7 @@ snapshots: proper-lockfile: 4.1.2 remeda: 2.33.4 std-env: 3.10.0 - valibot: 1.2.0(typescript@5.7.3) + valibot: 1.2.0(typescript@5.9.3) zeptomatch: 2.1.0 transitivePeerDependencies: - typescript @@ -7233,19 +7292,74 @@ snapshots: dependencies: openapi-fetch: 0.14.0 - '@prisma/orm-framework@8.0.0-rc.4(typescript@5.7.3)': + '@prisma/orm-family-sql@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) + '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + pathe: 2.0.3 + pluralize: 8.0.0 + ts-toolbelt: 9.6.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@prisma/cli-engine' + - magicast + - typanion + - vite + + '@prisma/orm-framework@8.0.0-rc.4(typescript@5.9.3)': dependencies: '@standard-schema/spec': 1.1.0 arktype: 2.2.3 pathe: 2.0.3 uniku: 0.5.0 optionalDependencies: - typescript: 5.7.3 + typescript: 5.9.3 + + '@prisma/orm-postgres@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@prisma/orm-family-sql': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) + '@prisma/orm-target-postgres': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@types/pg': 8.20.4 + pathe: 2.0.3 + pg: 8.22.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@prisma/cli-engine' + - magicast + - pg-native + - typanion + - vite + + '@prisma/orm-target-postgres@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@prisma/orm-family-sql': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) + '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@standard-schema/spec': 1.1.0 + '@types/pg': 8.20.4 + arktype: 2.2.3 + pathe: 2.0.3 + pg: 8.22.0 + pg-cursor: 2.22.0(pg@8.22.0) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@prisma/cli-engine' + - magicast + - pg-native + - typanion + - vite - '@prisma/orm-toolchain@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-toolchain@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@prisma/cli-engine': 0.2.0(magicast@0.5.4) - '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.7.3) + '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) '@vercel/detect-agent': 1.2.5 arktype: 2.2.3 c12: 3.3.4(magicast@0.5.4) @@ -7264,8 +7378,8 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 wrap-ansi: 10.0.1 optionalDependencies: - typescript: 5.7.3 - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + typescript: 5.9.3 + vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - magicast - typanion @@ -7457,7 +7571,7 @@ snapshots: chalk: 5.6.2 debug: 4.4.3(supports-color@8.1.1) pluralize: 8.0.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 table: 6.9.0 terminal-link: 4.0.0 transitivePeerDependencies: @@ -7654,6 +7768,12 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/pg@8.20.4': + dependencies: + '@types/node': 20.14.8 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + '@types/react@19.2.7': dependencies: csstype: 3.2.3 @@ -7859,14 +7979,6 @@ snapshots: optionalDependencies: vite: 7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0) - '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) - '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 @@ -8079,7 +8191,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@2.0.0-beta.67(@types/node@20.14.8)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3): + alchemy@2.0.0-beta.67(@types/node@14.18.63)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3): dependencies: '@alchemy.run/node-utils': 0.0.5 '@aws-sdk/credential-providers': 3.1116.0 @@ -8087,18 +8199,18 @@ snapshots: '@distilled.cloud/aws': 0.30.3(effect@4.0.0-beta.103) '@distilled.cloud/axiom': 0.30.3(effect@4.0.0-beta.103) '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) + '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) '@distilled.cloud/cloudflare-runtime': 0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare-vite-plugin': 0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) + '@distilled.cloud/cloudflare-vite-plugin': 0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) '@distilled.cloud/neon': 0.30.3(effect@4.0.0-beta.103) '@distilled.cloud/planetscale': 0.30.3(effect@4.0.0-beta.103) '@effect/sql-d1': 4.0.0-rc.111(effect@4.0.0-beta.103) - '@effect/vitest': 4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0)) + '@effect/vitest': 4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@2.1.9(@types/node@14.18.63)) '@libsql/client': 0.17.4 '@octokit/rest': 22.0.1 '@octokit/webhooks': 14.2.0 - '@prisma/dev': 0.20.0(typescript@5.7.3) + '@prisma/dev': 0.20.0(typescript@5.9.3) '@smithy/node-config-provider': 4.6.2 '@smithy/shared-ini-file-loader': 4.7.2 '@smithy/types': 4.17.2 @@ -8113,7 +8225,7 @@ snapshots: jszip: 3.10.1 libsodium-wrappers: 0.8.4 mongodb: 6.21.0(@aws-sdk/credential-providers@3.1116.0) - mysql2: 3.23.4(@types/node@20.14.8) + mysql2: 3.23.4(@types/node@14.18.63) pathe: 2.0.3 pg: 8.23.0 picomatch: 4.0.5 @@ -8122,7 +8234,7 @@ snapshots: undici: 7.16.0 yaml: 2.6.1 optionalDependencies: - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) + vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) ws: 8.21.3 transitivePeerDependencies: - '@mongodb-js/zstd' @@ -9513,7 +9625,7 @@ snapshots: is-fullwidth-code-point@5.1.0: dependencies: - get-east-asian-width: 1.4.0 + get-east-asian-width: 1.6.0 is-glob@4.0.3: dependencies: @@ -10008,9 +10120,9 @@ snapshots: mute-stream@0.0.8: {} - mysql2@3.23.4(@types/node@20.14.8): + mysql2@3.23.4(@types/node@14.18.63): dependencies: - '@types/node': 20.14.8 + '@types/node': 14.18.63 aws-ssl-profiles: 1.1.2 generate-function: 2.3.1 iconv-lite: 0.7.3 @@ -10301,8 +10413,16 @@ snapshots: pg-connection-string@2.14.0: {} + pg-cursor@2.22.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + pg-int8@1.0.1: {} + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + pg-pool@3.14.0(pg@8.23.0): dependencies: pg: 8.23.0 @@ -10317,6 +10437,16 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + pg@8.23.0: dependencies: pg-connection-string: 2.14.0 @@ -10417,14 +10547,14 @@ snapshots: dependencies: parse-ms: 4.0.0 - prisma@8.0.0-rc.7(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3): + prisma@8.0.0-rc.7(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3): dependencies: '@prisma/cli-engine': 0.2.0(magicast@0.5.4) - '@prisma/composer-cli': 0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@20.14.8)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)(ws@8.21.3) + '@prisma/composer-cli': 0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) '@prisma/compute-sdk': 0.39.0(@prisma/management-api-sdk@1.55.0)(rollup@4.53.3) '@prisma/credentials-store': 7.9.1 '@prisma/management-api-sdk': 1.55.0 - '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.7.3)(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) '@vercel/detect-agent': 1.2.5 better-result: 2.10.0 dotenv: 17.4.2 @@ -10849,7 +10979,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string-width@6.1.0: dependencies: @@ -10866,7 +10996,7 @@ snapshots: string-width@8.2.2: dependencies: get-east-asian-width: 1.6.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string_decoder@1.1.1: dependencies: @@ -11062,6 +11192,8 @@ snapshots: ts-dedent@2.2.0: {} + ts-toolbelt@9.6.0: {} + tslib@2.8.1: {} tunnel-agent@0.6.0: @@ -11124,6 +11256,8 @@ snapshots: typescript@5.7.3: {} + typescript@5.9.3: {} + uc.micro@1.0.6: {} uc.micro@2.1.0: {} @@ -11174,6 +11308,10 @@ snapshots: optionalDependencies: typescript: 5.7.3 + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -11238,27 +11376,6 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite@5.4.21(@types/node@14.18.63): dependencies: esbuild: 0.21.5 @@ -11277,7 +11394,7 @@ snapshots: '@types/node': 20.14.8 fsevents: 2.3.3 - vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): + vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -11286,12 +11403,12 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 18.19.76 + '@types/node': 14.18.63 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0): + vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -11300,7 +11417,7 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 20.14.8 + '@types/node': 18.19.76 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 @@ -11416,47 +11533,6 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) - expect-type: 1.2.2 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.2.6(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@20.14.8)(jiti@2.7.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.14.8 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vscode-jsonrpc@6.0.0: {} vscode-jsonrpc@8.1.0: {} @@ -11573,7 +11649,7 @@ snapshots: dependencies: ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 wrap-ansi@9.0.2: dependencies: From a81ac7e48f6969a132b6390398a1373589db0ba7 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 08:02:48 +0000 Subject: [PATCH 27/43] docs(vscode): document completion routing oracle --- docs/language-server.md | 3 ++- docs/testing.md | 11 +++-------- .../vscode/src/__test__/language-server/README.md | 13 +++++-------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/docs/language-server.md b/docs/language-server.md index 61df1fb17b..5870f48c8b 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -91,6 +91,7 @@ The registry exposes a narrow lifecycle API used by routing and later workspace - `openDocument(rootUri, document)` — verifies the document is still open before inserting it into the local middleware ledger. - `closeDocument(rootUri, document)` — idempotently balances an actually synchronized local document. - `clearDiagnostics(rootUri, uri)` — clears only the requested URI. -- `getTestState()` — reports successful root starts without exposing process handles; it is reachable through a command only in debug/test sessions. A started local client currently remains alive after its final marked document closes. Workspace-wide restart and rediscovery, runtime-failure recovery, workspace-folder removal, comprehensive deactivation, and live Prisma 6 pin transitions are separate lifecycle responsibilities that should build on this API rather than bypass the coordinator or middleware ledgers. + +Routing is covered end to end through public completion behavior. In one workspace root, the Electron integration test opens an unmarked document served by the bundled Prisma 7 language server beside a marked document served by the real workspace-local Prisma 8 CLI. The bundled document offers `datasource`, `generator`, and `model` but not `namespace`; the marked document offers the Prisma 8 `namespace` keyword but not `datasource`. The test does not expose or inspect coordinator ownership, routing events, or client startup counts. diff --git a/docs/testing.md b/docs/testing.md index 19a43a3fa8..76986320b5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -77,10 +77,7 @@ located in `packages/vscode/fixtures`. ## VS Code Electron integration tests -The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its roots include: - -- Two pnpm importers with the lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`. -- An additional marked-document fixture without that exact entrypoint, used to verify silent no-fallback behavior. +The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its single workspace root is a pnpm importer with the lockfile-resolved `prisma@8.0.0-rc.7` CLI at `node_modules/prisma/dist/prisma.js`. The fixture also includes the matching Prisma 8 engine and Postgres ORM packages plus a valid `prisma.config.ts` whose contract is only `next.prisma`. Run the focused minimum-runtime workspace suite with: @@ -88,8 +85,6 @@ Run the focused minimum-runtime workspace suite with: pnpm --filter prisma test:integration:workspace ``` -This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the real Prisma CLI process; no mock language-server executable is part of the fixture. It covers lazy activation, successful real-client initialization per root, root reuse and independence, exclusive bundled/local ownership, complete-text unsaved directive transfers, bundled diagnostic production and transfer-time clearing, and missing-entrypoint behavior. The current Prisma Next CLI does not publish schema diagnostics. - -The runner's installed `@vscode/test-electron` version always adds `--disable-workspace-trust`, so the Electron workspace is deterministically trusted. It cannot represent Restricted Mode without replacing or bypassing the runner's launch contract. Trust rejection is therefore covered at the production classifier and registry boundaries by focused unit tests; a manual Restricted Mode check remains necessary when validating trust behavior end to end. +This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the bundled language server and the real workspace-local Prisma CLI process side by side; no mock language-server executable is part of the fixture. -Routing observations are available only when `isDebugOrTestSession()` is true. The test command reports ownership/routing events and successful start counts. Complete document text and version are captured only by the optional test observer; production activation installs neither the collector nor the command, and no process handles are exposed. +The test opens an empty, unmarked `bundled.prisma` and a marked `next.prisma` in separate editor columns, then polls only the public `vscode.executeCompletionItemProvider` command with a fixed timeout. At `(0, 0)`, the bundled server must offer `datasource`, `generator`, and `model`, classify `datasource` as `CompletionItemKind.Class`, and omit `namespace`. At `(1, 0)`, the local Prisma 8 server must offer `namespace` as `CompletionItemKind.Keyword` with detail `PSL declaration keyword`, and omit `datasource`. These assertions verify observable routing behavior without extension-private commands, owner state, events, or process start counts. diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index ec6c45446c..08b4ed69d4 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -1,18 +1,15 @@ # Integration tests for the Language Server -Only one test per feature is done here. -The goal is to check that the integration is working between the VS Code extension and the Language Server. +Only one test per feature is done here. The goal is to check that the integration is working between the VS Code extension and the Language Server. -The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Two roots are pnpm workspace importers with the same lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`; a third root intentionally has no local CLI entrypoint. +The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Its single root is a pnpm workspace importer containing the lockfile-resolved real `prisma@8.0.0-rc.7` CLI, matching Prisma 8 engine and Postgres ORM packages, and a valid `prisma.config.ts` whose contract is only the marked `next.prisma` fixture. -Run the full minimum-and-latest integration suite with `pnpm test:integration`. Run the focused real-CLI routing suite on the minimum supported VS Code runtime with: +Run the full minimum-and-latest integration suite with `pnpm test:integration`. Run the focused side-by-side completion suite on the minimum supported VS Code runtime with: ```bash pnpm --filter prisma test:integration:workspace ``` -The focused suite verifies that activation and unmarked documents start no local process, each eligible marked root completes exactly one real client initialization handshake, additional documents reuse their root client, roots remain independent, and the missing-entrypoint root has no fallback process. It also observes exclusive bundled/local synchronization, both unsaved directive transfer directions, complete current text/version, and URI-scoped diagnostics clearing. The current Prisma Next CLI does not publish schema diagnostics, so diagnostic production is asserted only while the document is bundled; routing-state observations prove that those diagnostics are cleared during ownership transfers. +The focused suite opens an empty, unmarked `bundled.prisma` and a marked `next.prisma` in separate editor columns. It polls only the public `vscode.executeCompletionItemProvider` command with a fixed timeout. The bundled Prisma 7 language server must offer `datasource`, `generator`, and `model`, classify `datasource` as `CompletionItemKind.Class`, and omit `namespace`. The real workspace-local Prisma 8 language server must offer `namespace` as `CompletionItemKind.Keyword` with detail `PSL declaration keyword`, and omit `datasource`. -Test-only routing state is exposed through `prisma.test.languageServerRoutingState`. The command is registered only when `isDebugOrTestSession()` is true; production sessions do not install the observer or retain observed document contents. The state contains no process handles. - -`@vscode/test-electron` adds `--disable-workspace-trust` unconditionally, so this harness always runs trusted. Restricted Mode execution remains a manual check; focused classifier and registry tests cover the untrusted production boundaries. +No mock language server or extension-private routing state is used. The test asserts observable editor behavior rather than owners, routing events, document synchronization bookkeeping, or process start counts. From a0a2fb84b43bbcd7860f80afc911ccf26819e006 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 08:18:37 +0000 Subject: [PATCH 28/43] test(vscode): load Prisma 8 ORM config --- .../fixtures/integration-workspace/root-a/prisma.config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts b/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts index 28b47fc41a..0e689f5744 100644 --- a/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/prisma.config.ts @@ -1,5 +1,6 @@ -import { defineConfig } from '@prisma/orm-postgres/config' +import { definePrismaConfig } from '@prisma/cli-engine' +import { defineConfig as ormConfig } from '@prisma/orm-postgres/config' -export default defineConfig({ - contract: './next.prisma', +export default definePrismaConfig({ + orm: ormConfig({ contract: './next.prisma' }), }) From 4af47be454dbdd156dfd4e9216a02237c6d90221 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 08:26:42 +0000 Subject: [PATCH 29/43] chore(vscode): type-check integration fixture lint --- .eslintrc.js | 6 +++++- .../integration-workspace/root-a/tsconfig.eslint.json | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 packages/vscode/tests/fixtures/integration-workspace/root-a/tsconfig.eslint.json diff --git a/.eslintrc.js b/.eslintrc.js index f4ea911a31..b098b9c734 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,11 @@ module.exports = { { files: ['*.ts'], parserOptions: { - project: ['./tsconfig.json', './packages/*/tsconfig.json'], + project: [ + './tsconfig.json', + './packages/*/tsconfig.json', + './packages/vscode/tests/fixtures/integration-workspace/root-a/tsconfig.eslint.json', + ], }, extends: [ 'plugin:@typescript-eslint/recommended', diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/tsconfig.eslint.json b/packages/vscode/tests/fixtures/integration-workspace/root-a/tsconfig.eslint.json new file mode 100644 index 0000000000..d19f21ecb9 --- /dev/null +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/tsconfig.eslint.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + }, + "include": ["prisma.config.ts"] +} From f3b91171a595671caf15fd7bb1b57665b6764e1c Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 08:41:20 +0000 Subject: [PATCH 30/43] fix(vscode): register local Prisma providers cross-platform --- .../prisma-language-server/localPrismaNextClientRegistry.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index 6f6fba57fb..c91af10a0f 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -219,10 +219,8 @@ export function createLocalPrismaNextClientOptions( workspaceFolder: WorkspaceFolder, middleware: LocalClientMiddleware, ): LanguageClientOptions { - const rootPath = workspaceFolder.uri.fsPath.split('\\').join('/') - const normalizedRoot = rootPath.endsWith('/') ? rootPath.slice(0, -1) : rootPath return { - documentSelector: [{ language: 'prisma', scheme: 'file', pattern: `${normalizedRoot}/**/*` }], + documentSelector: [{ language: 'prisma', scheme: 'file' }], workspaceFolder, middleware, } From d2544a0cb12025ac88815d9bf9d22dc5d8709ad3 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 08:51:23 +0000 Subject: [PATCH 31/43] fix(vscode): gate local semantic providers by owner --- .../prisma-language-server/localClientMiddleware.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts index aa5af3b736..cb14143023 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts @@ -116,6 +116,14 @@ export function createLocalClientMiddleware(options: LocalClientMiddlewareOption provideDocumentSymbols: (document, token, next) => (isOwnedDocument(document) ? next(document, token) : undefined), provideDocumentFormattingEdits: (document, formattingOptions, token, next) => isOwnedDocument(document) ? next(document, formattingOptions, token) : undefined, + provideFoldingRanges: (document, context, token, next) => + isOwnedDocument(document) ? next(document, context, token) : undefined, + provideDocumentSemanticTokens: (document, token, next) => + isOwnedDocument(document) ? next(document, token) : undefined, + provideDocumentSemanticTokensEdits: (document, previousResultId, token, next) => + isOwnedDocument(document) ? next(document, previousResultId, token) : undefined, + provideDocumentRangeSemanticTokens: (document, range, token, next) => + isOwnedDocument(document) ? next(document, range, token) : undefined, provideRenameEdits: (document, position, newName, token, next) => isOwnedDocument(document) ? next(document, position, newName, token) : undefined, provideCodeActions: (document, range, context, token, next) => From 2603c152556eaf1afc2c1a220b3c406537b81da2 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 09:12:56 +0000 Subject: [PATCH 32/43] fix(vscode): fork local Prisma language server --- .../localPrismaNextClientRegistry.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index c91af10a0f..3d8a4348f9 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { stat } from 'node:fs/promises' -import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process' +import { fork, type ChildProcess, type ForkOptions } from 'node:child_process' import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' @@ -14,16 +14,11 @@ export interface LocalPrismaNextClientRegistryWorkspace { getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined } -export type SpawnLocalPrismaNextProcess = ( - executable: string, - args: string[], - options: SpawnOptionsWithoutStdio, -) => ChildProcessWithoutNullStreams +export type ForkLocalPrismaNextProcess = (modulePath: string, args: string[], options: ForkOptions) => ChildProcess export interface LocalPrismaNextLauncherOptions { - readonly executable?: string readonly environment?: NodeJS.ProcessEnv - readonly spawnProcess?: SpawnLocalPrismaNextProcess + readonly forkProcess?: ForkLocalPrismaNextProcess readonly handleProcessError?: (error: Error) => void } @@ -143,31 +138,29 @@ export function createLocalPrismaNextServerOptions( ): ServerOptions { return () => launchLocalPrismaNextServer({ - executable: launcher.executable ?? process.execPath, entrypoint, cwd: workspaceFolder.uri.fsPath, environment: createExtensionHostNodeEnvironment(launcher.environment ?? process.env), - spawnProcess: launcher.spawnProcess ?? spawn, + forkProcess: launcher.forkProcess ?? fork, handleProcessError: launcher.handleProcessError, }) } export interface LaunchLocalPrismaNextServerOptions { - readonly executable: string readonly entrypoint: string readonly cwd: string readonly environment: NodeJS.ProcessEnv - readonly spawnProcess: SpawnLocalPrismaNextProcess + readonly forkProcess: ForkLocalPrismaNextProcess readonly handleProcessError?: (error: Error) => void } export function launchLocalPrismaNextServer(options: LaunchLocalPrismaNextServerOptions): Promise { return new Promise((resolve, reject) => { - const child = options.spawnProcess(options.executable, [options.entrypoint, 'lsp'], { + const child = options.forkProcess(options.entrypoint, ['lsp'], { cwd: options.cwd, env: options.environment, - shell: false, - stdio: ['pipe', 'pipe', 'pipe'], + execArgv: [], + silent: true, }) const cleanupStartupListeners = (): void => { @@ -209,10 +202,10 @@ export function createExtensionHostNodeEnvironment(environment: NodeJS.ProcessEn } } -function destroyProcessStreams(child: ChildProcessWithoutNullStreams): void { - child.stdin.destroy() - child.stdout.destroy() - child.stderr.destroy() +function destroyProcessStreams(child: ChildProcess): void { + child.stdin?.destroy() + child.stdout?.destroy() + child.stderr?.destroy() } export function createLocalPrismaNextClientOptions( From 1a9070a1beb40011f1187bbbafeabec18b466182 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 12:31:02 +0000 Subject: [PATCH 33/43] fix(vscode): use fixed Prisma 8 ORM stack --- docs/testing.md | 2 +- .../src/__test__/language-server/README.md | 2 +- .../localClientMiddleware.ts | 8 -- .../localPrismaNextClientRegistry.ts | 35 +++-- .../integration-workspace/root-a/package.json | 4 +- pnpm-lock.yaml | 127 ++++++++++++++---- 6 files changed, 130 insertions(+), 48 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 76986320b5..e2349cd624 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -77,7 +77,7 @@ located in `packages/vscode/fixtures`. ## VS Code Electron integration tests -The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its single workspace root is a pnpm importer with the lockfile-resolved `prisma@8.0.0-rc.7` CLI at `node_modules/prisma/dist/prisma.js`. The fixture also includes the matching Prisma 8 engine and Postgres ORM packages plus a valid `prisma.config.ts` whose contract is only `next.prisma`. +The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its single workspace root is a pnpm importer with the lockfile-resolved `prisma@8.0.0-rc.7` CLI at `node_modules/prisma/dist/prisma.js`. The fixture uses `@prisma/cli-engine@0.2.3` and `@prisma/orm-postgres@8.0.0-rc.7-dev.1` (which resolves `@prisma/orm-toolchain@8.0.0-rc.7-dev.1`) plus a valid `prisma.config.ts` whose contract is only `next.prisma`. Run the focused minimum-runtime workspace suite with: diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index 08b4ed69d4..ec972940ae 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -2,7 +2,7 @@ Only one test per feature is done here. The goal is to check that the integration is working between the VS Code extension and the Language Server. -The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Its single root is a pnpm workspace importer containing the lockfile-resolved real `prisma@8.0.0-rc.7` CLI, matching Prisma 8 engine and Postgres ORM packages, and a valid `prisma.config.ts` whose contract is only the marked `next.prisma` fixture. +The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Its single root is a pnpm workspace importer containing the lockfile-resolved real `prisma@8.0.0-rc.7` CLI with `@prisma/cli-engine@0.2.3` and `@prisma/orm-postgres@8.0.0-rc.7-dev.1` (resolving `@prisma/orm-toolchain@8.0.0-rc.7-dev.1`), plus a valid `prisma.config.ts` whose contract is only the marked `next.prisma` fixture. Run the full minimum-and-latest integration suite with `pnpm test:integration`. Run the focused side-by-side completion suite on the minimum supported VS Code runtime with: diff --git a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts index cb14143023..aa5af3b736 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts @@ -116,14 +116,6 @@ export function createLocalClientMiddleware(options: LocalClientMiddlewareOption provideDocumentSymbols: (document, token, next) => (isOwnedDocument(document) ? next(document, token) : undefined), provideDocumentFormattingEdits: (document, formattingOptions, token, next) => isOwnedDocument(document) ? next(document, formattingOptions, token) : undefined, - provideFoldingRanges: (document, context, token, next) => - isOwnedDocument(document) ? next(document, context, token) : undefined, - provideDocumentSemanticTokens: (document, token, next) => - isOwnedDocument(document) ? next(document, token) : undefined, - provideDocumentSemanticTokensEdits: (document, previousResultId, token, next) => - isOwnedDocument(document) ? next(document, previousResultId, token) : undefined, - provideDocumentRangeSemanticTokens: (document, range, token, next) => - isOwnedDocument(document) ? next(document, range, token) : undefined, provideRenameEdits: (document, position, newName, token, next) => isOwnedDocument(document) ? next(document, position, newName, token) : undefined, provideCodeActions: (document, range, context, token, next) => diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts index 3d8a4348f9..6f6fba57fb 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { stat } from 'node:fs/promises' -import { fork, type ChildProcess, type ForkOptions } from 'node:child_process' +import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process' import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' @@ -14,11 +14,16 @@ export interface LocalPrismaNextClientRegistryWorkspace { getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined } -export type ForkLocalPrismaNextProcess = (modulePath: string, args: string[], options: ForkOptions) => ChildProcess +export type SpawnLocalPrismaNextProcess = ( + executable: string, + args: string[], + options: SpawnOptionsWithoutStdio, +) => ChildProcessWithoutNullStreams export interface LocalPrismaNextLauncherOptions { + readonly executable?: string readonly environment?: NodeJS.ProcessEnv - readonly forkProcess?: ForkLocalPrismaNextProcess + readonly spawnProcess?: SpawnLocalPrismaNextProcess readonly handleProcessError?: (error: Error) => void } @@ -138,29 +143,31 @@ export function createLocalPrismaNextServerOptions( ): ServerOptions { return () => launchLocalPrismaNextServer({ + executable: launcher.executable ?? process.execPath, entrypoint, cwd: workspaceFolder.uri.fsPath, environment: createExtensionHostNodeEnvironment(launcher.environment ?? process.env), - forkProcess: launcher.forkProcess ?? fork, + spawnProcess: launcher.spawnProcess ?? spawn, handleProcessError: launcher.handleProcessError, }) } export interface LaunchLocalPrismaNextServerOptions { + readonly executable: string readonly entrypoint: string readonly cwd: string readonly environment: NodeJS.ProcessEnv - readonly forkProcess: ForkLocalPrismaNextProcess + readonly spawnProcess: SpawnLocalPrismaNextProcess readonly handleProcessError?: (error: Error) => void } export function launchLocalPrismaNextServer(options: LaunchLocalPrismaNextServerOptions): Promise { return new Promise((resolve, reject) => { - const child = options.forkProcess(options.entrypoint, ['lsp'], { + const child = options.spawnProcess(options.executable, [options.entrypoint, 'lsp'], { cwd: options.cwd, env: options.environment, - execArgv: [], - silent: true, + shell: false, + stdio: ['pipe', 'pipe', 'pipe'], }) const cleanupStartupListeners = (): void => { @@ -202,18 +209,20 @@ export function createExtensionHostNodeEnvironment(environment: NodeJS.ProcessEn } } -function destroyProcessStreams(child: ChildProcess): void { - child.stdin?.destroy() - child.stdout?.destroy() - child.stderr?.destroy() +function destroyProcessStreams(child: ChildProcessWithoutNullStreams): void { + child.stdin.destroy() + child.stdout.destroy() + child.stderr.destroy() } export function createLocalPrismaNextClientOptions( workspaceFolder: WorkspaceFolder, middleware: LocalClientMiddleware, ): LanguageClientOptions { + const rootPath = workspaceFolder.uri.fsPath.split('\\').join('/') + const normalizedRoot = rootPath.endsWith('/') ? rootPath.slice(0, -1) : rootPath return { - documentSelector: [{ language: 'prisma', scheme: 'file' }], + documentSelector: [{ language: 'prisma', scheme: 'file', pattern: `${normalizedRoot}/**/*` }], workspaceFolder, middleware, } diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json index fd72bdd170..3b680f7c14 100644 --- a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "private": true, "devDependencies": { - "@prisma/cli-engine": "0.2.0", - "@prisma/orm-postgres": "8.0.0-rc.4", + "@prisma/cli-engine": "0.2.3", + "@prisma/orm-postgres": "8.0.0-rc.7-dev.1", "prisma": "8.0.0-rc.7", "typescript": "5.9.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58c0725b69..b67bd16eb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -245,11 +245,11 @@ importers: packages/vscode/tests/fixtures/integration-workspace/root-a: devDependencies: '@prisma/cli-engine': - specifier: 0.2.0 - version: 0.2.0(magicast@0.5.4) + specifier: 0.2.3 + version: 0.2.3(magicast@0.5.4) '@prisma/orm-postgres': - specifier: 8.0.0-rc.4 - version: 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + specifier: 8.0.0-rc.7-dev.1 + version: 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) prisma: specifier: 8.0.0-rc.7 version: 8.0.0-rc.7(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) @@ -1674,6 +1674,10 @@ packages: resolution: {integrity: sha512-nd0sP3l7W79ASoONP9DV8LKVcADmtPm0KMwKJ9rVJL3BbSJdN6HLO2pHFugI4Tp1NdiwcwlJuK5rCKxJPu7kFw==} engines: {node: '>=22.12.0'} + '@prisma/cli-engine@0.2.3': + resolution: {integrity: sha512-+OySr3NnF1BT6Gj34jKBBWwdQyzPL0YQVMygm0nn4qDRmB5OEYdlz4tS0Nw6HGhnTA82czEl05wTbX821VbBOA==} + engines: {node: '>=22.12.0'} + '@prisma/composer-cli@0.11.0': resolution: {integrity: sha512-qhibvb5ARF6JmvsUEpr3A0J2Kjx4whPRZP3LCOULk9+1/Cp2IWyHsLhDq+I6n0nmwrdvvySAWwW71ugSey24kA==} engines: {node: '>=22.18.0'} @@ -1735,8 +1739,8 @@ packages: '@prisma/management-api-sdk@1.67.0': resolution: {integrity: sha512-lgiCR2XD+xHXb5gIKcYmgjgEjIcM35TCBW6cAyWOLcKTlq88YfddqURaALC3TxDFo+Zp+3tXrf1Cr0aiXZgKPg==} - '@prisma/orm-family-sql@8.0.0-rc.4': - resolution: {integrity: sha512-C42YYbFtHB2lbSsBEHzap1txc+GundXAzweEQRR/qMLADhJj313MJ9utQauDWslaRwniexal5fZ5KNUtlpgJag==} + '@prisma/orm-family-sql@8.0.0-rc.7-dev.1': + resolution: {integrity: sha512-6VrG4E8l97A0YoFaNPRBadrmVVUK4j6q9zFAq4BvJGPDA2jq8C4qrml6iETJDAHuHSLY5ouzB4kcjYsdoAnBXQ==} peerDependencies: typescript: '>=5.9' peerDependenciesMeta: @@ -1751,16 +1755,24 @@ packages: typescript: optional: true - '@prisma/orm-postgres@8.0.0-rc.4': - resolution: {integrity: sha512-1sxBwpMYnQFKei3uyM43DZJoQ+b0SwNz+8/sEXp0cccLlPTJU1ws+WYyqUNrvD2mO5tw0bsJge6xFrFX72QWyg==} + '@prisma/orm-framework@8.0.0-rc.7-dev.1': + resolution: {integrity: sha512-U3GaRDrof0ulBIPGAvcN5ECCNqaWDXE3ivysXLKw3LjkaSUFgp48LiMFKvKlVRxjpni+NXn3mBDmokvHfj/cNA==} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma/orm-postgres@8.0.0-rc.7-dev.1': + resolution: {integrity: sha512-81hUHphlYE3p7LdxVOqr4w7A6GxlfY/cMIEVlFU0jiRsHVhFR+VOM9SEOfLrgy0LZHSzfd9j3nRhb51gfG35vw==} peerDependencies: typescript: '>=5.9' peerDependenciesMeta: typescript: optional: true - '@prisma/orm-target-postgres@8.0.0-rc.4': - resolution: {integrity: sha512-3JPTCdfrBOYp+vWrwtvkDioUi2yFpi5+mSP0wjo0bVN6WuK0/EA/YU19Zx5Hk/Y4T0fcP9kBCEVU0ZaHC71rmg==} + '@prisma/orm-target-postgres@8.0.0-rc.7-dev.1': + resolution: {integrity: sha512-I6rGUzQupGqb3M0gcwtOQFK58jGWwVM72Ri4RHlqDCC7a4ifmT7Ab+2gQpUkXBBrmzW8ubAS9H4WAlQESHmjJA==} peerDependencies: typescript: '>=5.9' peerDependenciesMeta: @@ -1779,6 +1791,18 @@ packages: vite: optional: true + '@prisma/orm-toolchain@8.0.0-rc.7-dev.1': + resolution: {integrity: sha512-TswFjFGg5afnO/2PvcL6a0lBoR/VW9jr9JbPydclm5Dmna5Nmd+nKb+kj2NOY5YsT+lVy1Gt4qz/KrdVBNdEbg==} + peerDependencies: + '@prisma/cli-engine': 0.2.3 + typescript: '>=5.9' + vite: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + vite: + optional: true + '@prisma/ppg@0.5.2': resolution: {integrity: sha512-WT5Kxj1gRLjcrnhMJfdTMsxs8RBhOHm4ZooCuaK0qhKP1ta1ukV3Ab11jEOSI2XeMWCRN6fvwO8+wqWzGmTXuQ==} @@ -7087,6 +7111,18 @@ snapshots: transitivePeerDependencies: - magicast + '@prisma/cli-engine@0.2.3(magicast@0.5.4)': + dependencies: + '@clack/prompts': 1.5.0 + '@prisma/management-api-sdk': 1.55.0 + '@stricli/core': 1.3.0 + c12: 3.3.4(magicast@0.5.4) + colorette: 2.0.20 + package-manager-detector: 1.8.0 + string-width: 8.2.2 + transitivePeerDependencies: + - magicast + '@prisma/composer-cli@0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3)': dependencies: '@prisma/cli-engine': 0.2.0(magicast@0.5.4) @@ -7292,10 +7328,10 @@ snapshots: dependencies: openapi-fetch: 0.14.0 - '@prisma/orm-family-sql@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-family-sql@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) - '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) '@standard-schema/spec': 1.1.0 arktype: 2.2.3 pathe: 2.0.3 @@ -7318,12 +7354,21 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@prisma/orm-postgres@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-framework@8.0.0-rc.7-dev.1(typescript@5.9.3)': dependencies: - '@prisma/orm-family-sql': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) - '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) - '@prisma/orm-target-postgres': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) - '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + pathe: 2.0.3 + uniku: 0.5.0 + optionalDependencies: + typescript: 5.9.3 + + '@prisma/orm-postgres@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@prisma/orm-family-sql': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) + '@prisma/orm-target-postgres': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) '@types/pg': 8.20.4 pathe: 2.0.3 pg: 8.22.0 @@ -7336,11 +7381,11 @@ snapshots: - typanion - vite - '@prisma/orm-target-postgres@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-target-postgres@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@prisma/orm-family-sql': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) - '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) - '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-family-sql': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) '@standard-schema/spec': 1.1.0 '@types/pg': 8.20.4 arktype: 2.2.3 @@ -7384,6 +7429,34 @@ snapshots: - magicast - typanion + '@prisma/orm-toolchain@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@prisma/cli-engine': 0.2.3(magicast@0.5.4) + '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) + '@vercel/detect-agent': 1.2.5 + arktype: 2.2.3 + c12: 3.3.4(magicast@0.5.4) + ci-info: 4.4.0 + clipanion: 4.0.0-rc.4(typanion@3.14.0) + closest-match: 1.3.3 + colorette: 2.0.20 + esbuild: 0.28.2 + jsonc-parser: 3.3.1 + package-manager-detector: 1.8.0 + pathe: 2.0.3 + prettier: 3.9.6 + string-width: 8.2.2 + strip-ansi: 7.2.0 + vscode-languageserver: 10.1.0 + vscode-languageserver-textdocument: 1.0.12 + wrap-ansi: 10.0.1 + optionalDependencies: + typescript: 5.9.3 + vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - magicast + - typanion + '@prisma/ppg@0.5.2': {} '@prisma/prisma-schema-wasm@6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773': {} @@ -7971,6 +8044,14 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@14.18.63) + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.14.8))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.14.8) + '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 @@ -11460,7 +11541,7 @@ snapshots: vitest@2.1.9(@types/node@20.14.8): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@14.18.63)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.14.8)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 From d0285aa8e6b0dd91f34af94dc9a2551be29c609e Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 12:58:07 +0000 Subject: [PATCH 34/43] test(vscode): use coherent Prisma 8 LSP build --- docs/testing.md | 2 +- .../src/__test__/language-server/README.md | 2 +- .../integration-workspace/root-a/package.json | 2 +- pnpm-lock.yaml | 1021 +++++++++++------ 4 files changed, 645 insertions(+), 382 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index e2349cd624..ae057b5b42 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -77,7 +77,7 @@ located in `packages/vscode/fixtures`. ## VS Code Electron integration tests -The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its single workspace root is a pnpm importer with the lockfile-resolved `prisma@8.0.0-rc.7` CLI at `node_modules/prisma/dist/prisma.js`. The fixture uses `@prisma/cli-engine@0.2.3` and `@prisma/orm-postgres@8.0.0-rc.7-dev.1` (which resolves `@prisma/orm-toolchain@8.0.0-rc.7-dev.1`) plus a valid `prisma.config.ts` whose contract is only `next.prisma`. +The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its single workspace root is a pnpm importer with the lockfile-resolved `prisma@8.0.0-rc.10-dev.82` CLI at `node_modules/prisma/dist/prisma.js`. The fixture uses `@prisma/cli-engine@0.2.3` and `@prisma/orm-postgres@8.0.0-rc.7-dev.1` (which resolves `@prisma/orm-toolchain@8.0.0-rc.7-dev.1`) plus a valid `prisma.config.ts` whose contract is only `next.prisma`. Run the focused minimum-runtime workspace suite with: diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index ec972940ae..078e6a71f8 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -2,7 +2,7 @@ Only one test per feature is done here. The goal is to check that the integration is working between the VS Code extension and the Language Server. -The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Its single root is a pnpm workspace importer containing the lockfile-resolved real `prisma@8.0.0-rc.7` CLI with `@prisma/cli-engine@0.2.3` and `@prisma/orm-postgres@8.0.0-rc.7-dev.1` (resolving `@prisma/orm-toolchain@8.0.0-rc.7-dev.1`), plus a valid `prisma.config.ts` whose contract is only the marked `next.prisma` fixture. +The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Its single root is a pnpm workspace importer containing the lockfile-resolved real `prisma@8.0.0-rc.10-dev.82` CLI with `@prisma/cli-engine@0.2.3` and `@prisma/orm-postgres@8.0.0-rc.7-dev.1` (resolving `@prisma/orm-toolchain@8.0.0-rc.7-dev.1`), plus a valid `prisma.config.ts` whose contract is only the marked `next.prisma` fixture. Run the full minimum-and-latest integration suite with `pnpm test:integration`. Run the focused side-by-side completion suite on the minimum supported VS Code runtime with: diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json index 3b680f7c14..0b22b7b8e7 100644 --- a/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json +++ b/packages/vscode/tests/fixtures/integration-workspace/root-a/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@prisma/cli-engine": "0.2.3", "@prisma/orm-postgres": "8.0.0-rc.7-dev.1", - "prisma": "8.0.0-rc.7", + "prisma": "8.0.0-rc.10-dev.82", "typescript": "5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b67bd16eb8..8eb6cd6252 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,10 +249,10 @@ importers: version: 0.2.3(magicast@0.5.4) '@prisma/orm-postgres': specifier: 8.0.0-rc.7-dev.1 - version: 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + version: 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) prisma: - specifier: 8.0.0-rc.7 - version: 8.0.0-rc.7(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) + specifier: 8.0.0-rc.10-dev.82 + version: 8.0.0-rc.10-dev.82(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(magicast@0.5.4)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -278,8 +278,32 @@ packages: resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} - '@alchemy.run/node-utils@0.0.5': - resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==} + '@alchemy.run/cloudflare-runtime@2.0.0-beta.74': + resolution: {integrity: sha512-P4eICKlw1TgnY6QjM3mztIuZcKXPdT4MmbatxWvzSp7ecueWcts9vnXn21dC0KR/vtRBRdopyuEbhmQ1CB0TfQ==} + peerDependencies: + '@distilled.cloud/cloudflare': 1.0.0-rc.6 + '@effect/platform-bun': '>=4.0.0-rc.110 || >=4.0.0' + '@effect/platform-node': '>=4.0.0-rc.110 || >=4.0.0' + effect: '>=4.0.0-rc.110 || >=4.0.0' + rolldown: 1.1.5 + vite: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@effect/platform-bun': + optional: true + '@effect/platform-node': + optional: true + rolldown: + optional: true + vite: + optional: true + + '@alchemy.run/floci@2.0.0-beta.74': + resolution: {integrity: sha512-JxQ1d1N8TzRXliJxI9HEM1DVIIONPB/NpPYx0S3EjGC6m0qM5Q5NzLJBXozxUL85mDTWba4cAodrpMF9oDIdsg==} + peerDependencies: + effect: '>=4.0.0-rc.110 || >=4.0.0' + + '@alchemy.run/node-utils@2.0.0-beta.74': + resolution: {integrity: sha512-UH7mbmF0qZWL0b5xURFaRbKR2qNXdvxEjIq86FdqSKacnH0lsHSgGzNKFbDSKFAY1u6Kq3unHFmgRk7UAD/YTQ==} '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} @@ -600,80 +624,56 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} - '@distilled.cloud/aws@0.30.3': - resolution: {integrity: sha512-6U/wO+fLNnqBlRnqFpF79edS5t6njDl/6UmnCVUfWpIQ4n23X0VY1ft0lzsaBa506xRtF4vRhMELR9FIp5DKUA==} - peerDependencies: - effect: '>=4.0.0-beta.100 || >=4.0.0' - - '@distilled.cloud/axiom@0.30.3': - resolution: {integrity: sha512-U4YvXsvz/TDYfIbZNRVAv8Yx1mnbms/rBQ/RHnRQWhL0JzxZOZOQvToxkB4kh/oa8KT+QbjTkDxRJP/f6P0M1g==} + '@distilled.cloud/aws@1.0.0-rc.6': + resolution: {integrity: sha512-WI0KK4mCdIvclKH7kbK/geY9iIRiSN6v067OdvuLUOEHA19GdQj88lUBSbtwdmIG0sm61zm2JKHwXO+RKseuIg==} peerDependencies: - effect: '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/cloudflare-rolldown-plugin@0.15.0': - resolution: {integrity: sha512-cBVl4Ck4Prf9/dPSvyQeGW+2CDLIKs+xbyUm/MgNOSyYDZYOLbsCnDePO4YwdvaxsT0oOZZIkSDDki2ve9DCHA==} + '@distilled.cloud/axiom@1.0.0-rc.6': + resolution: {integrity: sha512-np94ilGVgnsOjM8mMpfmSRMvtQBH5qvZdBJ1K9MG+NxCmP5yo+F57TTiWgw1Ta++jT8TDYuFPg/EITO2BAMkRA==} peerDependencies: - rolldown: ^1.1.5 - vite: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - rolldown: - optional: true - vite: - optional: true + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/cloudflare-runtime@0.15.0': - resolution: {integrity: sha512-0xx+LCiBzNwmMPN1SnnD4GixVl8WZET3zaMPU51LYRU+VQQrxtetDjcjnI5q83UComjBmbS0c0Bp4j/7hgobyg==} + '@distilled.cloud/cloudflare@1.0.0-rc.6': + resolution: {integrity: sha512-5nN5MuHo2UgQIHswt7J+4B/nUg8LvMiODTgPNL33ui4aqgAVj7H0S1+JsHIl03nh1d2twVaKglGtQbwZDXLf/w==} peerDependencies: - '@distilled.cloud/cloudflare': ^0.29.0 - '@effect/platform-bun': '>=4.0.0-beta.100 || >=4.0.0' - '@effect/platform-node': '>=4.0.0-beta.100 || >=4.0.0' - effect: '>=4.0.0-beta.100 || >=4.0.0' - peerDependenciesMeta: - '@effect/platform-bun': - optional: true - '@effect/platform-node': - optional: true + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/cloudflare-vite-plugin@0.15.0': - resolution: {integrity: sha512-+JYCTv/1Bqk3GRSb6e+UW61Pn2DFF68EAVvAbTYg5Z6tlztc2E7sd/pC4E2fsnAxtSMjwpFdrzlPqwQUq2HUQA==} + '@distilled.cloud/core@1.0.0-rc.6': + resolution: {integrity: sha512-nNKbsNmlRNMgXaXZdPGBqMrKLFpql2CGW7mWcGR5M3csA4WpdRJnwG5mkDcI+uw1VqxRrzOM1Ijiab0AzQCrLg==} peerDependencies: - '@distilled.cloud/cloudflare': ^0.29.0 - '@distilled.cloud/cloudflare-runtime': 0.15.0 - '@effect/platform-bun': '>=4.0.0-beta.100 || >=4.0.0' - '@effect/platform-node': '>=4.0.0-beta.100 || >=4.0.0' - effect: '>=4.0.0-beta.100 || >=4.0.0' - vite: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@effect/platform-bun': - optional: true - '@effect/platform-node': - optional: true + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/cloudflare@0.30.3': - resolution: {integrity: sha512-IwQyIZrfzRJ7Dn9Plsk5pMlr50CT72fMnD5YCFy31MKAthh906ewcO3NuJOKHGA8AF9u6SKTlebsQx4Jnj8uwA==} + '@distilled.cloud/fly-io@1.0.0-rc.6': + resolution: {integrity: sha512-023LsogDqQilo/wBCM+ASbD26DIZi3PInJKNLS42hB/eY2SH3D3QYiNpkKW72Qj+ZOoyT8AvNVHRKOQHXm/C8A==} peerDependencies: - effect: '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/core@0.30.3': - resolution: {integrity: sha512-RupX597cPmceEiOY6csihFbzH2WhDkfbwGCMk+Izx8+f16u+aLm4mgyrYoxj1/dzIHsyIfw8sFKgNPzAg0Rx2Q==} + '@distilled.cloud/hetzner@1.0.0-rc.6': + resolution: {integrity: sha512-k0uaPNFdl5l1rj0qlOt5y8NkfdSb70+nUa4p7Xt4PGMPJl9xsUCoPanOwaI+LbtWebWQfC6zhhdPEUXLNAr/5g==} peerDependencies: - effect: '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/neon@0.30.3': - resolution: {integrity: sha512-4iW6lNvrJ/BuraKQ572bTQPNNtK/pFMqALoKjgozXR5jAZXbohRrD8/3QHLZW9cOAPenaHugwk4YIm0/+u4j5Q==} + '@distilled.cloud/neon@1.0.0-rc.6': + resolution: {integrity: sha512-yn+4GhQ8Gu5GVcx3g1zxVCheBSmaQE5FAA5TzzXnlIiKUwmMLVTB/ucwJpDYhoUxjWK7bnv1eVZReoibYMHX4g==} peerDependencies: - effect: '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-rc.110 || >=4.0.0' - '@distilled.cloud/planetscale@0.30.3': - resolution: {integrity: sha512-0ZcPqoXKl5uhJ6Ytw9UpgHa2t2pjwsuQFZn4OlnjZl0F/lToO/8TdXp4iKyw7Wog5tKTkfRYrQXVzRbaPKP3QQ==} + '@distilled.cloud/planetscale@1.0.0-rc.6': + resolution: {integrity: sha512-6lIFwZAXiQCJ9u5QMIyY72qTdYbWmGAt7+HbONFEvmihwvecFuRo1i1QYkx5j9EN8c54KeO7CcEBq7HXpvwIhQ==} peerDependencies: - effect: '>=4.0.0-beta.100 || >=4.0.0' + effect: '>=4.0.0-rc.110 || >=4.0.0' '@effect/sql-d1@4.0.0-rc.111': resolution: {integrity: sha512-hjIoVS59gAvP1DjB+R5hwL9n/UbS1598/qcT1Hp3Tn3ksuJUOPTXfsCCiAQT3Ueecfkbiy32fxrCbzD0Z1YJLA==} peerDependencies: effect: ^4.0.0-rc.111 + '@effect/sql-sqlite-do@4.0.0-rc.112': + resolution: {integrity: sha512-PewNVasimmhrdxYbNU91O0YlCcalIHOoF0H5XAWXrmzcEQrKM7kVCyb5h0gmJAZjBM4ZgWRvPY0PUOfbNmWchQ==} + peerDependencies: + effect: ^4.0.0-rc.112 + '@effect/vitest@4.0.0-rc.111': resolution: {integrity: sha512-YDaEVT+grREBVMzykRNFtJwGxy02achzT0WXZYwMJA4ukzuB9krkgQ5roc3N6zhC3NQq/j4IyM32/Pcov7AhHw==} peerDependencies: @@ -1376,6 +1376,152 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -1466,6 +1612,10 @@ packages: cpu: [x64] os: [win32] + '@manypkg/tools@2.1.2': + resolution: {integrity: sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==} + engines: {node: '>=20.0.0'} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} @@ -1670,23 +1820,19 @@ packages: engines: {node: '>=18'} hasBin: true - '@prisma/cli-engine@0.2.0': - resolution: {integrity: sha512-nd0sP3l7W79ASoONP9DV8LKVcADmtPm0KMwKJ9rVJL3BbSJdN6HLO2pHFugI4Tp1NdiwcwlJuK5rCKxJPu7kFw==} - engines: {node: '>=22.12.0'} - '@prisma/cli-engine@0.2.3': resolution: {integrity: sha512-+OySr3NnF1BT6Gj34jKBBWwdQyzPL0YQVMygm0nn4qDRmB5OEYdlz4tS0Nw6HGhnTA82czEl05wTbX821VbBOA==} engines: {node: '>=22.12.0'} - '@prisma/composer-cli@0.11.0': - resolution: {integrity: sha512-qhibvb5ARF6JmvsUEpr3A0J2Kjx4whPRZP3LCOULk9+1/Cp2IWyHsLhDq+I6n0nmwrdvvySAWwW71ugSey24kA==} + '@prisma/composer-cli@0.14.0-dev.1': + resolution: {integrity: sha512-DORY5RsGpSil08sUtv+wO1W0x23amByJSLK4a7msa5v721/QxhbMgM0BI70aRApy+S+VDCkczBqycJEQL2arUA==} engines: {node: '>=22.18.0'} hasBin: true peerDependencies: - '@prisma/cli-engine': 0.2.0 + '@prisma/cli-engine': 0.2.3 - '@prisma/composer@0.11.0': - resolution: {integrity: sha512-NP4Ds9qrHQdtitj2pBRdUkuHs6Crtx+uCHHKyUfAaIeKW3b0vRrOibJUhaYXZ/dE9YrbYRmtpddLO0ZTL1h1yA==} + '@prisma/composer@0.14.0-dev.1': + resolution: {integrity: sha512-0b9vVVBlgHVfvMH9HTuUuBLruLUm5QsqyhOwwnJkIx67lVYmLzxbP/+X78W6otLA9JSh59+AiHwjQxAPUf8bMw==} engines: {node: '>=22.18.0'} '@prisma/compute-sdk@0.39.0': @@ -1704,9 +1850,6 @@ packages: '@prisma/credentials-store@7.1.0': resolution: {integrity: sha512-hQ5XKET/AHCeWuBISD9Y0d93Ja3ep6bpCRNox283IGV+IBUBjwVArb8Q5QwQWK1dniCLRynkE+fEMIfL3c6N5w==} - '@prisma/credentials-store@7.9.1': - resolution: {integrity: sha512-WCrMfi3EGBbN9QCBPHfkBCl6ri83h5HteTRxeLeBVkUs4pn9JbxRcPinXjqnIcxKWrIx2As/Q8yWXWeWb0jAyQ==} - '@prisma/debug@7.1.0': resolution: {integrity: sha512-pPAckG6etgAsEBusmZiFwM9bldLSNkn++YuC4jCTJACdK5hLOVnOzX7eSL2FgaU6Gomd6wIw21snUX2dYroMZQ==} @@ -1747,14 +1890,6 @@ packages: typescript: optional: true - '@prisma/orm-framework@8.0.0-rc.4': - resolution: {integrity: sha512-vEMX1h5UF5zOyIT5TVKeWR9c8TcghJWggaFTVp3uXuHFyRUOANPomMMocXTFt2bhvdp/Ny7KxJ3KDJXyc1wCmw==} - peerDependencies: - typescript: '>=5.9' - peerDependenciesMeta: - typescript: - optional: true - '@prisma/orm-framework@8.0.0-rc.7-dev.1': resolution: {integrity: sha512-U3GaRDrof0ulBIPGAvcN5ECCNqaWDXE3ivysXLKw3LjkaSUFgp48LiMFKvKlVRxjpni+NXn3mBDmokvHfj/cNA==} peerDependencies: @@ -1779,18 +1914,6 @@ packages: typescript: optional: true - '@prisma/orm-toolchain@8.0.0-rc.4': - resolution: {integrity: sha512-YufxTbj0jB8f6iSCbo/KEgWwPP6Y1C2XFEg5N6YnVLHV7nP92NfR51qlUJrtvj153mltPZjKpGdAdtxvf0Besw==} - peerDependencies: - '@prisma/cli-engine': 0.2.0 - typescript: '>=5.9' - vite: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - vite: - optional: true - '@prisma/orm-toolchain@8.0.0-rc.7-dev.1': resolution: {integrity: sha512-TswFjFGg5afnO/2PvcL6a0lBoR/VW9jr9JbPydclm5Dmna5Nmd+nKb+kj2NOY5YsT+lVy1Gt4qz/KrdVBNdEbg==} peerDependencies: @@ -1831,6 +1954,19 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@puppeteer/browsers@3.2.1': + resolution: {integrity: sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A==} + engines: {node: '>=22.12.0'} + hasBin: true + peerDependencies: + proxy-agent: '>=8.0.1' + yauzl: ^2.10.0 || ^3.4.0 + peerDependenciesMeta: + proxy-agent: + optional: true + yauzl: + optional: true + '@rolldown/binding-android-arm64@1.1.5': resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2504,32 +2640,50 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - alchemy@2.0.0-beta.67: - resolution: {integrity: sha512-kFEKEXtRdf781lRzGyYinPRVgrMKJFs5MNQTxF2Y5RIxyA2qCsK0NOOBBoOzYmGOln8e3CIYF/oEASFQjffXJA==} + alchemy@2.0.0-beta.74: + resolution: {integrity: sha512-Dpy6lZxk1SS5sZeV8vA79c0RaMLcniFCKewai1jEEamzFz0dR+yi2Ckmgi59ubMLAKvp9LNGNnhvRXfF0ETbkA==} hasBin: true peerDependencies: + '@alchemy.run/frontend-frameworks': 2.0.0-beta.74 '@aws/durable-execution-sdk-js': ^2.1.0 - '@effect/platform-bun': '>=4.0.0-beta.100 || >=4.0.0' - '@effect/platform-node': '>=4.0.0-beta.100 || >=4.0.0' - '@effect/sql-pg': '>=4.0.0-beta.100 || >=4.0.0' - drizzle-kit: 1.0.0-rc.4 - drizzle-orm: 1.0.0-rc.4 - effect: '>=4.0.0-beta.100 || >=4.0.0' + '@effect/platform-bun': '>=4.0.0-rc.110 || >=4.0.0' + '@effect/platform-node': '>=4.0.0-rc.110 || >=4.0.0' + '@effect/sql-mysql2': '>=4.0.0-rc.110 || >=4.0.0' + '@effect/sql-pg': '>=4.0.0-rc.110 || >=4.0.0' + '@vercel/nft': ^1.10.2 + drizzle-kit: 1.0.0-rc.5-ab785fc + drizzle-orm: 1.0.0-rc.5-ab785fc + effect: '>=4.0.0-rc.110 || >=4.0.0' + mongodb: ^6.10.0 + mysql2: ^3.23.2 + pg: ^8.22.0 vite: ^8.0.7 ws: ^8.20.0 peerDependenciesMeta: + '@alchemy.run/frontend-frameworks': + optional: true '@aws/durable-execution-sdk-js': optional: true '@effect/platform-bun': optional: true '@effect/platform-node': optional: true + '@effect/sql-mysql2': + optional: true '@effect/sql-pg': optional: true + '@vercel/nft': + optional: true drizzle-kit: optional: true drizzle-orm: optional: true + mongodb: + optional: true + mysql2: + optional: true + pg: + optional: true vite: optional: true ws: @@ -2788,6 +2942,15 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + capnp-es@0.0.14: + resolution: {integrity: sha512-8lWj4GJISiqRSlAJGkWpI4Azib7QY5UDIkqxeHcI7aAnXVk9SFuWxl1Fme+2HhzNANV0WTJqwUZvvXvloc3sBA==} + hasBin: true + peerDependencies: + typescript: ^5.7.3 + peerDependenciesMeta: + typescript: + optional: true + capnweb@0.6.1: resolution: {integrity: sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg==} @@ -2901,6 +3064,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + closest-match@1.3.3: resolution: {integrity: sha512-RSdHrZwNOvt2uMQgqJDJdM/I+5MlJ1tQJEXYrbRjSMXWiCRo06g2hwObJ7+WKt2J9ySK9/pJ0Q2vbL+BPkofDA==} @@ -3151,8 +3318,8 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} - effect@4.0.0-beta.103: - resolution: {integrity: sha512-pE8TxF4m2tQzVI+77dIlm3s+81TACV1AiX1JEkvY+zVuxgQQ8aGSkqXNJF6b/ST+coCSk5cUdbULpQ7sm4oHyw==} + effect@4.0.0-rc.111: + resolution: {integrity: sha512-ASd5L58EIR0CUNueZNKKjSsyOCd+2alxOAIaTcHaqkJkPsaYSsw5Cg/cfANk5K4Jr2YsX756xvX11shzqsreWA==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3418,9 +3585,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3740,10 +3904,6 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - ink@6.8.0: resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} engines: {node: '>=20'} @@ -3918,6 +4078,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-base64@3.9.3: resolution: {integrity: sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g==} @@ -3991,9 +4154,6 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -4262,6 +4422,10 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true + modern-tar@0.8.4: + resolution: {integrity: sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==} + engines: {node: '>=18.0.0'} + mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} @@ -4302,9 +4466,6 @@ packages: msgpackr@2.0.5: resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} - multipasta@0.2.8: - resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} - mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -4614,15 +4775,6 @@ packages: pg-native: optional: true - pg@8.23.0: - resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} - engines: {node: '>= 16.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} @@ -4729,8 +4881,8 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - prisma@8.0.0-rc.7: - resolution: {integrity: sha512-fIdFG8puEM+NVqGbyUtS+rGGCmm8ODday1CGZNgqxQdzoFDm9Tmj4HqetfAZOLKac2YLD6R/8IyAtChTKgZ7mw==} + prisma@8.0.0-rc.10-dev.82: + resolution: {integrity: sha512-fGDlAXlIuFS7qejjX/Hldpe0LCizKfr5NcYhlgeiud6IGERO1qOxjrABj9Om1Gzj5R6+yzl/oNCXCo6zYR3zuA==} engines: {node: '>=22.18.0'} hasBin: true @@ -4930,12 +5082,26 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5251,10 +5417,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toml@4.3.0: - resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} - engines: {node: '>=20'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5413,10 +5575,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@14.0.2: - resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} - hasBin: true - uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -5772,6 +5930,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs-unparser@2.0.0: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} engines: {node: '>=10'} @@ -5784,6 +5946,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} @@ -5791,6 +5957,10 @@ packages: resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} engines: {node: '>=12'} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + yazl@2.5.1: resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} @@ -5845,7 +6015,31 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@alchemy.run/node-utils@0.0.5': {} + '@alchemy.run/cloudflare-runtime@2.0.0-beta.74(@distilled.cloud/cloudflare@1.0.0-rc.6(effect@4.0.0-rc.111))(@types/node@20.14.8)(effect@4.0.0-rc.111)(rolldown@1.1.5)(typescript@5.9.3)': + dependencies: + '@alchemy.run/node-utils': 2.0.0-beta.74 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260704.1) + '@distilled.cloud/cloudflare': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@puppeteer/browsers': 3.2.1(yauzl@3.4.0) + capnp-es: 0.0.14(typescript@5.9.3) + effect: 4.0.0-rc.111 + magic-string: 0.30.21 + sharp: 0.35.3(@types/node@20.14.8) + unenv: 2.0.0-rc.24 + workerd: 1.20260704.1 + yauzl: 3.4.0 + optionalDependencies: + rolldown: 1.1.5 + transitivePeerDependencies: + - '@types/node' + - proxy-agent + - typescript + + '@alchemy.run/floci@2.0.0-beta.74(effect@4.0.0-rc.111)': + dependencies: + effect: 4.0.0-rc.111 + + '@alchemy.run/node-utils@2.0.0-beta.74': {} '@ampproject/remapping@2.3.0': dependencies: @@ -6328,82 +6522,67 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 - '@distilled.cloud/aws@0.30.3(effect@4.0.0-beta.103)': + '@distilled.cloud/aws@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/util': 5.2.0 '@aws-sdk/credential-providers': 3.1116.0 '@aws-sdk/types': 3.974.5 - '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) '@smithy/shared-ini-file-loader': 4.7.2 '@smithy/types': 4.17.2 '@smithy/util-base64': 4.6.2 aws4fetch: 1.0.20 - effect: 4.0.0-beta.103 + effect: 4.0.0-rc.111 fast-xml-parser: 5.11.0 - '@distilled.cloud/axiom@0.30.3(effect@4.0.0-beta.103)': + '@distilled.cloud/axiom@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) - effect: 4.0.0-beta.103 + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + effect: 4.0.0-rc.111 - '@distilled.cloud/cloudflare-rolldown-plugin@0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': + '@distilled.cloud/cloudflare@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260704.1) - magic-string: 0.30.21 - unenv: 2.0.0-rc.24 - optionalDependencies: - rolldown: 1.1.5 - vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) - transitivePeerDependencies: - - workerd + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + effect: 4.0.0-rc.111 - '@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)': + '@distilled.cloud/core@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - '@alchemy.run/node-utils': 0.0.5 - '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) - effect: 4.0.0-beta.103 - workerd: 1.20260704.1 + effect: 4.0.0-rc.111 - '@distilled.cloud/cloudflare-vite-plugin@0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1)': + '@distilled.cloud/fly-io@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) - '@distilled.cloud/cloudflare-runtime': 0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103) - effect: 4.0.0-beta.103 - vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) - transitivePeerDependencies: - - rolldown - - workerd + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + effect: 4.0.0-rc.111 - '@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103)': + '@distilled.cloud/hetzner@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) - effect: 4.0.0-beta.103 + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + effect: 4.0.0-rc.111 - '@distilled.cloud/core@0.30.3(effect@4.0.0-beta.103)': + '@distilled.cloud/neon@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - effect: 4.0.0-beta.103 + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + effect: 4.0.0-rc.111 - '@distilled.cloud/neon@0.30.3(effect@4.0.0-beta.103)': + '@distilled.cloud/planetscale@1.0.0-rc.6(effect@4.0.0-rc.111)': dependencies: - '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) - effect: 4.0.0-beta.103 + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + effect: 4.0.0-rc.111 - '@distilled.cloud/planetscale@0.30.3(effect@4.0.0-beta.103)': + '@effect/sql-d1@4.0.0-rc.111(effect@4.0.0-rc.111)': dependencies: - '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) - effect: 4.0.0-beta.103 + '@cloudflare/workers-types': 5.20260822.1 + effect: 4.0.0-rc.111 - '@effect/sql-d1@4.0.0-rc.111(effect@4.0.0-beta.103)': + '@effect/sql-sqlite-do@4.0.0-rc.112(effect@4.0.0-rc.111)': dependencies: - '@cloudflare/workers-types': 5.20260822.1 - effect: 4.0.0-beta.103 + effect: 4.0.0-rc.111 - '@effect/vitest@4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@2.1.9(@types/node@14.18.63))': + '@effect/vitest@4.0.0-rc.111(effect@4.0.0-rc.111)(vitest@2.1.9(@types/node@20.14.8))': dependencies: - effect: 4.0.0-beta.103 - vitest: 2.1.9(@types/node@14.18.63) + effect: 4.0.0-rc.111 + vitest: 2.1.9(@types/node@20.14.8) '@electric-sql/pglite-socket@0.0.19(@electric-sql/pglite@0.3.14)': dependencies: @@ -6793,6 +6972,112 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true + + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true + + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true + + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true + + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true + + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true + + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': + optional: true + + '@img/sharp-win32-ia32@0.35.3': + optional: true + + '@img/sharp-win32-x64@0.35.3': + optional: true + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -6810,7 +7095,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@istanbuljs/schema@0.1.3': {} @@ -6886,6 +7171,12 @@ snapshots: '@libsql/win32-x64-msvc@0.5.29': optional: true + '@manypkg/tools@2.1.2': + dependencies: + jju: 1.4.0 + tinyglobby: 0.2.15 + yaml: 2.9.0 + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 @@ -6902,6 +7193,7 @@ snapshots: '@mongodb-js/saslprep@1.5.0': dependencies: sparse-bitfield: 3.0.3 + optional: true '@mrleebo/prisma-ast@0.13.1': dependencies: @@ -7099,18 +7391,6 @@ snapshots: dependencies: playwright: 1.57.0 - '@prisma/cli-engine@0.2.0(magicast@0.5.4)': - dependencies: - '@clack/prompts': 1.5.0 - '@prisma/management-api-sdk': 1.55.0 - '@stricli/core': 1.3.0 - c12: 3.3.4(magicast@0.5.4) - colorette: 2.0.20 - package-manager-detector: 1.8.0 - string-width: 8.2.2 - transitivePeerDependencies: - - magicast - '@prisma/cli-engine@0.2.3(magicast@0.5.4)': dependencies: '@clack/prompts': 1.5.0 @@ -7123,79 +7403,71 @@ snapshots: transitivePeerDependencies: - magicast - '@prisma/composer-cli@0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3)': + '@prisma/composer-cli@0.14.0-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(magicast@0.5.4)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3)': dependencies: - '@prisma/cli-engine': 0.2.0(magicast@0.5.4) - '@prisma/composer': 0.11.0(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) - alchemy: 2.0.0-beta.67(@types/node@14.18.63)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) + '@prisma/cli-engine': 0.2.3(magicast@0.5.4) + '@prisma/composer': 0.14.0-dev.1(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(magicast@0.5.4)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3) + alchemy: 2.0.0-beta.74(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(effect@4.0.0-rc.111)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3) c12: 3.3.4(magicast@0.5.4) - effect: 4.0.0-beta.103 + effect: 4.0.0-rc.111 esbuild: 0.28.2 transitivePeerDependencies: + - '@alchemy.run/frontend-frameworks' - '@aws/durable-execution-sdk-js' - '@effect/platform-bun' - '@effect/platform-node' + - '@effect/sql-mysql2' - '@effect/sql-pg' - - '@mongodb-js/zstd' - '@types/node' - '@types/react' + - '@vercel/nft' - bufferutil - drizzle-kit - drizzle-orm - - encoding - - gcp-metadata - - kerberos - magicast - - mongodb-client-encryption - - pg-native + - mongodb + - mysql2 + - pg + - proxy-agent - react-devtools-core - - rollup - - snappy - - socks - - supports-color - typescript - utf-8-validate - vite - vitest - - workerd - ws - '@prisma/composer@0.11.0(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3)': + '@prisma/composer@0.14.0-dev.1(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(magicast@0.5.4)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3)': dependencies: '@prisma/management-api-sdk': 1.67.0 '@standard-schema/spec': 1.1.0 - alchemy: 2.0.0-beta.67(@types/node@14.18.63)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) + alchemy: 2.0.0-beta.74(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(effect@4.0.0-rc.111)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3) arktype: 2.2.3 c12: 3.3.4(magicast@0.5.4) - effect: 4.0.0-beta.103 + effect: 4.0.0-rc.111 esbuild: 0.28.2 transitivePeerDependencies: + - '@alchemy.run/frontend-frameworks' - '@aws/durable-execution-sdk-js' - '@effect/platform-bun' - '@effect/platform-node' + - '@effect/sql-mysql2' - '@effect/sql-pg' - - '@mongodb-js/zstd' - '@types/node' - '@types/react' + - '@vercel/nft' - bufferutil - drizzle-kit - drizzle-orm - - encoding - - gcp-metadata - - kerberos - magicast - - mongodb-client-encryption - - pg-native + - mongodb + - mysql2 + - pg + - proxy-agent - react-devtools-core - - rollup - - snappy - - socks - - supports-color - typescript - utf-8-validate - vite - vitest - - workerd - ws '@prisma/compute-sdk@0.39.0(@prisma/management-api-sdk@1.55.0)(rollup@4.53.3)': @@ -7242,10 +7514,6 @@ snapshots: dependencies: xdg-app-paths: 8.3.0 - '@prisma/credentials-store@7.9.1': - dependencies: - xdg-app-paths: 8.3.0 - '@prisma/debug@7.1.0': {} '@prisma/debug@7.2.0': {} @@ -7328,10 +7596,10 @@ snapshots: dependencies: openapi-fetch: 0.14.0 - '@prisma/orm-family-sql@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-family-sql@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)': dependencies: '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) - '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) '@standard-schema/spec': 1.1.0 arktype: 2.2.3 pathe: 2.0.3 @@ -7345,15 +7613,6 @@ snapshots: - typanion - vite - '@prisma/orm-framework@8.0.0-rc.4(typescript@5.9.3)': - dependencies: - '@standard-schema/spec': 1.1.0 - arktype: 2.2.3 - pathe: 2.0.3 - uniku: 0.5.0 - optionalDependencies: - typescript: 5.9.3 - '@prisma/orm-framework@8.0.0-rc.7-dev.1(typescript@5.9.3)': dependencies: '@standard-schema/spec': 1.1.0 @@ -7363,12 +7622,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@prisma/orm-postgres@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-postgres@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)': dependencies: - '@prisma/orm-family-sql': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-family-sql': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) - '@prisma/orm-target-postgres': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) - '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-target-postgres': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) '@types/pg': 8.20.4 pathe: 2.0.3 pg: 8.22.0 @@ -7381,11 +7640,11 @@ snapshots: - typanion - vite - '@prisma/orm-target-postgres@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-target-postgres@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)': dependencies: - '@prisma/orm-family-sql': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-family-sql': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) - '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) '@standard-schema/spec': 1.1.0 '@types/pg': 8.20.4 arktype: 2.2.3 @@ -7401,35 +7660,7 @@ snapshots: - typanion - vite - '@prisma/orm-toolchain@8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': - dependencies: - '@prisma/cli-engine': 0.2.0(magicast@0.5.4) - '@prisma/orm-framework': 8.0.0-rc.4(typescript@5.9.3) - '@vercel/detect-agent': 1.2.5 - arktype: 2.2.3 - c12: 3.3.4(magicast@0.5.4) - ci-info: 4.4.0 - clipanion: 4.0.0-rc.4(typanion@3.14.0) - closest-match: 1.3.3 - colorette: 2.0.20 - esbuild: 0.28.2 - jsonc-parser: 3.3.1 - package-manager-detector: 1.8.0 - pathe: 2.0.3 - prettier: 3.9.6 - string-width: 8.2.2 - strip-ansi: 7.2.0 - vscode-languageserver: 10.1.0 - vscode-languageserver-textdocument: 1.0.12 - wrap-ansi: 10.0.1 - optionalDependencies: - typescript: 5.9.3 - vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) - transitivePeerDependencies: - - magicast - - typanion - - '@prisma/orm-toolchain@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))': + '@prisma/orm-toolchain@8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)': dependencies: '@prisma/cli-engine': 0.2.3(magicast@0.5.4) '@prisma/orm-framework': 8.0.0-rc.7-dev.1(typescript@5.9.3) @@ -7452,7 +7683,6 @@ snapshots: wrap-ansi: 10.0.1 optionalDependencies: typescript: 5.9.3 - vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - magicast - typanion @@ -7483,6 +7713,13 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + '@puppeteer/browsers@3.2.1(yauzl@3.4.0)': + dependencies: + modern-tar: 0.8.4 + yargs: 18.1.0 + optionalDependencies: + yauzl: 3.4.0 + '@rolldown/binding-android-arm64@1.1.5': optional: true @@ -7863,11 +8100,13 @@ snapshots: '@types/vscode@1.104.0': {} - '@types/webidl-conversions@7.0.3': {} + '@types/webidl-conversions@7.0.3': + optional: true '@types/whatwg-url@11.0.5': dependencies: '@types/webidl-conversions': 7.0.3 + optional: true '@types/ws@8.18.1': dependencies: @@ -8272,22 +8511,24 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@2.0.0-beta.67(@types/node@14.18.63)(@types/react@19.2.7)(effect@4.0.0-beta.103)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3): + alchemy@2.0.0-beta.74(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(effect@4.0.0-rc.111)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3): dependencies: - '@alchemy.run/node-utils': 0.0.5 + '@alchemy.run/cloudflare-runtime': 2.0.0-beta.74(@distilled.cloud/cloudflare@1.0.0-rc.6(effect@4.0.0-rc.111))(@types/node@20.14.8)(effect@4.0.0-rc.111)(rolldown@1.1.5)(typescript@5.9.3) + '@alchemy.run/floci': 2.0.0-beta.74(effect@4.0.0-rc.111) + '@alchemy.run/node-utils': 2.0.0-beta.74 '@aws-sdk/credential-providers': 3.1116.0 '@clack/prompts': 1.7.0 - '@distilled.cloud/aws': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/axiom': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare-rolldown-plugin': 0.15.0(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) - '@distilled.cloud/cloudflare-runtime': 0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103) - '@distilled.cloud/cloudflare-vite-plugin': 0.15.0(@distilled.cloud/cloudflare-runtime@0.15.0(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103))(@distilled.cloud/cloudflare@0.30.3(effect@4.0.0-beta.103))(effect@4.0.0-beta.103)(rolldown@1.1.5)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(workerd@1.20260704.1) - '@distilled.cloud/core': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/neon': 0.30.3(effect@4.0.0-beta.103) - '@distilled.cloud/planetscale': 0.30.3(effect@4.0.0-beta.103) - '@effect/sql-d1': 4.0.0-rc.111(effect@4.0.0-beta.103) - '@effect/vitest': 4.0.0-rc.111(effect@4.0.0-beta.103)(vitest@2.1.9(@types/node@14.18.63)) + '@distilled.cloud/aws': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/axiom': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/cloudflare': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/core': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/fly-io': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/hetzner': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/neon': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@distilled.cloud/planetscale': 1.0.0-rc.6(effect@4.0.0-rc.111) + '@effect/sql-d1': 4.0.0-rc.111(effect@4.0.0-rc.111) + '@effect/sql-sqlite-do': 4.0.0-rc.112(effect@4.0.0-rc.111) + '@effect/vitest': 4.0.0-rc.111(effect@4.0.0-rc.111)(vitest@2.1.9(@types/node@20.14.8)) '@libsql/client': 0.17.4 '@octokit/rest': 22.0.1 '@octokit/webhooks': 14.2.0 @@ -8296,46 +8537,35 @@ snapshots: '@smithy/shared-ini-file-loader': 4.7.2 '@smithy/types': 4.17.2 '@types/aws-lambda': 8.10.162 - '@vercel/nft': 1.11.0(rollup@4.53.3) aws4fetch: 1.0.20 capnweb: 0.6.1 - effect: 4.0.0-beta.103 + effect: 4.0.0-rc.111 fast-glob: 3.3.3 fast-xml-parser: 5.11.0 ink: 6.8.0(@types/react@19.2.7)(react@19.2.1) jszip: 3.10.1 libsodium-wrappers: 0.8.4 - mongodb: 6.21.0(@aws-sdk/credential-providers@3.1116.0) - mysql2: 3.23.4(@types/node@14.18.63) pathe: 2.0.3 - pg: 8.23.0 picomatch: 4.0.5 react: 19.2.1 rolldown: 1.1.5 undici: 7.16.0 - yaml: 2.6.1 + yaml: 2.9.0 optionalDependencies: - vite: 7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0) + '@vercel/nft': 1.11.0(rollup@4.53.3) + mongodb: 6.21.0(@aws-sdk/credential-providers@3.1116.0) + mysql2: 3.23.4(@types/node@20.14.8) + pg: 8.22.0 ws: 8.21.3 transitivePeerDependencies: - - '@mongodb-js/zstd' - '@types/node' - '@types/react' - bufferutil - - encoding - - gcp-metadata - - kerberos - - mongodb-client-encryption - - pg-native + - proxy-agent - react-devtools-core - - rollup - - snappy - - socks - - supports-color - typescript - utf-8-validate - vitest - - workerd ansi-colors@4.1.3: {} @@ -8400,7 +8630,8 @@ snapshots: auto-bind@5.0.1: {} - aws-ssl-profiles@1.1.2: {} + aws-ssl-profiles@1.1.2: + optional: true aws4fetch@1.0.20: {} @@ -8500,7 +8731,8 @@ snapshots: browser-stdout@1.3.1: {} - bson@6.10.4: {} + bson@6.10.4: + optional: true buffer-crc32@0.2.13: {} @@ -8571,6 +8803,10 @@ snapshots: camelcase@6.3.0: {} + capnp-es@0.0.14(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + capnweb@0.6.1: {} chai@5.3.3: @@ -8716,6 +8952,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + closest-match@1.3.3: {} cockatiel@3.2.1: {} @@ -8929,18 +9171,11 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - effect@4.0.0-beta.103: + effect@4.0.0-rc.111: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 4.9.0 - find-my-way-ts: 0.1.6 - ini: 7.0.0 - kubernetes-types: 1.30.0 msgpackr: 2.0.5 - multipasta: 0.2.8 - toml: 4.3.0 - uuid: 14.0.2 - yaml: 2.9.0 emoji-regex@10.6.0: {} @@ -9333,8 +9568,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-my-way-ts@0.1.6: {} - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -9399,6 +9632,7 @@ snapshots: generate-function@2.3.1: dependencies: is-property: 1.0.2 + optional: true get-caller-file@2.0.5: {} @@ -9612,6 +9846,7 @@ snapshots: iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 + optional: true ieee754@1.2.1: {} @@ -9646,8 +9881,6 @@ snapshots: ini@4.1.1: {} - ini@7.0.0: {} - ink@6.8.0(@types/react@19.2.7)(react@19.2.1): dependencies: '@alcalzone/ansi-tokenize': 0.2.5 @@ -9734,7 +9967,8 @@ snapshots: is-primitive@3.0.1: {} - is-property@1.0.2: {} + is-property@1.0.2: + optional: true is-stream@2.0.1: {} @@ -9803,6 +10037,8 @@ snapshots: jiti@2.7.0: {} + jju@1.4.0: {} + js-base64@3.9.3: {} js-levenshtein@1.1.6: {} @@ -9882,8 +10118,6 @@ snapshots: klona@2.0.6: {} - kubernetes-types@1.30.0: {} - leven@3.1.0: {} levn@0.4.1: @@ -10019,7 +10253,8 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 - long@5.3.2: {} + long@5.3.2: + optional: true loupe@3.2.1: {} @@ -10031,7 +10266,8 @@ snapshots: dependencies: yallist: 4.0.0 - lru.min@1.1.4: {} + lru.min@1.1.4: + optional: true magic-string@0.30.21: dependencies: @@ -10076,7 +10312,8 @@ snapshots: mdurl@2.0.0: {} - memory-pager@1.5.0: {} + memory-pager@1.5.0: + optional: true meow@12.1.1: {} @@ -10138,7 +10375,7 @@ snapshots: minizlib@3.1.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mkdirp-classic@0.5.3: optional: true @@ -10166,10 +10403,13 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + modern-tar@0.8.4: {} + mongodb-connection-string-url@3.0.2: dependencies: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 + optional: true mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0): dependencies: @@ -10178,6 +10418,7 @@ snapshots: mongodb-connection-string-url: 3.0.2 optionalDependencies: '@aws-sdk/credential-providers': 3.1116.0 + optional: true ms@2.1.3: {} @@ -10197,13 +10438,11 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.8: {} - mute-stream@0.0.8: {} - mysql2@3.23.4(@types/node@14.18.63): + mysql2@3.23.4(@types/node@20.14.8): dependencies: - '@types/node': 14.18.63 + '@types/node': 20.14.8 aws-ssl-profiles: 1.1.2 generate-function: 2.3.1 iconv-lite: 0.7.3 @@ -10211,10 +10450,12 @@ snapshots: lru.min: 1.1.4 named-placeholders: 1.1.6 sql-escaper: 1.5.1 + optional: true named-placeholders@1.1.6: dependencies: lru.min: 1.1.4 + optional: true nanoid@3.3.11: {} @@ -10504,10 +10745,6 @@ snapshots: dependencies: pg: 8.22.0 - pg-pool@3.14.0(pg@8.23.0): - dependencies: - pg: 8.23.0 - pg-protocol@1.16.0: {} pg-types@2.2.0: @@ -10528,16 +10765,6 @@ snapshots: optionalDependencies: pg-cloudflare: 1.4.0 - pg@8.23.0: - dependencies: - pg-connection-string: 2.14.0 - pg-pool: 3.14.0(pg@8.23.0) - pg-protocol: 1.16.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.4.0 - pgpass@1.0.5: dependencies: split2: 4.2.0 @@ -10628,50 +10855,49 @@ snapshots: dependencies: parse-ms: 4.0.0 - prisma@8.0.0-rc.7(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3): + prisma@8.0.0-rc.10-dev.82(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(magicast@0.5.4)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(rollup@4.53.3)(typanion@3.14.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3): dependencies: - '@prisma/cli-engine': 0.2.0(magicast@0.5.4) - '@prisma/composer-cli': 0.11.0(@prisma/cli-engine@0.2.0(magicast@0.5.4))(@types/node@14.18.63)(@types/react@19.2.7)(magicast@0.5.4)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0))(vitest@2.1.9(@types/node@14.18.63))(workerd@1.20260704.1)(ws@8.21.3) + '@manypkg/tools': 2.1.2 + '@prisma/cli-engine': 0.2.3(magicast@0.5.4) + '@prisma/composer-cli': 0.14.0-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(@types/node@20.14.8)(@types/react@19.2.7)(@vercel/nft@1.11.0(rollup@4.53.3))(magicast@0.5.4)(mongodb@6.21.0(@aws-sdk/credential-providers@3.1116.0))(mysql2@3.23.4(@types/node@20.14.8))(pg@8.22.0)(typescript@5.9.3)(vitest@2.1.9(@types/node@20.14.8))(ws@8.21.3) '@prisma/compute-sdk': 0.39.0(@prisma/management-api-sdk@1.55.0)(rollup@4.53.3) - '@prisma/credentials-store': 7.9.1 '@prisma/management-api-sdk': 1.55.0 - '@prisma/orm-toolchain': 8.0.0-rc.4(@prisma/cli-engine@0.2.0(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3)(vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.7-dev.1(@prisma/cli-engine@0.2.3(magicast@0.5.4))(magicast@0.5.4)(typanion@3.14.0)(typescript@5.9.3) '@vercel/detect-agent': 1.2.5 better-result: 2.10.0 dotenv: 17.4.2 execa: 9.6.1 open: 11.0.1 transitivePeerDependencies: + - '@alchemy.run/frontend-frameworks' - '@aws/durable-execution-sdk-js' - '@effect/platform-bun' - '@effect/platform-node' + - '@effect/sql-mysql2' - '@effect/sql-pg' - - '@mongodb-js/zstd' - '@types/node' - '@types/react' + - '@vercel/nft' - bare-abort-controller - bare-buffer - bufferutil - drizzle-kit - drizzle-orm - encoding - - gcp-metadata - - kerberos - magicast - - mongodb-client-encryption - - pg-native + - mongodb + - mysql2 + - pg + - proxy-agent - react-devtools-core - react-native-b4a - rollup - - snappy - - socks - supports-color - typanion - typescript - utf-8-validate - vite - vitest - - workerd - ws process-nextick-args@2.0.1: {} @@ -10908,12 +11134,47 @@ snapshots: semver@7.6.3: {} + semver@7.8.5: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 setimmediate@1.0.5: {} + sharp@0.35.3(@types/node@20.14.8): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 20.14.8 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11004,6 +11265,7 @@ snapshots: sparse-bitfield@3.0.3: dependencies: memory-pager: 1.5.0 + optional: true spdx-correct@3.2.0: dependencies: @@ -11021,7 +11283,8 @@ snapshots: split2@4.2.0: {} - sql-escaper@1.5.1: {} + sql-escaper@1.5.1: + optional: true stack-utils@2.0.6: dependencies: @@ -11186,7 +11449,7 @@ snapshots: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 - minipass: 7.1.2 + minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 @@ -11259,13 +11522,12 @@ snapshots: dependencies: is-number: 7.0.0 - toml@4.3.0: {} - tr46@0.0.3: {} tr46@5.1.1: dependencies: punycode: 2.3.1 + optional: true ts-api-utils@1.4.3(typescript@5.7.3): dependencies: @@ -11379,8 +11641,6 @@ snapshots: util-deprecate@1.0.2: {} - uuid@14.0.2: {} - uuid@8.3.2: {} uuid@9.0.1: {} @@ -11475,20 +11735,6 @@ snapshots: '@types/node': 20.14.8 fsevents: 2.3.3 - vite@7.2.6(@types/node@14.18.63)(jiti@2.7.0)(yaml@2.9.0): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.53.3 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 14.18.63 - fsevents: 2.3.3 - jiti: 2.7.0 - yaml: 2.9.0 - vite@7.2.6(@types/node@18.19.76)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 @@ -11672,7 +11918,8 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} + webidl-conversions@7.0.0: + optional: true whatwg-encoding@3.1.1: dependencies: @@ -11684,6 +11931,7 @@ snapshots: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 + optional: true whatwg-url@5.0.0: dependencies: @@ -11788,6 +12036,8 @@ snapshots: yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs-unparser@2.0.0: dependencies: camelcase: 6.3.0 @@ -11815,6 +12065,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 @@ -11825,6 +12084,10 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + yazl@2.5.1: dependencies: buffer-crc32: 0.2.13 From 0d18a3628a0211378d1893198d4a86ba21bb8076 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 13:27:48 +0000 Subject: [PATCH 35/43] refactor(vscode): clarify language server ownership --- docs/language-server.md | 23 ++-- docs/testing.md | 8 +- .../src/__test__/language-server/README.md | 2 +- .../vscode/src/__test__/workspace.test.ts | 28 ++--- .../documentOwnership.ts | 83 ++++++------- .../prisma-language-server/documentRouting.ts | 68 ++++++---- .../plugins/prisma-language-server/index.ts | 117 +++++++++--------- ...iddleware.ts => legacyClientMiddleware.ts} | 67 +++++----- ...lientStartup.ts => legacyClientStartup.ts} | 16 +-- ...eware.ts => prismaNextClientMiddleware.ts} | 22 ++-- ...egistry.ts => prismaNextClientRegistry.ts} | 58 ++++----- .../root-a/{bundled.prisma => legacy.prisma} | 0 12 files changed, 252 insertions(+), 240 deletions(-) rename packages/vscode/src/plugins/prisma-language-server/{bundledClientMiddleware.ts => legacyClientMiddleware.ts} (71%) rename packages/vscode/src/plugins/prisma-language-server/{bundledClientStartup.ts => legacyClientStartup.ts} (84%) rename packages/vscode/src/plugins/prisma-language-server/{localClientMiddleware.ts => prismaNextClientMiddleware.ts} (87%) rename packages/vscode/src/plugins/prisma-language-server/{localPrismaNextClientRegistry.ts => prismaNextClientRegistry.ts} (79%) rename packages/vscode/tests/fixtures/integration-workspace/root-a/{bundled.prisma => legacy.prisma} (100%) diff --git a/docs/language-server.md b/docs/language-server.md index 5870f48c8b..49acf7305c 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -40,7 +40,7 @@ When `prisma.pinToPrisma6` is disabled, the VS Code extension routes each open P | Document | Owner | | --------------------------------------------------------------------------------- | ------------------------------------------ | -| No `// use prisma-next` directive | Bundled language server | +| No `// use prisma-next` directive | Legacy language server | | Directive present, trusted file workspace, matching root, and local CLI available | Prisma Next client for that workspace root | | Directive present but local execution is ineligible or unavailable | No active language-server synchronization | @@ -48,22 +48,23 @@ The directive is content based and applies per file. A marked file does not opt ### Coordinator and synchronization boundary -`DocumentOwnershipCoordinator` is the authoritative per-URI state machine. Open and change events are serialized per document. A transfer performs these operations in order: +`DocumentOwnershipCoordinator` is the authoritative per-URI state machine. `desiredOwner` is computed from the document's current text and workspace policy; `settledOwner` records the server synchronized only after a serialized transition's commit closure completes. Open and change events are serialized per document. A transfer performs these operations in order: 1. Close the prior synchronized owner. 2. Clear that owner's diagnostics for only the transferred URI. 3. Reclassify current unsaved text. -4. Lazily ensure the candidate root-local client when needed. +4. Lazily ensure the candidate Prisma Next client for the exact workspace root when needed. 5. Reclassify after asynchronous startup. 6. Open the complete current document on the surviving owner. +7. Record the successfully synchronized candidate as the settled owner, or `unowned` if no candidate opened. -A close event invalidates pending revisions immediately, queues final cleanup, and leaves the URI internally unowned. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. +A close event invalidates pending revisions immediately and queues final cleanup. Its settled owner becomes `unowned` only after the cleanup commit closure completes. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. -Bundled and local middleware maintain ledgers of documents actually synchronized to their client. Raw editor notifications are forwarded only when committed ownership, current content classification, and (for local clients) the exact workspace root agree. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use the same ownership gate. Automatic local-client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to a local client over LSP. +Legacy and Prisma Next middleware maintain ledgers of documents actually synchronized to their client. Raw editor notifications, feature requests, and diagnostics are forwarded only when `getSettledOwner(document.uri)` and `getDesiredOwner(document)` agree on the middleware's expected identity and, for Prisma Next, the exact workspace root. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use this same gate. Automatic Prisma Next client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to Prisma Next over LSP. -### Root-local Prisma Next launch contract +### Workspace-root Prisma Next launch contract -The local-client registry is keyed by `WorkspaceFolder.uri.toString()` and coalesces concurrent startup for one root. Discovery checks only: +The Prisma Next client registry is keyed by `WorkspaceFolder.uri.toString()` and coalesces concurrent startup for one root. Discovery checks only: ```text /node_modules/prisma/dist/prisma.js @@ -88,10 +89,10 @@ Electron extension hosts receive `ELECTRON_RUN_AS_NODE=1` and `ELECTRON_NO_ASAR= The registry exposes a narrow lifecycle API used by routing and later workspace lifecycle handling: - `ensureClientForDocument(document)` — trust/root checks, exact discovery, and coalesced lazy startup. -- `openDocument(rootUri, document)` — verifies the document is still open before inserting it into the local middleware ledger. -- `closeDocument(rootUri, document)` — idempotently balances an actually synchronized local document. +- `openDocument(rootUri, document)` — verifies the document is still open before inserting it into the Prisma Next middleware ledger. +- `closeDocument(rootUri, document)` — idempotently balances an actually synchronized Prisma Next document. - `clearDiagnostics(rootUri, uri)` — clears only the requested URI. -A started local client currently remains alive after its final marked document closes. Workspace-wide restart and rediscovery, runtime-failure recovery, workspace-folder removal, comprehensive deactivation, and live Prisma 6 pin transitions are separate lifecycle responsibilities that should build on this API rather than bypass the coordinator or middleware ledgers. +A started Prisma Next client currently remains alive after its final marked document closes. Workspace-wide restart and rediscovery, runtime-failure recovery, workspace-folder removal, comprehensive deactivation, and live Prisma 6 pin transitions are separate lifecycle responsibilities that should build on this API rather than bypass the coordinator or middleware ledgers. -Routing is covered end to end through public completion behavior. In one workspace root, the Electron integration test opens an unmarked document served by the bundled Prisma 7 language server beside a marked document served by the real workspace-local Prisma 8 CLI. The bundled document offers `datasource`, `generator`, and `model` but not `namespace`; the marked document offers the Prisma 8 `namespace` keyword but not `datasource`. The test does not expose or inspect coordinator ownership, routing events, or client startup counts. +Routing is covered end to end through public completion behavior. In one workspace root, the Electron integration test opens an unmarked document served by the legacy Prisma 7 language server beside a marked document served by the real Prisma Next server from the workspace-local Prisma 8 CLI. The legacy document offers `datasource`, `generator`, and `model` but not `namespace`; the marked document offers the Prisma 8 `namespace` keyword but not `datasource`. The test does not expose or inspect coordinator ownership, routing events, or client startup counts. diff --git a/docs/testing.md b/docs/testing.md index ae057b5b42..67db2cb830 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -61,8 +61,8 @@ const userFile = helper.file('User.prisma') pnpm test:e2e # runs scripts/e2e.sh ``` -Uses the VS Code test framework for E2E testing of the extension. The language -server is bundled with the extension, so tests always use the local version. +Uses the VS Code test framework for E2E testing of the extension. The legacy +language server ships with the extension, so tests exercise the workspace build. ### Post-Publish E2E Testing @@ -85,6 +85,6 @@ Run the focused minimum-runtime workspace suite with: pnpm --filter prisma test:integration:workspace ``` -This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the bundled language server and the real workspace-local Prisma CLI process side by side; no mock language-server executable is part of the fixture. +This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the legacy language server and the real Prisma Next server launched from the workspace-local Prisma CLI side by side; no mock language-server executable is part of the fixture. -The test opens an empty, unmarked `bundled.prisma` and a marked `next.prisma` in separate editor columns, then polls only the public `vscode.executeCompletionItemProvider` command with a fixed timeout. At `(0, 0)`, the bundled server must offer `datasource`, `generator`, and `model`, classify `datasource` as `CompletionItemKind.Class`, and omit `namespace`. At `(1, 0)`, the local Prisma 8 server must offer `namespace` as `CompletionItemKind.Keyword` with detail `PSL declaration keyword`, and omit `datasource`. These assertions verify observable routing behavior without extension-private commands, owner state, events, or process start counts. +The test opens an empty, unmarked `legacy.prisma` and a marked `next.prisma` in separate editor columns, then polls only the public `vscode.executeCompletionItemProvider` command with a fixed timeout. At `(0, 0)`, the legacy Prisma 7 server must offer `datasource`, `generator`, and `model`, classify `datasource` as `CompletionItemKind.Class`, and omit `namespace`. At `(1, 0)`, the Prisma Next server from the workspace-local Prisma 8 CLI must offer `namespace` as `CompletionItemKind.Keyword` with detail `PSL declaration keyword`, and omit `datasource`. These assertions verify observable routing behavior without extension-private commands, owner state, events, or process start counts. diff --git a/packages/vscode/src/__test__/language-server/README.md b/packages/vscode/src/__test__/language-server/README.md index 078e6a71f8..99db0d0b32 100644 --- a/packages/vscode/src/__test__/language-server/README.md +++ b/packages/vscode/src/__test__/language-server/README.md @@ -10,6 +10,6 @@ Run the full minimum-and-latest integration suite with `pnpm test:integration`. pnpm --filter prisma test:integration:workspace ``` -The focused suite opens an empty, unmarked `bundled.prisma` and a marked `next.prisma` in separate editor columns. It polls only the public `vscode.executeCompletionItemProvider` command with a fixed timeout. The bundled Prisma 7 language server must offer `datasource`, `generator`, and `model`, classify `datasource` as `CompletionItemKind.Class`, and omit `namespace`. The real workspace-local Prisma 8 language server must offer `namespace` as `CompletionItemKind.Keyword` with detail `PSL declaration keyword`, and omit `datasource`. +The focused suite opens an empty, unmarked `legacy.prisma` and a marked `next.prisma` in separate editor columns. It polls only the public `vscode.executeCompletionItemProvider` command with a fixed timeout. The legacy Prisma 7 language server must offer `datasource`, `generator`, and `model`, classify `datasource` as `CompletionItemKind.Class`, and omit `namespace`. The real Prisma Next server launched from the workspace-local Prisma 8 CLI must offer `namespace` as `CompletionItemKind.Keyword` with detail `PSL declaration keyword`, and omit `datasource`. No mock language server or extension-private routing state is used. The test asserts observable editor behavior rather than owners, routing events, document synchronization bookkeeping, or process start counts. diff --git a/packages/vscode/src/__test__/workspace.test.ts b/packages/vscode/src/__test__/workspace.test.ts index 5ff600769f..4c8e0a7482 100644 --- a/packages/vscode/src/__test__/workspace.test.ts +++ b/packages/vscode/src/__test__/workspace.test.ts @@ -5,16 +5,16 @@ const completionTimeoutMs = 30_000 const completionPollIntervalMs = 100 suite('Prisma language server routing', () => { - test('provides bundled Prisma 7 and workspace-local Prisma 8 completions side by side', async () => { + test('provides legacy Prisma 7 and Prisma Next completions side by side', async () => { const workspaceFolders = vscode.workspace.workspaceFolders assert.ok(workspaceFolders) assert.strictEqual(workspaceFolders.length, 1) const root = workspaceFolders[0] - const bundledUri = vscode.Uri.joinPath(root.uri, 'bundled.prisma') + const legacyUri = vscode.Uri.joinPath(root.uri, 'legacy.prisma') const nextUri = vscode.Uri.joinPath(root.uri, 'next.prisma') - const bundledDocument = await vscode.workspace.openTextDocument(bundledUri) - await vscode.window.showTextDocument(bundledDocument, { viewColumn: vscode.ViewColumn.One }) + const legacyDocument = await vscode.workspace.openTextDocument(legacyUri) + await vscode.window.showTextDocument(legacyDocument, { viewColumn: vscode.ViewColumn.One }) const nextDocument = await vscode.workspace.openTextDocument(nextUri) await vscode.window.showTextDocument(nextDocument, { viewColumn: vscode.ViewColumn.Two }) @@ -22,20 +22,20 @@ suite('Prisma language server routing', () => { assert.ok(extension) await extension.activate() - const bundledCompletions = await waitForCompletions( - bundledUri, + const legacyCompletions = await waitForCompletions( + legacyUri, new vscode.Position(0, 0), (completions) => ['datasource', 'generator', 'model'].every((label) => hasLabel(completions, label)) && findCompletion(completions, 'datasource')?.kind === vscode.CompletionItemKind.Class, - 'bundled Prisma 7 declaration completions', + 'legacy Prisma 7 declaration completions', ) - const bundledDatasource = findCompletion(bundledCompletions, 'datasource') - assert.ok(bundledDatasource) - assert.strictEqual(bundledDatasource.kind, vscode.CompletionItemKind.Class) - assert.ok(hasLabel(bundledCompletions, 'generator')) - assert.ok(hasLabel(bundledCompletions, 'model')) - assert.ok(!hasLabel(bundledCompletions, 'namespace')) + const legacyDatasource = findCompletion(legacyCompletions, 'datasource') + assert.ok(legacyDatasource) + assert.strictEqual(legacyDatasource.kind, vscode.CompletionItemKind.Class) + assert.ok(hasLabel(legacyCompletions, 'generator')) + assert.ok(hasLabel(legacyCompletions, 'model')) + assert.ok(!hasLabel(legacyCompletions, 'namespace')) const nextCompletions = await waitForCompletions( nextUri, @@ -44,7 +44,7 @@ suite('Prisma language server routing', () => { const namespace = findCompletion(completions, 'namespace') return namespace?.kind === vscode.CompletionItemKind.Keyword && namespace.detail === 'PSL declaration keyword' }, - 'workspace-local Prisma 8 declaration completions', + 'Prisma Next declaration completions', ) const namespace = findCompletion(nextCompletions, 'namespace') assert.ok(namespace) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index 9b542e98a4..9e573e2f1d 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -2,8 +2,8 @@ import { isPrismaNextSchema } from '@prisma/language-server/prisma-next' import type { TextDocument, Uri, WorkspaceFolder } from 'vscode' export type DocumentOwner = - | { readonly kind: 'bundled' } - | { readonly kind: 'local'; readonly workspaceFolderUri: string } + | { readonly kind: 'legacy' } + | { readonly kind: 'prisma-next'; readonly workspaceFolderUri: string } | { readonly kind: 'unowned' } export interface DocumentOwnershipPolicy { @@ -17,12 +17,12 @@ export interface DocumentOwnershipWorkspace { export interface DocumentOwnershipTransition { readonly document: TextDocument - readonly previousOwner: DocumentOwner - readonly nextOwner: DocumentOwner + readonly previousSettledOwner: DocumentOwner + readonly nextDesiredOwner: DocumentOwner readonly revision: number } -export type PreparedDocumentOwnerCommit = () => Promise | void +export type PreparedDocumentOwnerCommit = () => Promise | DocumentOwner export type PrepareDocumentOwnerCommit = ( transition: DocumentOwnershipTransition, @@ -31,16 +31,16 @@ export type PrepareDocumentOwnerCommit = ( export interface DocumentOwnershipCoordinatorOptions { readonly workspace: DocumentOwnershipWorkspace readonly policy: DocumentOwnershipPolicy - readonly prepareOwner?: PrepareDocumentOwnerCommit + readonly prepareTransition?: PrepareDocumentOwnerCommit } interface DocumentOwnershipState { revision: number - owner: DocumentOwner + settledOwner: DocumentOwner pending: Promise } -const bundledOwner: DocumentOwner = { kind: 'bundled' } +const legacyOwner: DocumentOwner = { kind: 'legacy' } const unownedOwner: DocumentOwner = { kind: 'unowned' } export class DocumentOwnershipCoordinator { @@ -48,13 +48,13 @@ export class DocumentOwnershipCoordinator { constructor(private readonly options: DocumentOwnershipCoordinatorOptions) {} - classify(document: TextDocument): DocumentOwner { + getDesiredOwner(document: TextDocument): DocumentOwner { if (this.options.policy.isPinnedToPrisma6()) { - return bundledOwner + return legacyOwner } if (!isPrismaNextSchema(document.getText())) { - return bundledOwner + return legacyOwner } if (document.uri.scheme !== 'file' || !this.options.workspace.isTrusted) { @@ -66,15 +66,15 @@ export class DocumentOwnershipCoordinator { return unownedOwner } - return { kind: 'local', workspaceFolderUri: workspaceFolder.uri.toString() } + return { kind: 'prisma-next', workspaceFolderUri: workspaceFolder.uri.toString() } } - getOwner(documentUri: Uri): DocumentOwner { - return this.states.get(documentUri.toString())?.owner ?? unownedOwner + getSettledOwner(documentUri: Uri): DocumentOwner { + return this.states.get(documentUri.toString())?.settledOwner ?? unownedOwner } synchronize(document: TextDocument): Promise { - return this.enqueue(document, (state, revision) => this.commitCurrentOwner(document, state, revision)) + return this.enqueue(document, (state, revision) => this.commitDesiredOwner(document, state, revision)) } close(document: TextDocument): Promise { @@ -103,7 +103,7 @@ export class DocumentOwnershipCoordinator { const state: DocumentOwnershipState = { revision: 0, - owner: unownedOwner, + settledOwner: unownedOwner, pending: Promise.resolve(), } this.states.set(documentUri, state) @@ -116,59 +116,53 @@ export class DocumentOwnershipCoordinator { revision: number, ): Promise { if (revision !== state.revision) { - return state.owner + return state.settledOwner } - const commitOwner = await this.options.prepareOwner?.({ + const commitOwner = await this.options.prepareTransition?.({ document, - previousOwner: state.owner, - nextOwner: unownedOwner, + previousSettledOwner: state.settledOwner, + nextDesiredOwner: unownedOwner, revision, }) if (revision !== state.revision) { - return state.owner + return state.settledOwner } - if (commitOwner) { - await commitOwner() - } - - state.owner = unownedOwner - return unownedOwner + const settledOwner = commitOwner ? await commitOwner() : unownedOwner + state.settledOwner = settledOwner + return settledOwner } - private async commitCurrentOwner( + private async commitDesiredOwner( document: TextDocument, state: DocumentOwnershipState, revision: number, ): Promise { while (revision === state.revision) { - const nextOwner = this.classify(document) - const commitOwner = await this.options.prepareOwner?.({ + const nextDesiredOwner = this.getDesiredOwner(document) + const commitOwner = await this.options.prepareTransition?.({ document, - previousOwner: state.owner, - nextOwner, + previousSettledOwner: state.settledOwner, + nextDesiredOwner, revision, }) if (revision !== state.revision) { - return state.owner + return state.settledOwner } - const currentOwner = this.classify(document) - if (!ownersEqual(currentOwner, nextOwner)) { + const desiredOwner = this.getDesiredOwner(document) + if (!ownersEqual(desiredOwner, nextDesiredOwner)) { continue } - if (commitOwner) { - await commitOwner() - } - - state.owner = currentOwner - return currentOwner + const settledOwner = commitOwner ? await commitOwner() : desiredOwner + state.settledOwner = settledOwner + return settledOwner } - return state.owner + return state.settledOwner } } @@ -176,5 +170,8 @@ function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { if (left.kind !== right.kind) { return false } - return left.kind !== 'local' || (right.kind === 'local' && left.workspaceFolderUri === right.workspaceFolderUri) + return ( + left.kind !== 'prisma-next' || + (right.kind === 'prisma-next' && left.workspaceFolderUri === right.workspaceFolderUri) + ) } diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index e6d18fe7af..75c909337a 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -1,13 +1,13 @@ import type { TextDocument, Uri } from 'vscode' import type { DocumentOwner, DocumentOwnershipCoordinator, PrepareDocumentOwnerCommit } from './documentOwnership' -export interface BundledDocumentSynchronization { +export interface LegacyDocumentSynchronization { openDocument(document: TextDocument): void closeDocument(document: TextDocument): void clearDiagnostics(uri: Uri): void } -export interface LocalDocumentSynchronization { +export interface PrismaNextDocumentSynchronization { ensureClientForDocument(document: TextDocument): Promise openDocument(workspaceFolderUri: string, document: TextDocument): Promise closeDocument(workspaceFolderUri: string, document: TextDocument): Promise @@ -17,56 +17,70 @@ export interface LocalDocumentSynchronization { export interface DocumentRoutingOptions { readonly getOwnership: () => DocumentOwnershipCoordinator readonly isDocumentOpen: (document: TextDocument) => boolean - readonly getBundled: () => BundledDocumentSynchronization - readonly getLocal: () => LocalDocumentSynchronization + readonly getLegacy: () => LegacyDocumentSynchronization + readonly getPrismaNext: () => PrismaNextDocumentSynchronization } export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptions): PrepareDocumentOwnerCommit { - return ({ document, previousOwner, nextOwner }) => { - if (documentOwnersEqual(previousOwner, nextOwner)) return undefined + return ({ document, previousSettledOwner, nextDesiredOwner }) => { + if (documentOwnersEqual(previousSettledOwner, nextDesiredOwner)) return undefined return async () => { - await closePreviousOwner(options, previousOwner, document) + await closePreviousSettledOwner(options, previousSettledOwner, document) - if (!isCurrentOpenCandidate(options, document, nextOwner)) return + if (!isDesiredOpenCandidate(options, document, nextDesiredOwner)) return { kind: 'unowned' } - if (nextOwner.kind === 'bundled') { - options.getBundled().openDocument(document) - } else if (nextOwner.kind === 'local') { - const local = options.getLocal() - const client = await local.ensureClientForDocument(document) - if (client && isCurrentOpenCandidate(options, document, nextOwner)) { - await local.openDocument(nextOwner.workspaceFolderUri, document) + if (nextDesiredOwner.kind === 'legacy') { + options.getLegacy().openDocument(document) + return nextDesiredOwner + } + + if (nextDesiredOwner.kind === 'prisma-next') { + const prismaNext = options.getPrismaNext() + const client = await prismaNext.ensureClientForDocument(document) + if ( + client && + isDesiredOpenCandidate(options, document, nextDesiredOwner) && + (await prismaNext.openDocument(nextDesiredOwner.workspaceFolderUri, document)) + ) { + return nextDesiredOwner } } + + return { kind: 'unowned' } } } } -async function closePreviousOwner( +async function closePreviousSettledOwner( options: DocumentRoutingOptions, - previousOwner: DocumentOwner, + previousSettledOwner: DocumentOwner, document: TextDocument, ): Promise { - if (previousOwner.kind === 'bundled') { - options.getBundled().closeDocument(document) - options.getBundled().clearDiagnostics(document.uri) - } else if (previousOwner.kind === 'local') { - const local = options.getLocal() - await local.closeDocument(previousOwner.workspaceFolderUri, document) - await local.clearDiagnostics(previousOwner.workspaceFolderUri, document.uri) + if (previousSettledOwner.kind === 'legacy') { + options.getLegacy().closeDocument(document) + options.getLegacy().clearDiagnostics(document.uri) + } else if (previousSettledOwner.kind === 'prisma-next') { + const prismaNext = options.getPrismaNext() + await prismaNext.closeDocument(previousSettledOwner.workspaceFolderUri, document) + await prismaNext.clearDiagnostics(previousSettledOwner.workspaceFolderUri, document.uri) } } -function isCurrentOpenCandidate( +function isDesiredOpenCandidate( options: DocumentRoutingOptions, document: TextDocument, candidate: DocumentOwner, ): boolean { - return options.isDocumentOpen(document) && documentOwnersEqual(options.getOwnership().classify(document), candidate) + return ( + options.isDocumentOpen(document) && documentOwnersEqual(options.getOwnership().getDesiredOwner(document), candidate) + ) } export function documentOwnersEqual(left: DocumentOwner, right: DocumentOwner): boolean { if (left.kind !== right.kind) return false - return left.kind !== 'local' || (right.kind === 'local' && left.workspaceFolderUri === right.workspaceFolderUri) + return ( + left.kind !== 'prisma-next' || + (right.kind === 'prisma-next' && left.workspaceFolderUri === right.workspaceFolderUri) + ) } diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 71ff6ddcc0..bb300d4fd7 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -22,33 +22,33 @@ import { CodelensProvider, generateClient } from '../../CodeLensProvider' import * as prisma6Handling from '../../prisma6Handling' import { getPackageJSON } from '../../getPackageJSON' import { DocumentOwnershipCoordinator } from './documentOwnership' -import { createBundledClientMiddleware, type BundledClientMiddleware } from './bundledClientMiddleware' +import { createLegacyClientMiddleware, type LegacyClientMiddleware } from './legacyClientMiddleware' import { createPrepareDocumentRoutingCommit } from './documentRouting' -import { LocalPrismaNextClientRegistry } from './localPrismaNextClientRegistry' -import { BundledClientStartup, deactivateBundledClient } from './bundledClientStartup' +import { PrismaNextClientRegistry } from './prismaNextClientRegistry' +import { LegacyClientStartup, deactivateLegacyClient } from './legacyClientStartup' -let client: LanguageClient -let serverModule: string +let legacyClient: LanguageClient +let legacyServerModule: string let telemetry: TelemetryReporter let fileWatcher: FileWatcher.type | undefined -let bundledClientStartup: BundledClientStartup | undefined +let legacyClientStartup: LegacyClientStartup | undefined const isDebugMode = () => process.env.VSCODE_DEBUG_MODE === 'true' -const logBundledClientError = (error: unknown): void => { - console.error('Bundled Prisma Language Server failed', error) +const logLegacyClientError = (error: unknown): void => { + console.error('Legacy Prisma Language Server failed', error) } -const activateClient = async (context: ExtensionContext, clientOptions: LanguageClientOptions): Promise => { +const activateLegacyClient = async ( + context: ExtensionContext, + legacyClientOptions: LanguageClientOptions, +): Promise => { const prismaConfig = workspace.getConfiguration('prisma') - // Create the language client - const serverOptions = getServerOptions(prismaConfig, context) - client = createLanguageServer(serverOptions, clientOptions) + legacyClient = createLanguageServer(getLegacyServerOptions(prismaConfig, context), legacyClientOptions) - const disposable = client.start() + const disposable = legacyClient.start() - // Start the client. This will also launch the server context.subscriptions.push(disposable) - await client.onReady() + await legacyClient.onReady() } const onFileChange = (filepath: string) => { @@ -118,28 +118,28 @@ const plugin: PrismaVSCodePlugin = { policy: { isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), }, - prepareOwner: createPrepareDocumentRoutingCommit({ + prepareTransition: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, isDocumentOpen: (document) => workspace.textDocuments.includes(document), - getBundled: () => bundledClientMiddleware, - getLocal: () => localClients, + getLegacy: () => legacyClientMiddleware, + getPrismaNext: () => prismaNextClients, }), }) - const localClients = new LocalPrismaNextClientRegistry({ + const prismaNextClients = new PrismaNextClientRegistry({ workspace, ownership, getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), - createClient: (id, name, serverOptions, localClientOptions) => - new LanguageClient(id, name, serverOptions, localClientOptions), + createClient: (id, name, serverOptions, prismaNextClientOptions) => + new LanguageClient(id, name, serverOptions, prismaNextClientOptions), registerDisposable: (disposable) => context.subscriptions.push(disposable), handleStartError: (workspaceFolder, error) => { console.error(`Failed to start Prisma Next Language Server for ${workspaceFolder.uri.toString()}`, error) }, }) - const bundledClientMiddleware: BundledClientMiddleware = createBundledClientMiddleware({ + const legacyClientMiddleware: LegacyClientMiddleware = createLegacyClientMiddleware({ ownership, - getClient: () => client, + getClient: () => legacyClient, getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), handleDiagnosticMessage: (message) => { void prisma6Handling.handleDiagnostic(message, context) @@ -148,36 +148,36 @@ const plugin: PrismaVSCodePlugin = { }) // Options to control the language client - const clientOptions: LanguageClientOptions = { - // Register the server for prisma documents + const legacyClientOptions: LanguageClientOptions = { documentSelector: [{ scheme: 'file', language: 'prisma' }], - middleware: bundledClientMiddleware, + middleware: legacyClientMiddleware, } let started = false - bundledClientStartup?.dispose() - const startup = new BundledClientStartup({ + legacyClientStartup?.dispose() + const startup = new LegacyClientStartup({ isCurrent: (document) => workspace.textDocuments.includes(document), synchronize: (document) => ownership.synchronize(document), - logError: logBundledClientError, + logError: logLegacyClientError, }) - bundledClientStartup = startup - const needsLanguageServer = (doc: TextDocument): boolean => - doc.languageId === 'prisma' && ownership.classify(doc).kind === 'bundled' + legacyClientStartup = startup + const needsLegacyLanguageServer = (document: TextDocument): boolean => + document.languageId === 'prisma' && ownership.getDesiredOwner(document).kind === 'legacy' const synchronizeDocument = (document: TextDocument): void => { if (document.languageId !== 'prisma') return - if (ownership.classify(document).kind === 'bundled') { + if (ownership.getDesiredOwner(document).kind === 'legacy') { startup.schedule(document) } else { - void ownership.synchronize(document).catch(logBundledClientError) + void ownership.synchronize(document).catch(logLegacyClientError) } } const maybeStart = (document?: TextDocument) => { if (started) return - if (document ? !needsLanguageServer(document) : !workspace.textDocuments.some(needsLanguageServer)) return + if (document ? !needsLegacyLanguageServer(document) : !workspace.textDocuments.some(needsLegacyLanguageServer)) + return started = true - startup.start(() => activateClient(context, clientOptions)) + startup.start(() => activateLegacyClient(context, legacyClientOptions)) } const restartLanguageServer = async () => { @@ -185,15 +185,15 @@ const plugin: PrismaVSCodePlugin = { maybeStart() return } - const serverOptions = getServerOptions(workspace.getConfiguration('prisma'), context) - const replacement = restartClient(context, client, serverOptions, clientOptions, { - onClientStopped: () => bundledClientMiddleware.resetClientState(), + const serverOptions = getLegacyServerOptions(workspace.getConfiguration('prisma'), context) + const replacement = restartClient(context, legacyClient, serverOptions, legacyClientOptions, { + onClientStopped: () => legacyClientMiddleware.resetClientState(), onClientCreated: (replacementClient) => { - client = replacementClient + legacyClient = replacementClient }, }) startup.replace(replacement.then(() => undefined)) - client = await replacement + legacyClient = await replacement } context.subscriptions.push( @@ -256,7 +256,7 @@ const plugin: PrismaVSCodePlugin = { }), workspace.onDidCloseTextDocument((document) => { if (document.languageId === 'prisma') { - void ownership.close(document).catch(logBundledClientError) + void ownership.close(document).catch(logLegacyClientError) } }), ) @@ -285,13 +285,13 @@ const plugin: PrismaVSCodePlugin = { checkForMinimalColorTheme() }, deactivate: () => { - const startup = bundledClientStartup - const activeClient = client - bundledClientStartup = undefined - const deactivation = deactivateBundledClient( + const startup = legacyClientStartup + const activeClient = legacyClient + legacyClientStartup = undefined + const deactivation = deactivateLegacyClient( startup, activeClient ? () => activeClient.stop() : undefined, - logBundledClientError, + logLegacyClientError, ) if (activeClient && !isDebugOrTestSession()) { @@ -301,22 +301,21 @@ const plugin: PrismaVSCodePlugin = { }, } -function getServerOptions(prismaConfig: WorkspaceConfiguration, context: ExtensionContext): ServerOptions { +function getLegacyServerOptions(prismaConfig: WorkspaceConfiguration, context: ExtensionContext): ServerOptions { const pinToPrisma6 = prismaConfig.get('pinToPrisma6') if (pinToPrisma6) { - console.log('Using bundled Prisma 6 Language Server') - serverModule = context.asAbsolutePath(path.join('dist/prisma6-language-server/bin.js')) + console.log('Using legacy Prisma 6 Language Server') + legacyServerModule = context.asAbsolutePath(path.join('dist/prisma6-language-server/bin.js')) } else if (isDebugMode()) { - // use Language Server from folder for debugging - console.log('Using local Language Server from filesystem') - serverModule = context.asAbsolutePath(path.join('../../packages/language-server/dist/bin')) + // Use the legacy Language Server from the source tree for debugging. + console.log('Using legacy Language Server from filesystem') + legacyServerModule = context.asAbsolutePath(path.join('../../packages/language-server/dist/bin')) } else { - // use bundled language server - console.log('Using bundled Language Server') - serverModule = context.asAbsolutePath(path.join('dist/language-server/bin.js')) + console.log('Using legacy Language Server') + legacyServerModule = context.asAbsolutePath(path.join('dist/language-server/bin.js')) } - console.log(`serverModule: ${serverModule}`) + console.log(`legacyServerModule: ${legacyServerModule}`) // The debug options for the server // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging @@ -328,9 +327,9 @@ function getServerOptions(prismaConfig: WorkspaceConfiguration, context: Extensi // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used return { - run: { module: serverModule, transport: TransportKind.ipc }, + run: { module: legacyServerModule, transport: TransportKind.ipc }, debug: { - module: serverModule, + module: legacyServerModule, transport: TransportKind.ipc, options: debugOptions, }, diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts similarity index 71% rename from packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts rename to packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts index ee274303e5..6dffcc4ac1 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts @@ -7,7 +7,7 @@ import type { import type { LanguageClient, Middleware } from 'vscode-languageclient/node' import { DocumentOwnershipCoordinator } from './documentOwnership' -export interface BundledClientMiddlewareOptions { +export interface LegacyClientMiddlewareOptions { readonly ownership: DocumentOwnershipCoordinator readonly getClient: () => LanguageClient readonly getDocument: (uri: Uri) => TextDocument | undefined @@ -15,44 +15,44 @@ export interface BundledClientMiddlewareOptions { readonly isSnippetEdit: (action: ProtocolCodeAction, document: TextDocumentIdentifier) => boolean } -export interface BundledClientMiddleware extends Middleware { +export interface LegacyClientMiddleware extends Middleware { openDocument(document: TextDocument): void closeDocument(document: TextDocument): void clearDiagnostics(uri: Uri): void resetClientState(): void } -export function createBundledClientMiddleware(options: BundledClientMiddlewareOptions): BundledClientMiddleware { +export function createLegacyClientMiddleware(options: LegacyClientMiddlewareOptions): LegacyClientMiddleware { let completionDocuments = new WeakMap() - const bundledDocuments = new Set() + const legacyDocuments = new Set() - const isBundledDocument = (document: TextDocument): boolean => { - const committedOwner = options.ownership.getOwner(document.uri) - const currentOwner = options.ownership.classify(document) - return committedOwner.kind === 'bundled' && currentOwner.kind === 'bundled' + const isLegacyDocument = (document: TextDocument): boolean => { + const settledOwner = options.ownership.getSettledOwner(document.uri) + const desiredOwner = options.ownership.getDesiredOwner(document) + return settledOwner.kind === 'legacy' && desiredOwner.kind === 'legacy' } const clearDiagnostics = (uri: Uri): void => { options.getClient().diagnostics?.delete(uri) } - const openBundledDocument = (document: TextDocument): void => { + const openLegacyDocument = (document: TextDocument): void => { const documentUri = document.uri.toString() - if (bundledDocuments.has(documentUri)) return + if (legacyDocuments.has(documentUri)) return - bundledDocuments.add(documentUri) + legacyDocuments.add(documentUri) const client = options.getClient() try { client.sendNotification('textDocument/didOpen', client.code2ProtocolConverter.asOpenTextDocumentParams(document)) } catch (error) { - bundledDocuments.delete(documentUri) + legacyDocuments.delete(documentUri) throw error } } - const closeBundledDocument = (document: TextDocument): void => { + const closeLegacyDocument = (document: TextDocument): void => { const documentUri = document.uri.toString() - if (!bundledDocuments.delete(documentUri)) return + if (!legacyDocuments.delete(documentUri)) return const client = options.getClient() try { @@ -61,41 +61,41 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp client.code2ProtocolConverter.asCloseTextDocumentParams(document), ) } catch (error) { - bundledDocuments.add(documentUri) + legacyDocuments.add(documentUri) throw error } } - const middleware: BundledClientMiddleware = { - openDocument: openBundledDocument, - closeDocument: closeBundledDocument, + const middleware: LegacyClientMiddleware = { + openDocument: openLegacyDocument, + closeDocument: closeLegacyDocument, clearDiagnostics, resetClientState: () => { - bundledDocuments.clear() + legacyDocuments.clear() completionDocuments = new WeakMap() }, didOpen: (document, next) => { const documentUri = document.uri.toString() - if (isBundledDocument(document) && !bundledDocuments.has(documentUri)) { - bundledDocuments.add(documentUri) + if (isLegacyDocument(document) && !legacyDocuments.has(documentUri)) { + legacyDocuments.add(documentUri) next(document) } }, didChange: (event, next) => { const document = event.document - if (isBundledDocument(document) && bundledDocuments.has(document.uri.toString())) { + if (isLegacyDocument(document) && legacyDocuments.has(document.uri.toString())) { next(event) } }, didClose: (document, next) => { - if (bundledDocuments.delete(document.uri.toString())) { + if (legacyDocuments.delete(document.uri.toString())) { next(document) } clearDiagnostics(document.uri) }, handleDiagnostics: (uri, diagnostics, next) => { const document = options.getDocument(uri) - if (!document || !isBundledDocument(document)) { + if (!document || !isLegacyDocument(document)) { next(uri, []) return } @@ -106,7 +106,7 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp next(uri, diagnostics) }, provideCompletionItem: (document, position, context, token, next) => { - if (!isBundledDocument(document)) { + if (!isLegacyDocument(document)) { return undefined } @@ -119,25 +119,24 @@ export function createBundledClientMiddleware(options: BundledClientMiddlewareOp }, resolveCompletionItem: (item, token, next) => { const document = completionDocuments.get(item) - if (!document || !isBundledDocument(document)) { + if (!document || !isLegacyDocument(document)) { return undefined } return next(item, token) }, provideHover: (document, position, token, next) => - isBundledDocument(document) ? next(document, position, token) : undefined, + isLegacyDocument(document) ? next(document, position, token) : undefined, provideDefinition: (document, position, token, next) => - isBundledDocument(document) ? next(document, position, token) : undefined, + isLegacyDocument(document) ? next(document, position, token) : undefined, provideReferences: (document, position, referenceContext, token, next) => - isBundledDocument(document) ? next(document, position, referenceContext, token) : undefined, - provideDocumentSymbols: (document, token, next) => - isBundledDocument(document) ? next(document, token) : undefined, + isLegacyDocument(document) ? next(document, position, referenceContext, token) : undefined, + provideDocumentSymbols: (document, token, next) => (isLegacyDocument(document) ? next(document, token) : undefined), provideDocumentFormattingEdits: (document, formattingOptions, token, next) => - isBundledDocument(document) ? next(document, formattingOptions, token) : undefined, + isLegacyDocument(document) ? next(document, formattingOptions, token) : undefined, provideRenameEdits: (document, position, newName, token, next) => - isBundledDocument(document) ? next(document, position, newName, token) : undefined, + isLegacyDocument(document) ? next(document, position, newName, token) : undefined, provideCodeActions: async (document, range, context, token) => { - if (!isBundledDocument(document)) { + if (!isLegacyDocument(document)) { return undefined } diff --git a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts b/packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts similarity index 84% rename from packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts rename to packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts index 650e07a99b..7cddd9ca2c 100644 --- a/packages/vscode/src/plugins/prisma-language-server/bundledClientStartup.ts +++ b/packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts @@ -1,13 +1,13 @@ -export type BundledClientStartupStatus = 'idle' | 'starting' | 'ready' | 'failed' | 'disposed' +export type LegacyClientStartupStatus = 'idle' | 'starting' | 'ready' | 'failed' | 'disposed' -export interface BundledClientStartupOptions { +export interface LegacyClientStartupOptions { readonly isCurrent: (value: T) => boolean readonly synchronize: (value: T) => Promise readonly logError: (error: unknown) => void } -export async function deactivateBundledClient( - startup: BundledClientStartup | undefined, +export async function deactivateLegacyClient( + startup: LegacyClientStartup | undefined, stop: (() => Promise) | undefined, logError: (error: unknown) => void, ): Promise { @@ -21,15 +21,15 @@ export async function deactivateBundledClient( } } -export class BundledClientStartup { +export class LegacyClientStartup { private generation = 0 private readiness: Promise = Promise.resolve(false) private readonly pending = new Map() - private currentStatus: BundledClientStartupStatus = 'idle' + private currentStatus: LegacyClientStartupStatus = 'idle' - constructor(private readonly options: BundledClientStartupOptions) {} + constructor(private readonly options: LegacyClientStartupOptions) {} - get status(): BundledClientStartupStatus { + get status(): LegacyClientStartupStatus { return this.currentStatus } diff --git a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts similarity index 87% rename from packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts rename to packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts index aa5af3b736..7b7e49a7ee 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts @@ -2,31 +2,33 @@ import type { CompletionItem, CompletionList, ProviderResult, TextDocument, Uri import type { LanguageClient, Middleware } from 'vscode-languageclient/node' import type { DocumentOwnershipCoordinator } from './documentOwnership' -export interface LocalClientMiddlewareOptions { +export interface PrismaNextClientMiddlewareOptions { readonly workspaceFolderUri: string readonly ownership: DocumentOwnershipCoordinator readonly getClient: () => LanguageClient readonly getDocument: (uri: Uri) => TextDocument | undefined } -export interface LocalClientMiddleware extends Middleware { +export interface PrismaNextClientMiddleware extends Middleware { openDocument(document: TextDocument): void closeDocument(document: TextDocument): void clearDiagnostics(uri: Uri): void } -export function createLocalClientMiddleware(options: LocalClientMiddlewareOptions): LocalClientMiddleware { +export function createPrismaNextClientMiddleware( + options: PrismaNextClientMiddlewareOptions, +): PrismaNextClientMiddleware { const synchronizedDocuments = new Set() const completionDocuments = new WeakMap() const isOwnedDocument = (document: TextDocument): boolean => { - const committedOwner = options.ownership.getOwner(document.uri) - const currentOwner = options.ownership.classify(document) + const settledOwner = options.ownership.getSettledOwner(document.uri) + const desiredOwner = options.ownership.getDesiredOwner(document) return ( - committedOwner.kind === 'local' && - currentOwner.kind === 'local' && - committedOwner.workspaceFolderUri === options.workspaceFolderUri && - currentOwner.workspaceFolderUri === options.workspaceFolderUri + settledOwner.kind === 'prisma-next' && + desiredOwner.kind === 'prisma-next' && + settledOwner.workspaceFolderUri === options.workspaceFolderUri && + desiredOwner.workspaceFolderUri === options.workspaceFolderUri ) } @@ -64,7 +66,7 @@ export function createLocalClientMiddleware(options: LocalClientMiddlewareOption } } - const middleware: LocalClientMiddleware = { + const middleware: PrismaNextClientMiddleware = { openDocument, closeDocument, clearDiagnostics, diff --git a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts similarity index 79% rename from packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts rename to packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts index 6f6fba57fb..299b4abf9d 100644 --- a/packages/vscode/src/plugins/prisma-language-server/localPrismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts @@ -5,30 +5,30 @@ import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' import type { LanguageClientOptions } from 'vscode-languageclient' import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' import type { DocumentOwnershipCoordinator } from './documentOwnership' -import { createLocalClientMiddleware, type LocalClientMiddleware } from './localClientMiddleware' +import { createPrismaNextClientMiddleware, type PrismaNextClientMiddleware } from './prismaNextClientMiddleware' const prismaCliRelativePath = ['node_modules', 'prisma', 'dist', 'prisma.js'] as const -export interface LocalPrismaNextClientRegistryWorkspace { +export interface PrismaNextClientRegistryWorkspace { readonly isTrusted: boolean getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined } -export type SpawnLocalPrismaNextProcess = ( +export type SpawnPrismaNextProcess = ( executable: string, args: string[], options: SpawnOptionsWithoutStdio, ) => ChildProcessWithoutNullStreams -export interface LocalPrismaNextLauncherOptions { +export interface PrismaNextLauncherOptions { readonly executable?: string readonly environment?: NodeJS.ProcessEnv - readonly spawnProcess?: SpawnLocalPrismaNextProcess + readonly spawnProcess?: SpawnPrismaNextProcess readonly handleProcessError?: (error: Error) => void } -export interface LocalPrismaNextClientRegistryOptions { - readonly workspace: LocalPrismaNextClientRegistryWorkspace +export interface PrismaNextClientRegistryOptions { + readonly workspace: PrismaNextClientRegistryWorkspace readonly ownership: DocumentOwnershipCoordinator readonly getDocument: (uri: Uri) => TextDocument | undefined readonly createClient: ( @@ -40,18 +40,18 @@ export interface LocalPrismaNextClientRegistryOptions { readonly registerDisposable: (disposable: Disposable) => void readonly entrypointExists?: (entrypoint: string) => Promise readonly handleStartError?: (workspaceFolder: WorkspaceFolder, error: unknown) => void - readonly launcher?: Omit + readonly launcher?: Omit } -interface LocalPrismaNextClientEntry { +interface PrismaNextClientEntry { readonly client: LanguageClient - readonly middleware: LocalClientMiddleware + readonly middleware: PrismaNextClientMiddleware } -export class LocalPrismaNextClientRegistry { - private readonly clients = new Map>() +export class PrismaNextClientRegistry { + private readonly clients = new Map>() - constructor(private readonly options: LocalPrismaNextClientRegistryOptions) {} + constructor(private readonly options: PrismaNextClientRegistryOptions) {} ensureClientForDocument(document: TextDocument): Promise { if (!this.options.workspace.isTrusted || document.uri.scheme !== 'file') { @@ -85,7 +85,7 @@ export class LocalPrismaNextClientRegistry { entry?.middleware.clearDiagnostics(uri) } - private ensureClient(workspaceFolder: WorkspaceFolder): Promise { + private ensureClient(workspaceFolder: WorkspaceFolder): Promise { const workspaceFolderUri = workspaceFolder.uri.toString() const existing = this.clients.get(workspaceFolderUri) if (existing) { @@ -97,8 +97,8 @@ export class LocalPrismaNextClientRegistry { return pending } - private async discoverAndStart(workspaceFolder: WorkspaceFolder): Promise { - const entrypoint = getLocalPrismaNextEntrypoint(workspaceFolder) + private async discoverAndStart(workspaceFolder: WorkspaceFolder): Promise { + const entrypoint = getPrismaNextEntrypoint(workspaceFolder) try { const exists = await (this.options.entrypointExists ?? isFile)(entrypoint) @@ -107,7 +107,7 @@ export class LocalPrismaNextClientRegistry { } const workspaceFolderUri = workspaceFolder.uri.toString() - const middleware = createLocalClientMiddleware({ + const middleware = createPrismaNextClientMiddleware({ workspaceFolderUri, ownership: this.options.ownership, getClient: () => client, @@ -116,11 +116,11 @@ export class LocalPrismaNextClientRegistry { const client = this.options.createClient( `prisma-next:${workspaceFolderUri}`, `Prisma Next Language Server (${workspaceFolder.name})`, - createLocalPrismaNextServerOptions(workspaceFolder, entrypoint, { + createPrismaNextServerOptions(workspaceFolder, entrypoint, { ...this.options.launcher, handleProcessError: (error) => this.options.handleStartError?.(workspaceFolder, error), }), - createLocalPrismaNextClientOptions(workspaceFolder, middleware), + createPrismaNextClientOptions(workspaceFolder, middleware), ) this.options.registerDisposable(client.start()) await client.onReady() @@ -132,17 +132,17 @@ export class LocalPrismaNextClientRegistry { } } -export function getLocalPrismaNextEntrypoint(workspaceFolder: WorkspaceFolder): string { +export function getPrismaNextEntrypoint(workspaceFolder: WorkspaceFolder): string { return path.join(workspaceFolder.uri.fsPath, ...prismaCliRelativePath) } -export function createLocalPrismaNextServerOptions( +export function createPrismaNextServerOptions( workspaceFolder: WorkspaceFolder, - entrypoint = getLocalPrismaNextEntrypoint(workspaceFolder), - launcher: LocalPrismaNextLauncherOptions = {}, + entrypoint = getPrismaNextEntrypoint(workspaceFolder), + launcher: PrismaNextLauncherOptions = {}, ): ServerOptions { return () => - launchLocalPrismaNextServer({ + launchPrismaNextServer({ executable: launcher.executable ?? process.execPath, entrypoint, cwd: workspaceFolder.uri.fsPath, @@ -152,16 +152,16 @@ export function createLocalPrismaNextServerOptions( }) } -export interface LaunchLocalPrismaNextServerOptions { +export interface LaunchPrismaNextServerOptions { readonly executable: string readonly entrypoint: string readonly cwd: string readonly environment: NodeJS.ProcessEnv - readonly spawnProcess: SpawnLocalPrismaNextProcess + readonly spawnProcess: SpawnPrismaNextProcess readonly handleProcessError?: (error: Error) => void } -export function launchLocalPrismaNextServer(options: LaunchLocalPrismaNextServerOptions): Promise { +export function launchPrismaNextServer(options: LaunchPrismaNextServerOptions): Promise { return new Promise((resolve, reject) => { const child = options.spawnProcess(options.executable, [options.entrypoint, 'lsp'], { cwd: options.cwd, @@ -215,9 +215,9 @@ function destroyProcessStreams(child: ChildProcessWithoutNullStreams): void { child.stderr.destroy() } -export function createLocalPrismaNextClientOptions( +export function createPrismaNextClientOptions( workspaceFolder: WorkspaceFolder, - middleware: LocalClientMiddleware, + middleware: PrismaNextClientMiddleware, ): LanguageClientOptions { const rootPath = workspaceFolder.uri.fsPath.split('\\').join('/') const normalizedRoot = rootPath.endsWith('/') ? rootPath.slice(0, -1) : rootPath diff --git a/packages/vscode/tests/fixtures/integration-workspace/root-a/bundled.prisma b/packages/vscode/tests/fixtures/integration-workspace/root-a/legacy.prisma similarity index 100% rename from packages/vscode/tests/fixtures/integration-workspace/root-a/bundled.prisma rename to packages/vscode/tests/fixtures/integration-workspace/root-a/legacy.prisma From 0d21c2036c6941ba214b631dfe09af178c96a844 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 13:45:40 +0000 Subject: [PATCH 36/43] fix(vscode): preserve ownership through restart failures --- docs/language-server.md | 6 +- packages/vscode/.vscodeignore | 2 +- packages/vscode/esbuild.mjs | 10 +-- .../documentOwnership.ts | 38 ++++++---- .../prisma-language-server/documentRouting.ts | 74 ++++++++++++++----- .../plugins/prisma-language-server/index.ts | 71 +++++++++++++++--- .../legacyClientMiddleware.ts | 17 ++++- .../prismaNextClientMiddleware.ts | 17 ++++- packages/vscode/src/util.ts | 9 ++- .../src/workers/prisma6-language-server.ts | 4 +- 10 files changed, 184 insertions(+), 64 deletions(-) diff --git a/docs/language-server.md b/docs/language-server.md index 49acf7305c..13e84c8b9b 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -58,9 +58,11 @@ The directive is content based and applies per file. A marked file does not opt 6. Open the complete current document on the surviving owner. 7. Record the successfully synchronized candidate as the settled owner, or `unowned` if no candidate opened. -A close event invalidates pending revisions immediately and queues final cleanup. Its settled owner becomes `unowned` only after the cleanup commit closure completes. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. +A close event invalidates pending revisions immediately and queues final cleanup. Its settled owner becomes `unowned` only after the cleanup commit closure completes. Commit closures return a structured outcome containing the owner established by their completed stages and an optional operational error. A failed prior-owner close keeps the prior settled owner because middleware restores its ledger; once close succeeds, a diagnostic-clear or candidate-open failure settles `unowned`. The coordinator records that outcome before surfacing the error, so later per-URI work observes the established state and the queue continues. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. -Legacy and Prisma Next middleware maintain ledgers of documents actually synchronized to their client. Raw editor notifications, feature requests, and diagnostics are forwarded only when `getSettledOwner(document.uri)` and `getDesiredOwner(document)` agree on the middleware's expected identity and, for Prisma Next, the exact workspace root. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use this same gate. Automatic Prisma Next client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to Prisma Next over LSP. +Legacy and Prisma Next middleware maintain ledgers of documents actually synchronized to their client. Ledger insertion and removal are rolled back when notification dispatch throws synchronously. Raw editor notifications, feature requests, and diagnostics are forwarded only when `getSettledOwner(document.uri)` and `getDesiredOwner(document)` agree on the middleware's expected identity and, for Prisma Next, the exact workspace root. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use this same gate. Automatic Prisma Next client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to Prisma Next over LSP. + +A legacy-client restart first marks legacy service temporarily unavailable, making legacy documents desired `unowned` so requests are gated immediately. It then serializes every document settled to legacy through coordinator close and cleanup, stops the old client, resets middleware state, and publishes and starts the replacement. Once the replacement is ready, legacy availability is restored and every currently open Prisma document is explicitly reconciled. Legacy ownership settles only after its explicit replacement-client open succeeds. Unaffected Prisma Next documents remain settled to their existing exact-root client; policy changes are applied during the all-document reconciliation. ### Workspace-root Prisma Next launch contract diff --git a/packages/vscode/.vscodeignore b/packages/vscode/.vscodeignore index 4446585737..ad5cbcd354 100644 --- a/packages/vscode/.vscodeignore +++ b/packages/vscode/.vscodeignore @@ -32,7 +32,7 @@ fixtures/** # Keep these files (not ignored): # - dist/extension.js (bundled extension) -# - dist/language-server/** (bundled language server) +# - dist/language-server/** (legacy language server) # - dist/node_modules/** (copied static assets) # - syntaxes/** (TextMate grammars) # - language-configuration.json diff --git a/packages/vscode/esbuild.mjs b/packages/vscode/esbuild.mjs index b26081d3dc..ef50b5f7fa 100644 --- a/packages/vscode/esbuild.mjs +++ b/packages/vscode/esbuild.mjs @@ -107,7 +107,7 @@ const languageServerConfig = { /** * Configuration for the Prisma 6 Language Server. - * This is bundled separately and used when pinToPrisma6 is enabled. + * This legacy server is built separately and used when pinToPrisma6 is enabled. * @type {import('esbuild').BuildOptions} */ const prisma6LanguageServerConfig = { @@ -263,8 +263,8 @@ function copyStaticAssets() { cpSync(studioSrc, studioDest, { recursive: true, dereference: true }) // Copy prisma-schema-wasm WASM file to Prisma 6 language server directory - // The WASM is loaded via __dirname in the bundled code, so it needs to be - // in the same directory as the bundled Prisma 6 language server bin.js + // The WASM is loaded via __dirname in the legacy server code, so it needs to be + // in the same directory as the legacy Prisma 6 language server bin.js. // Note: We need to find the Prisma 6 version specifically since there are // two versions (Prisma 6 and Prisma 7) of @prisma/prisma-schema-wasm const prisma6LsDistDir = join(__dirname, 'dist/prisma6-language-server') @@ -292,8 +292,8 @@ function copyStaticAssets() { cpSync(prisma6WasmSrc, join(prisma6LsDistDir, 'prisma_schema_build_bg.wasm')) // Copy prisma-schema-wasm WASM file to language server directory - // The WASM is loaded via __dirname in the bundled code, so it needs to be - // in the same directory as the bundled language server bin.js + // The WASM is loaded via __dirname in the legacy server code, so it needs to be + // in the same directory as the legacy language server bin.js. const lsDistDir = join(__dirname, 'dist/language-server') mkdirSync(lsDistDir, { recursive: true }) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index 9e573e2f1d..68a35406ed 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -8,6 +8,7 @@ export type DocumentOwner = export interface DocumentOwnershipPolicy { isPinnedToPrisma6(): boolean + isLegacyUnavailable?(): boolean } export interface DocumentOwnershipWorkspace { @@ -22,7 +23,12 @@ export interface DocumentOwnershipTransition { readonly revision: number } -export type PreparedDocumentOwnerCommit = () => Promise | DocumentOwner +export interface DocumentOwnerCommitOutcome { + readonly settledOwner: DocumentOwner + readonly error?: unknown +} + +export type PreparedDocumentOwnerCommit = () => Promise | DocumentOwnerCommitOutcome export type PrepareDocumentOwnerCommit = ( transition: DocumentOwnershipTransition, @@ -49,12 +55,8 @@ export class DocumentOwnershipCoordinator { constructor(private readonly options: DocumentOwnershipCoordinatorOptions) {} getDesiredOwner(document: TextDocument): DocumentOwner { - if (this.options.policy.isPinnedToPrisma6()) { - return legacyOwner - } - - if (!isPrismaNextSchema(document.getText())) { - return legacyOwner + if (this.options.policy.isPinnedToPrisma6() || !isPrismaNextSchema(document.getText())) { + return this.options.policy.isLegacyUnavailable?.() ? unownedOwner : legacyOwner } if (document.uri.scheme !== 'file' || !this.options.workspace.isTrusted) { @@ -119,7 +121,7 @@ export class DocumentOwnershipCoordinator { return state.settledOwner } - const commitOwner = await this.options.prepareTransition?.({ + const commitTransition = await this.options.prepareTransition?.({ document, previousSettledOwner: state.settledOwner, nextDesiredOwner: unownedOwner, @@ -129,9 +131,8 @@ export class DocumentOwnershipCoordinator { return state.settledOwner } - const settledOwner = commitOwner ? await commitOwner() : unownedOwner - state.settledOwner = settledOwner - return settledOwner + const outcome = commitTransition ? await commitTransition() : { settledOwner: unownedOwner } + return this.recordCommitOutcome(state, outcome) } private async commitDesiredOwner( @@ -141,7 +142,7 @@ export class DocumentOwnershipCoordinator { ): Promise { while (revision === state.revision) { const nextDesiredOwner = this.getDesiredOwner(document) - const commitOwner = await this.options.prepareTransition?.({ + const commitTransition = await this.options.prepareTransition?.({ document, previousSettledOwner: state.settledOwner, nextDesiredOwner, @@ -157,13 +158,20 @@ export class DocumentOwnershipCoordinator { continue } - const settledOwner = commitOwner ? await commitOwner() : desiredOwner - state.settledOwner = settledOwner - return settledOwner + const outcome = commitTransition ? await commitTransition() : { settledOwner: desiredOwner } + return this.recordCommitOutcome(state, outcome) } return state.settledOwner } + + private recordCommitOutcome(state: DocumentOwnershipState, outcome: DocumentOwnerCommitOutcome): DocumentOwner { + state.settledOwner = outcome.settledOwner + if (outcome.error !== undefined) { + throw outcome.error + } + return outcome.settledOwner + } } function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index 75c909337a..965e549bcf 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -1,5 +1,10 @@ import type { TextDocument, Uri } from 'vscode' -import type { DocumentOwner, DocumentOwnershipCoordinator, PrepareDocumentOwnerCommit } from './documentOwnership' +import type { + DocumentOwner, + DocumentOwnerCommitOutcome, + DocumentOwnershipCoordinator, + PrepareDocumentOwnerCommit, +} from './documentOwnership' export interface LegacyDocumentSynchronization { openDocument(document: TextDocument): void @@ -26,28 +31,48 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio if (documentOwnersEqual(previousSettledOwner, nextDesiredOwner)) return undefined return async () => { - await closePreviousSettledOwner(options, previousSettledOwner, document) + try { + await closePreviousSettledOwner(options, previousSettledOwner, document) + } catch (error) { + return commitOutcome(previousSettledOwner, error) + } + + try { + await clearPreviousSettledOwnerDiagnostics(options, previousSettledOwner, document.uri) + } catch (error) { + return commitOutcome({ kind: 'unowned' }, error) + } - if (!isDesiredOpenCandidate(options, document, nextDesiredOwner)) return { kind: 'unowned' } + if (!isDesiredOpenCandidate(options, document, nextDesiredOwner)) { + return commitOutcome({ kind: 'unowned' }) + } if (nextDesiredOwner.kind === 'legacy') { - options.getLegacy().openDocument(document) - return nextDesiredOwner + try { + options.getLegacy().openDocument(document) + return commitOutcome(nextDesiredOwner) + } catch (error) { + return commitOutcome({ kind: 'unowned' }, error) + } } if (nextDesiredOwner.kind === 'prisma-next') { - const prismaNext = options.getPrismaNext() - const client = await prismaNext.ensureClientForDocument(document) - if ( - client && - isDesiredOpenCandidate(options, document, nextDesiredOwner) && - (await prismaNext.openDocument(nextDesiredOwner.workspaceFolderUri, document)) - ) { - return nextDesiredOwner + try { + const prismaNext = options.getPrismaNext() + const client = await prismaNext.ensureClientForDocument(document) + if ( + client && + isDesiredOpenCandidate(options, document, nextDesiredOwner) && + (await prismaNext.openDocument(nextDesiredOwner.workspaceFolderUri, document)) + ) { + return commitOutcome(nextDesiredOwner) + } + } catch (error) { + return commitOutcome({ kind: 'unowned' }, error) } } - return { kind: 'unowned' } + return commitOutcome({ kind: 'unowned' }) } } } @@ -59,14 +84,27 @@ async function closePreviousSettledOwner( ): Promise { if (previousSettledOwner.kind === 'legacy') { options.getLegacy().closeDocument(document) - options.getLegacy().clearDiagnostics(document.uri) } else if (previousSettledOwner.kind === 'prisma-next') { - const prismaNext = options.getPrismaNext() - await prismaNext.closeDocument(previousSettledOwner.workspaceFolderUri, document) - await prismaNext.clearDiagnostics(previousSettledOwner.workspaceFolderUri, document.uri) + await options.getPrismaNext().closeDocument(previousSettledOwner.workspaceFolderUri, document) } } +async function clearPreviousSettledOwnerDiagnostics( + options: DocumentRoutingOptions, + previousSettledOwner: DocumentOwner, + documentUri: Uri, +): Promise { + if (previousSettledOwner.kind === 'legacy') { + options.getLegacy().clearDiagnostics(documentUri) + } else if (previousSettledOwner.kind === 'prisma-next') { + await options.getPrismaNext().clearDiagnostics(previousSettledOwner.workspaceFolderUri, documentUri) + } +} + +function commitOutcome(settledOwner: DocumentOwner, error?: unknown): DocumentOwnerCommitOutcome { + return error === undefined ? { settledOwner } : { settledOwner, error } +} + function isDesiredOpenCandidate( options: DocumentRoutingOptions, document: TextDocument, diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index bb300d4fd7..60b80160c3 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -13,7 +13,7 @@ import { isDebugOrTestSession, isSnippetEdit, restartClient, - createLanguageServer, + createLegacyLanguageServer, } from '../../util' import { PrismaVSCodePlugin } from '../types' import paths from 'env-paths' @@ -43,7 +43,7 @@ const activateLegacyClient = async ( legacyClientOptions: LanguageClientOptions, ): Promise => { const prismaConfig = workspace.getConfiguration('prisma') - legacyClient = createLanguageServer(getLegacyServerOptions(prismaConfig, context), legacyClientOptions) + legacyClient = createLegacyLanguageServer(getLegacyServerOptions(prismaConfig, context), legacyClientOptions) const disposable = legacyClient.start() @@ -113,10 +113,13 @@ const plugin: PrismaVSCodePlugin = { setGenerateWatcher(!!workspace.getConfiguration('prisma').get('fileWatcher')) + let legacyUnavailable = false + let restarting = false const ownership: DocumentOwnershipCoordinator = new DocumentOwnershipCoordinator({ workspace, policy: { isPinnedToPrisma6: () => !!workspace.getConfiguration('prisma').get('pinToPrisma6'), + isLegacyUnavailable: () => legacyUnavailable, }, prepareTransition: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, @@ -164,7 +167,7 @@ const plugin: PrismaVSCodePlugin = { const needsLegacyLanguageServer = (document: TextDocument): boolean => document.languageId === 'prisma' && ownership.getDesiredOwner(document).kind === 'legacy' const synchronizeDocument = (document: TextDocument): void => { - if (document.languageId !== 'prisma') return + if (restarting || document.languageId !== 'prisma') return if (ownership.getDesiredOwner(document).kind === 'legacy') { startup.schedule(document) } else { @@ -180,20 +183,64 @@ const plugin: PrismaVSCodePlugin = { startup.start(() => activateLegacyClient(context, legacyClientOptions)) } + const getOpenPrismaDocuments = (): TextDocument[] => + workspace.textDocuments.filter((document) => document.languageId === 'prisma') + + const waitForOwnershipOperations = async (operations: readonly Promise[]): Promise => { + const results = await Promise.allSettled(operations) + const failure = results.find((result): result is PromiseRejectedResult => result.status === 'rejected') + if (failure) throw failure.reason + } + + const synchronizeDocuments = (documents: readonly TextDocument[]): Promise => + waitForOwnershipOperations(documents.map((document) => ownership.synchronize(document))) + + const invalidateLegacyDocuments = (): Promise => { + const legacyDocuments = getOpenPrismaDocuments().filter( + (document) => ownership.getSettledOwner(document.uri).kind === 'legacy', + ) + return waitForOwnershipOperations(legacyDocuments.map((document) => ownership.close(document))) + } + const restartLanguageServer = async () => { if (!started) { maybeStart() + for (const document of getOpenPrismaDocuments()) synchronizeDocument(document) return } - const serverOptions = getLegacyServerOptions(workspace.getConfiguration('prisma'), context) - const replacement = restartClient(context, legacyClient, serverOptions, legacyClientOptions, { - onClientStopped: () => legacyClientMiddleware.resetClientState(), - onClientCreated: (replacementClient) => { - legacyClient = replacementClient - }, - }) - startup.replace(replacement.then(() => undefined)) - legacyClient = await replacement + + restarting = true + legacyUnavailable = true + try { + try { + await invalidateLegacyDocuments() + } catch (error) { + legacyUnavailable = false + try { + await synchronizeDocuments(getOpenPrismaDocuments()) + } catch (restoreError) { + logLegacyClientError(restoreError) + } + throw error + } + + const serverOptions = getLegacyServerOptions(workspace.getConfiguration('prisma'), context) + const replacement = restartClient(context, legacyClient, serverOptions, legacyClientOptions, { + onClientStopped: () => legacyClientMiddleware.resetClientState(), + onClientCreated: (replacementClient) => { + legacyClient = replacementClient + }, + }).then(async (replacementClient) => { + legacyUnavailable = false + await synchronizeDocuments(getOpenPrismaDocuments()) + return replacementClient + }) + startup.replace(replacement.then(() => undefined)) + legacyClient = await replacement + } finally { + legacyUnavailable = false + restarting = false + } } context.subscriptions.push( diff --git a/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts index 6dffcc4ac1..38685325ad 100644 --- a/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts @@ -78,7 +78,12 @@ export function createLegacyClientMiddleware(options: LegacyClientMiddlewareOpti const documentUri = document.uri.toString() if (isLegacyDocument(document) && !legacyDocuments.has(documentUri)) { legacyDocuments.add(documentUri) - next(document) + try { + next(document) + } catch (error) { + legacyDocuments.delete(documentUri) + throw error + } } }, didChange: (event, next) => { @@ -88,8 +93,14 @@ export function createLegacyClientMiddleware(options: LegacyClientMiddlewareOpti } }, didClose: (document, next) => { - if (legacyDocuments.delete(document.uri.toString())) { - next(document) + const documentUri = document.uri.toString() + if (legacyDocuments.delete(documentUri)) { + try { + next(document) + } catch (error) { + legacyDocuments.add(documentUri) + throw error + } } clearDiagnostics(document.uri) }, diff --git a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts index 7b7e49a7ee..3492f8abe0 100644 --- a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts @@ -74,7 +74,12 @@ export function createPrismaNextClientMiddleware( const documentUri = document.uri.toString() if (isOwnedDocument(document) && !synchronizedDocuments.has(documentUri)) { synchronizedDocuments.add(documentUri) - next(document) + try { + next(document) + } catch (error) { + synchronizedDocuments.delete(documentUri) + throw error + } } }, didChange: (event, next) => { @@ -83,8 +88,14 @@ export function createPrismaNextClientMiddleware( } }, didClose: (document, next) => { - if (synchronizedDocuments.delete(document.uri.toString())) { - next(document) + const documentUri = document.uri.toString() + if (synchronizedDocuments.delete(documentUri)) { + try { + next(document) + } catch (error) { + synchronizedDocuments.add(documentUri) + throw error + } } clearDiagnostics(document.uri) }, diff --git a/packages/vscode/src/util.ts b/packages/vscode/src/util.ts index d9601e135c..fa05227d98 100644 --- a/packages/vscode/src/util.ts +++ b/packages/vscode/src/util.ts @@ -106,11 +106,14 @@ export function applySnippetWorkspaceEdit(): (edit: WorkspaceEdit) => Promise Date: Tue, 25 Aug 2026 13:58:24 +0000 Subject: [PATCH 37/43] fix(vscode): serialize legacy server restarts --- docs/language-server.md | 6 ++- .../prisma-language-server/documentRouting.ts | 26 ++++------ .../plugins/prisma-language-server/index.ts | 49 +++++++++++++------ packages/vscode/src/util.ts | 2 +- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/docs/language-server.md b/docs/language-server.md index 13e84c8b9b..ad0668bea9 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -58,11 +58,13 @@ The directive is content based and applies per file. A marked file does not opt 6. Open the complete current document on the surviving owner. 7. Record the successfully synchronized candidate as the settled owner, or `unowned` if no candidate opened. -A close event invalidates pending revisions immediately and queues final cleanup. Its settled owner becomes `unowned` only after the cleanup commit closure completes. Commit closures return a structured outcome containing the owner established by their completed stages and an optional operational error. A failed prior-owner close keeps the prior settled owner because middleware restores its ledger; once close succeeds, a diagnostic-clear or candidate-open failure settles `unowned`. The coordinator records that outcome before surfacing the error, so later per-URI work observes the established state and the queue continues. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. +A close event invalidates pending revisions immediately and queues final cleanup. Its settled owner becomes `unowned` only after the cleanup commit closure completes. Commit closures return a structured outcome containing the owner established by their completed stages and an optional operational error. A failed prior-owner close keeps the prior settled owner because middleware restores its ledger. Once close succeeds, diagnostic clearing, desired-owner validation, ownership lookup, client startup, and candidate open share one outcome-producing error boundary: any failure settles `unowned` before the coordinator surfaces the error. Later per-URI work therefore observes the established state and the queue continues. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. Legacy and Prisma Next middleware maintain ledgers of documents actually synchronized to their client. Ledger insertion and removal are rolled back when notification dispatch throws synchronously. Raw editor notifications, feature requests, and diagnostics are forwarded only when `getSettledOwner(document.uri)` and `getDesiredOwner(document)` agree on the middleware's expected identity and, for Prisma Next, the exact workspace root. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use this same gate. Automatic Prisma Next client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to Prisma Next over LSP. -A legacy-client restart first marks legacy service temporarily unavailable, making legacy documents desired `unowned` so requests are gated immediately. It then serializes every document settled to legacy through coordinator close and cleanup, stops the old client, resets middleware state, and publishes and starts the replacement. Once the replacement is ready, legacy availability is restored and every currently open Prisma document is explicitly reconciled. Legacy ownership settles only after its explicit replacement-client open succeeds. Unaffected Prisma Next documents remain settled to their existing exact-root client; policy changes are applied during the all-document reconciliation. +Restart, pin, and unpin operations enter one shared promise queue before reading or changing Prisma pin configuration. Each caller receives its own operation result, while a failure is absorbed only by the queue tail so later operations still run. This serialization prevents one operation from resetting another's restart or legacy-availability flags. + +Within its queue slot, a legacy-client restart first marks legacy service temporarily unavailable, making legacy documents desired `unowned` so requests are gated immediately. It then serializes every document settled to legacy through coordinator close and cleanup, stops the old client, resets middleware state, and publishes and starts the replacement. Once the replacement is ready, legacy availability is restored and every currently open Prisma document is explicitly reconciled. Legacy ownership settles only after its explicit replacement-client open succeeds. Unaffected Prisma Next documents remain settled to their existing exact-root client; policy changes are applied during the all-document reconciliation. Replacement clients retain the stable `prisma` client ID for `prisma.trace.server` compatibility while using `Prisma Legacy Language Server` as their display and output-channel identity. ### Workspace-root Prisma Next launch contract diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index 965e549bcf..9c4a07bef6 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -39,25 +39,17 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio try { await clearPreviousSettledOwnerDiagnostics(options, previousSettledOwner, document.uri) - } catch (error) { - return commitOutcome({ kind: 'unowned' }, error) - } - if (!isDesiredOpenCandidate(options, document, nextDesiredOwner)) { - return commitOutcome({ kind: 'unowned' }) - } + if (!isDesiredOpenCandidate(options, document, nextDesiredOwner)) { + return commitOutcome({ kind: 'unowned' }) + } - if (nextDesiredOwner.kind === 'legacy') { - try { + if (nextDesiredOwner.kind === 'legacy') { options.getLegacy().openDocument(document) return commitOutcome(nextDesiredOwner) - } catch (error) { - return commitOutcome({ kind: 'unowned' }, error) } - } - if (nextDesiredOwner.kind === 'prisma-next') { - try { + if (nextDesiredOwner.kind === 'prisma-next') { const prismaNext = options.getPrismaNext() const client = await prismaNext.ensureClientForDocument(document) if ( @@ -67,12 +59,12 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio ) { return commitOutcome(nextDesiredOwner) } - } catch (error) { - return commitOutcome({ kind: 'unowned' }, error) } - } - return commitOutcome({ kind: 'unowned' }) + return commitOutcome({ kind: 'unowned' }) + } catch (error) { + return commitOutcome({ kind: 'unowned' }, error) + } } } } diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 60b80160c3..28a8226c72 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -157,6 +157,7 @@ const plugin: PrismaVSCodePlugin = { } let started = false + let legacyReadiness: Promise | undefined legacyClientStartup?.dispose() const startup = new LegacyClientStartup({ isCurrent: (document) => workspace.textDocuments.includes(document), @@ -175,12 +176,14 @@ const plugin: PrismaVSCodePlugin = { } } - const maybeStart = (document?: TextDocument) => { + const maybeStart = (document?: TextDocument): void => { if (started) return if (document ? !needsLegacyLanguageServer(document) : !workspace.textDocuments.some(needsLegacyLanguageServer)) return started = true - startup.start(() => activateLegacyClient(context, legacyClientOptions)) + const readiness = activateLegacyClient(context, legacyClientOptions) + legacyReadiness = readiness + startup.start(() => readiness) } const getOpenPrismaDocuments = (): TextDocument[] => @@ -202,10 +205,11 @@ const plugin: PrismaVSCodePlugin = { return waitForOwnershipOperations(legacyDocuments.map((document) => ownership.close(document))) } - const restartLanguageServer = async () => { + const restartLanguageServerNow = async (): Promise => { if (!started) { maybeStart() - for (const document of getOpenPrismaDocuments()) synchronizeDocument(document) + await legacyReadiness + await synchronizeDocuments(getOpenPrismaDocuments()) return } @@ -243,6 +247,17 @@ const plugin: PrismaVSCodePlugin = { } } + let restartQueue: Promise = Promise.resolve() + const enqueueLanguageServerOperation = (operation: () => Promise): Promise => { + const queued = restartQueue.then(operation, operation) + restartQueue = queued.then( + () => undefined, + () => undefined, + ) + return queued + } + const restartLanguageServer = (): Promise => enqueueLanguageServerOperation(restartLanguageServerNow) + context.subscriptions.push( // when the file watcher settings change, we need to ensure they are applied workspace.onDidChangeConfiguration((event) => { @@ -281,17 +296,21 @@ const plugin: PrismaVSCodePlugin = { await prismaConfig.update('fileWatcher', false /* value */, false /* workspace */) }), - commands.registerCommand('prisma.pinWorkspaceToPrisma6', async () => { - await workspace.getConfiguration('prisma').update('pinToPrisma6', true, false) - await restartLanguageServer() - void window.showInformationMessage('Pinned workspace to Prisma 6.') - }), - - commands.registerCommand('prisma.unpinWorkspaceFromPrisma6', async () => { - await workspace.getConfiguration('prisma').update('pinToPrisma6', false, false) - await restartLanguageServer() - void window.showInformationMessage('Unpinned workspace from Prisma 6.') - }), + commands.registerCommand('prisma.pinWorkspaceToPrisma6', () => + enqueueLanguageServerOperation(async () => { + await workspace.getConfiguration('prisma').update('pinToPrisma6', true, false) + await restartLanguageServerNow() + void window.showInformationMessage('Pinned workspace to Prisma 6.') + }), + ), + + commands.registerCommand('prisma.unpinWorkspaceFromPrisma6', () => + enqueueLanguageServerOperation(async () => { + await workspace.getConfiguration('prisma').update('pinToPrisma6', false, false) + await restartLanguageServerNow() + void window.showInformationMessage('Unpinned workspace from Prisma 6.') + }), + ), workspace.onDidOpenTextDocument((document) => { maybeStart(document) diff --git a/packages/vscode/src/util.ts b/packages/vscode/src/util.ts index fa05227d98..b0211efa19 100644 --- a/packages/vscode/src/util.ts +++ b/packages/vscode/src/util.ts @@ -110,7 +110,7 @@ export function createLegacyLanguageServer( serverOptions: ServerOptions, clientOptions: LanguageClientOptions, ): LanguageClient { - return new LanguageClient('prisma-legacy', 'Prisma Legacy Language Server', serverOptions, { + return new LanguageClient('prisma', 'Prisma Legacy Language Server', serverOptions, { ...clientOptions, outputChannelName: 'Prisma Legacy Language Server', }) From bcaa22a75463c8c137d351694d751e644bde68bf Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 14:13:58 +0000 Subject: [PATCH 38/43] fix(vscode): serialize language server disposal --- .../prisma-language-server/documentRouting.ts | 14 +- .../plugins/prisma-language-server/index.ts | 179 ++++++++++-------- .../languageServerLifecycle.ts | 70 +++++++ .../legacyClientMiddleware.ts | 8 + .../legacyClientStartup.ts | 102 ---------- .../prismaNextClientMiddleware.ts | 8 + .../prismaNextClientRegistry.ts | 50 ++++- packages/vscode/src/util.ts | 14 +- 8 files changed, 259 insertions(+), 186 deletions(-) create mode 100644 packages/vscode/src/plugins/prisma-language-server/languageServerLifecycle.ts delete mode 100644 packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index 9c4a07bef6..edcd8a877f 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -21,6 +21,7 @@ export interface PrismaNextDocumentSynchronization { export interface DocumentRoutingOptions { readonly getOwnership: () => DocumentOwnershipCoordinator + readonly isActive: () => boolean readonly isDocumentOpen: (document: TextDocument) => boolean readonly getLegacy: () => LegacyDocumentSynchronization readonly getPrismaNext: () => PrismaNextDocumentSynchronization @@ -31,6 +32,8 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio if (documentOwnersEqual(previousSettledOwner, nextDesiredOwner)) return undefined return async () => { + if (!options.isActive()) return commitOutcome(previousSettledOwner) + try { await closePreviousSettledOwner(options, previousSettledOwner, document) } catch (error) { @@ -38,6 +41,7 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio } try { + if (!options.isActive()) return commitOutcome(previousSettledOwner) await clearPreviousSettledOwnerDiagnostics(options, previousSettledOwner, document.uri) if (!isDesiredOpenCandidate(options, document, nextDesiredOwner)) { @@ -45,6 +49,7 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio } if (nextDesiredOwner.kind === 'legacy') { + if (!options.isActive()) return commitOutcome({ kind: 'unowned' }) options.getLegacy().openDocument(document) return commitOutcome(nextDesiredOwner) } @@ -55,6 +60,7 @@ export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptio if ( client && isDesiredOpenCandidate(options, document, nextDesiredOwner) && + options.isActive() && (await prismaNext.openDocument(nextDesiredOwner.workspaceFolderUri, document)) ) { return commitOutcome(nextDesiredOwner) @@ -74,6 +80,8 @@ async function closePreviousSettledOwner( previousSettledOwner: DocumentOwner, document: TextDocument, ): Promise { + if (!options.isActive()) return + if (previousSettledOwner.kind === 'legacy') { options.getLegacy().closeDocument(document) } else if (previousSettledOwner.kind === 'prisma-next') { @@ -86,6 +94,8 @@ async function clearPreviousSettledOwnerDiagnostics( previousSettledOwner: DocumentOwner, documentUri: Uri, ): Promise { + if (!options.isActive()) return + if (previousSettledOwner.kind === 'legacy') { options.getLegacy().clearDiagnostics(documentUri) } else if (previousSettledOwner.kind === 'prisma-next') { @@ -103,7 +113,9 @@ function isDesiredOpenCandidate( candidate: DocumentOwner, ): boolean { return ( - options.isDocumentOpen(document) && documentOwnersEqual(options.getOwnership().getDesiredOwner(document), candidate) + options.isActive() && + options.isDocumentOpen(document) && + documentOwnersEqual(options.getOwnership().getDesiredOwner(document), candidate) ) } diff --git a/packages/vscode/src/plugins/prisma-language-server/index.ts b/packages/vscode/src/plugins/prisma-language-server/index.ts index 28a8226c72..4d04df15de 100644 --- a/packages/vscode/src/plugins/prisma-language-server/index.ts +++ b/packages/vscode/src/plugins/prisma-language-server/index.ts @@ -25,30 +25,36 @@ import { DocumentOwnershipCoordinator } from './documentOwnership' import { createLegacyClientMiddleware, type LegacyClientMiddleware } from './legacyClientMiddleware' import { createPrepareDocumentRoutingCommit } from './documentRouting' import { PrismaNextClientRegistry } from './prismaNextClientRegistry' -import { LegacyClientStartup, deactivateLegacyClient } from './legacyClientStartup' +import { LanguageServerLifecycleController } from './languageServerLifecycle' let legacyClient: LanguageClient let legacyServerModule: string let telemetry: TelemetryReporter let fileWatcher: FileWatcher.type | undefined -let legacyClientStartup: LegacyClientStartup | undefined +let languageServerLifecycle: LanguageServerLifecycleController | undefined +let prismaNextClientRegistry: PrismaNextClientRegistry | undefined const isDebugMode = () => process.env.VSCODE_DEBUG_MODE === 'true' const logLegacyClientError = (error: unknown): void => { console.error('Legacy Prisma Language Server failed', error) } -const activateLegacyClient = async ( +const activateLegacyClientNow = async ( context: ExtensionContext, legacyClientOptions: LanguageClientOptions, + lifecycle: LanguageServerLifecycleController, ): Promise => { + lifecycle.assertActive() const prismaConfig = workspace.getConfiguration('prisma') - legacyClient = createLegacyLanguageServer(getLegacyServerOptions(prismaConfig, context), legacyClientOptions) - - const disposable = legacyClient.start() - - context.subscriptions.push(disposable) - await legacyClient.onReady() + lifecycle.assertActive() + const client = createLegacyLanguageServer(getLegacyServerOptions(prismaConfig, context), legacyClientOptions) + lifecycle.publishLegacyClient(client) + legacyClient = client + + lifecycle.assertActive() + context.subscriptions.push(client.start()) + await lifecycle.waitFor(client.onReady()) + lifecycle.assertActive() } const onFileChange = (filepath: string) => { @@ -115,6 +121,9 @@ const plugin: PrismaVSCodePlugin = { let legacyUnavailable = false let restarting = false + languageServerLifecycle?.dispose().catch(logLegacyClientError) + const lifecycle = new LanguageServerLifecycleController() + languageServerLifecycle = lifecycle const ownership: DocumentOwnershipCoordinator = new DocumentOwnershipCoordinator({ workspace, policy: { @@ -123,6 +132,7 @@ const plugin: PrismaVSCodePlugin = { }, prepareTransition: createPrepareDocumentRoutingCommit({ getOwnership: (): DocumentOwnershipCoordinator => ownership, + isActive: () => lifecycle.isActive, isDocumentOpen: (document) => workspace.textDocuments.includes(document), getLegacy: () => legacyClientMiddleware, getPrismaNext: () => prismaNextClients, @@ -131,6 +141,7 @@ const plugin: PrismaVSCodePlugin = { const prismaNextClients = new PrismaNextClientRegistry({ workspace, ownership, + isActive: () => lifecycle.isActive, getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), createClient: (id, name, serverOptions, prismaNextClientOptions) => new LanguageClient(id, name, serverOptions, prismaNextClientOptions), @@ -139,9 +150,11 @@ const plugin: PrismaVSCodePlugin = { console.error(`Failed to start Prisma Next Language Server for ${workspaceFolder.uri.toString()}`, error) }, }) + prismaNextClientRegistry = prismaNextClients const legacyClientMiddleware: LegacyClientMiddleware = createLegacyClientMiddleware({ ownership, + isActive: () => lifecycle.isActive, getClient: () => legacyClient, getDocument: (uri) => workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()), handleDiagnosticMessage: (message) => { @@ -156,36 +169,11 @@ const plugin: PrismaVSCodePlugin = { middleware: legacyClientMiddleware, } + let startRequested = false let started = false - let legacyReadiness: Promise | undefined - legacyClientStartup?.dispose() - const startup = new LegacyClientStartup({ - isCurrent: (document) => workspace.textDocuments.includes(document), - synchronize: (document) => ownership.synchronize(document), - logError: logLegacyClientError, - }) - legacyClientStartup = startup + let ready = false const needsLegacyLanguageServer = (document: TextDocument): boolean => document.languageId === 'prisma' && ownership.getDesiredOwner(document).kind === 'legacy' - const synchronizeDocument = (document: TextDocument): void => { - if (restarting || document.languageId !== 'prisma') return - if (ownership.getDesiredOwner(document).kind === 'legacy') { - startup.schedule(document) - } else { - void ownership.synchronize(document).catch(logLegacyClientError) - } - } - - const maybeStart = (document?: TextDocument): void => { - if (started) return - if (document ? !needsLegacyLanguageServer(document) : !workspace.textDocuments.some(needsLegacyLanguageServer)) - return - started = true - const readiness = activateLegacyClient(context, legacyClientOptions) - legacyReadiness = readiness - startup.start(() => readiness) - } - const getOpenPrismaDocuments = (): TextDocument[] => workspace.textDocuments.filter((document) => document.languageId === 'prisma') @@ -195,68 +183,100 @@ const plugin: PrismaVSCodePlugin = { if (failure) throw failure.reason } - const synchronizeDocuments = (documents: readonly TextDocument[]): Promise => - waitForOwnershipOperations(documents.map((document) => ownership.synchronize(document))) + const synchronizeDocuments = (documents: readonly TextDocument[]): Promise => { + lifecycle.assertActive() + return waitForOwnershipOperations(documents.map((document) => ownership.synchronize(document))) + } const invalidateLegacyDocuments = (): Promise => { + lifecycle.assertActive() const legacyDocuments = getOpenPrismaDocuments().filter( (document) => ownership.getSettledOwner(document.uri).kind === 'legacy', ) return waitForOwnershipOperations(legacyDocuments.map((document) => ownership.close(document))) } + const startLegacyLanguageServerNow = async (): Promise => { + lifecycle.assertActive() + if (started) return + + started = true + await activateLegacyClientNow(context, legacyClientOptions, lifecycle) + ready = true + lifecycle.assertActive() + await synchronizeDocuments(getOpenPrismaDocuments()) + } + + const maybeStart = (document?: TextDocument): void => { + if (!lifecycle.isActive || startRequested || started) return + if (document ? !needsLegacyLanguageServer(document) : !workspace.textDocuments.some(needsLegacyLanguageServer)) + return + + startRequested = true + void lifecycle.enqueue(startLegacyLanguageServerNow).catch(logLegacyClientError) + } + + const synchronizeDocument = (document: TextDocument): void => { + if (!lifecycle.isActive || restarting || document.languageId !== 'prisma') return + if (ownership.getDesiredOwner(document).kind === 'legacy') { + if (!started) maybeStart(document) + if (!ready) return + } + void ownership.synchronize(document).catch(logLegacyClientError) + } + const restartLanguageServerNow = async (): Promise => { + lifecycle.assertActive() if (!started) { - maybeStart() - await legacyReadiness - await synchronizeDocuments(getOpenPrismaDocuments()) + if (getOpenPrismaDocuments().some(needsLegacyLanguageServer)) { + await startLegacyLanguageServerNow() + } else { + await synchronizeDocuments(getOpenPrismaDocuments()) + } return } restarting = true + ready = false legacyUnavailable = true try { try { await invalidateLegacyDocuments() } catch (error) { legacyUnavailable = false - try { - await synchronizeDocuments(getOpenPrismaDocuments()) - } catch (restoreError) { - logLegacyClientError(restoreError) + if (lifecycle.isActive) { + try { + await synchronizeDocuments(getOpenPrismaDocuments()) + } catch (restoreError) { + logLegacyClientError(restoreError) + } } throw error } + lifecycle.assertActive() const serverOptions = getLegacyServerOptions(workspace.getConfiguration('prisma'), context) - const replacement = restartClient(context, legacyClient, serverOptions, legacyClientOptions, { + const replacement = await restartClient(context, legacyClient, serverOptions, legacyClientOptions, { + assertActive: () => lifecycle.assertActive(), + waitFor: (operation) => lifecycle.waitFor(operation), onClientStopped: () => legacyClientMiddleware.resetClientState(), onClientCreated: (replacementClient) => { + lifecycle.publishLegacyClient(replacementClient) legacyClient = replacementClient }, - }).then(async (replacementClient) => { - legacyUnavailable = false - await synchronizeDocuments(getOpenPrismaDocuments()) - return replacementClient }) - startup.replace(replacement.then(() => undefined)) - legacyClient = await replacement + lifecycle.assertActive() + legacyClient = replacement + ready = true + legacyUnavailable = false + await synchronizeDocuments(getOpenPrismaDocuments()) } finally { legacyUnavailable = false restarting = false } } - let restartQueue: Promise = Promise.resolve() - const enqueueLanguageServerOperation = (operation: () => Promise): Promise => { - const queued = restartQueue.then(operation, operation) - restartQueue = queued.then( - () => undefined, - () => undefined, - ) - return queued - } - const restartLanguageServer = (): Promise => enqueueLanguageServerOperation(restartLanguageServerNow) + const restartLanguageServer = (): Promise => lifecycle.enqueue(restartLanguageServerNow) context.subscriptions.push( // when the file watcher settings change, we need to ensure they are applied @@ -297,17 +317,23 @@ const plugin: PrismaVSCodePlugin = { }), commands.registerCommand('prisma.pinWorkspaceToPrisma6', () => - enqueueLanguageServerOperation(async () => { + lifecycle.enqueue(async () => { + lifecycle.assertActive() await workspace.getConfiguration('prisma').update('pinToPrisma6', true, false) + lifecycle.assertActive() await restartLanguageServerNow() + lifecycle.assertActive() void window.showInformationMessage('Pinned workspace to Prisma 6.') }), ), commands.registerCommand('prisma.unpinWorkspaceFromPrisma6', () => - enqueueLanguageServerOperation(async () => { + lifecycle.enqueue(async () => { + lifecycle.assertActive() await workspace.getConfiguration('prisma').update('pinToPrisma6', false, false) + lifecycle.assertActive() await restartLanguageServerNow() + lifecycle.assertActive() void window.showInformationMessage('Unpinned workspace from Prisma 6.') }), ), @@ -321,7 +347,7 @@ const plugin: PrismaVSCodePlugin = { synchronizeDocument(event.document) }), workspace.onDidCloseTextDocument((document) => { - if (document.languageId === 'prisma') { + if (lifecycle.isActive && document.languageId === 'prisma') { void ownership.close(document).catch(logLegacyClientError) } }), @@ -350,20 +376,23 @@ const plugin: PrismaVSCodePlugin = { checkForMinimalColorTheme() }, - deactivate: () => { - const startup = legacyClientStartup - const activeClient = legacyClient - legacyClientStartup = undefined - const deactivation = deactivateLegacyClient( - startup, - activeClient ? () => activeClient.stop() : undefined, - logLegacyClientError, + deactivate: async () => { + const lifecycle = languageServerLifecycle + const prismaNextClients = prismaNextClientRegistry + languageServerLifecycle = undefined + prismaNextClientRegistry = undefined + + const deactivations = [lifecycle?.dispose(), prismaNextClients?.dispose()].filter( + (deactivation): deactivation is Promise => deactivation !== undefined, ) + const results = await Promise.allSettled(deactivations) + for (const result of results) { + if (result.status === 'rejected') logLegacyClientError(result.reason) + } - if (activeClient && !isDebugOrTestSession()) { + if (legacyClient && !isDebugOrTestSession()) { telemetry.dispose() // eslint-disable-line @typescript-eslint/no-floating-promises } - return deactivation }, } diff --git a/packages/vscode/src/plugins/prisma-language-server/languageServerLifecycle.ts b/packages/vscode/src/plugins/prisma-language-server/languageServerLifecycle.ts new file mode 100644 index 0000000000..f582d9fb5c --- /dev/null +++ b/packages/vscode/src/plugins/prisma-language-server/languageServerLifecycle.ts @@ -0,0 +1,70 @@ +import type { LanguageClient } from 'vscode-languageclient/node' + +export class LanguageServerLifecycleController { + private disposed = false + private queue: Promise = Promise.resolve() + private latestLegacyClient: LanguageClient | undefined + private deactivation: Promise | undefined + private signalDisposal!: () => void + private readonly disposalSignal = new Promise((resolve) => { + this.signalDisposal = resolve + }) + + get isActive(): boolean { + return !this.disposed + } + + assertActive(): void { + if (this.disposed) { + throw new Error('Prisma language-server lifecycle has been disposed.') + } + } + + publishLegacyClient(client: LanguageClient): void { + this.assertActive() + this.latestLegacyClient = client + } + + waitFor(operation: Promise): Promise { + return Promise.race([ + operation, + this.disposalSignal.then(() => { + throw new Error('Prisma language-server lifecycle has been disposed.') + }), + ]) + } + + enqueue(operation: () => Promise): Promise { + if (this.disposed) { + return Promise.reject(new Error('Prisma language-server lifecycle has been disposed.')) + } + + const queued = this.queue.then( + () => { + this.assertActive() + return operation() + }, + () => { + this.assertActive() + return operation() + }, + ) + this.queue = queued.then( + () => undefined, + () => undefined, + ) + return queued + } + + dispose(): Promise { + if (this.deactivation) return this.deactivation + + this.disposed = true + this.signalDisposal() + this.deactivation = this.queue.then(async () => { + const client = this.latestLegacyClient + if (client) await client.stop() + }) + return this.deactivation + } +} diff --git a/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts index 38685325ad..b3817feb1f 100644 --- a/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/legacyClientMiddleware.ts @@ -9,6 +9,7 @@ import { DocumentOwnershipCoordinator } from './documentOwnership' export interface LegacyClientMiddlewareOptions { readonly ownership: DocumentOwnershipCoordinator + readonly isActive: () => boolean readonly getClient: () => LanguageClient readonly getDocument: (uri: Uri) => TextDocument | undefined readonly handleDiagnosticMessage: (message: string) => void @@ -27,16 +28,21 @@ export function createLegacyClientMiddleware(options: LegacyClientMiddlewareOpti const legacyDocuments = new Set() const isLegacyDocument = (document: TextDocument): boolean => { + if (!options.isActive()) return false + const settledOwner = options.ownership.getSettledOwner(document.uri) const desiredOwner = options.ownership.getDesiredOwner(document) return settledOwner.kind === 'legacy' && desiredOwner.kind === 'legacy' } const clearDiagnostics = (uri: Uri): void => { + if (!options.isActive()) return options.getClient().diagnostics?.delete(uri) } const openLegacyDocument = (document: TextDocument): void => { + if (!options.isActive()) return + const documentUri = document.uri.toString() if (legacyDocuments.has(documentUri)) return @@ -51,6 +57,8 @@ export function createLegacyClientMiddleware(options: LegacyClientMiddlewareOpti } const closeLegacyDocument = (document: TextDocument): void => { + if (!options.isActive()) return + const documentUri = document.uri.toString() if (!legacyDocuments.delete(documentUri)) return diff --git a/packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts b/packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts deleted file mode 100644 index 7cddd9ca2c..0000000000 --- a/packages/vscode/src/plugins/prisma-language-server/legacyClientStartup.ts +++ /dev/null @@ -1,102 +0,0 @@ -export type LegacyClientStartupStatus = 'idle' | 'starting' | 'ready' | 'failed' | 'disposed' - -export interface LegacyClientStartupOptions { - readonly isCurrent: (value: T) => boolean - readonly synchronize: (value: T) => Promise - readonly logError: (error: unknown) => void -} - -export async function deactivateLegacyClient( - startup: LegacyClientStartup | undefined, - stop: (() => Promise) | undefined, - logError: (error: unknown) => void, -): Promise { - startup?.dispose() - if (!stop) return - - try { - await stop() - } catch (error) { - reportError(logError, error) - } -} - -export class LegacyClientStartup { - private generation = 0 - private readiness: Promise = Promise.resolve(false) - private readonly pending = new Map() - private currentStatus: LegacyClientStartupStatus = 'idle' - - constructor(private readonly options: LegacyClientStartupOptions) {} - - get status(): LegacyClientStartupStatus { - return this.currentStatus - } - - start(startClient: () => Promise): void { - if (this.currentStatus !== 'idle') return - this.install(startClient()) - } - - replace(readiness: Promise): void { - if (this.currentStatus === 'disposed') return - this.install(readiness) - } - - schedule(value: T): void { - if (this.currentStatus === 'idle' || this.currentStatus === 'failed' || this.currentStatus === 'disposed') return - if (this.pending.has(value)) return - - const generation = this.generation - this.pending.set(value, generation) - void this.readiness - .then(async (ready) => { - if (!ready || generation !== this.generation || !this.options.isCurrent(value)) return - await this.options.synchronize(value) - }) - .catch((error: unknown) => this.report(error)) - .finally(() => { - if (this.pending.get(value) === generation) { - this.pending.delete(value) - } - }) - .catch((error: unknown) => this.report(error)) - } - - dispose(): void { - this.generation += 1 - this.currentStatus = 'disposed' - this.pending.clear() - } - - private install(readiness: Promise): void { - const generation = ++this.generation - this.currentStatus = 'starting' - this.pending.clear() - this.readiness = readiness.then( - () => { - if (generation !== this.generation) return false - this.currentStatus = 'ready' - return true - }, - (error: unknown) => { - if (generation !== this.generation) return false - this.currentStatus = 'failed' - this.report(error) - return false - }, - ) - } - - private report(error: unknown): void { - reportError(this.options.logError, error) - } -} - -function reportError(logError: (error: unknown) => void, error: unknown): void { - try { - logError(error) - } catch { - // Logging must never turn handled lifecycle failures back into detached rejections. - } -} diff --git a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts index 3492f8abe0..b28cc841eb 100644 --- a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientMiddleware.ts @@ -5,6 +5,7 @@ import type { DocumentOwnershipCoordinator } from './documentOwnership' export interface PrismaNextClientMiddlewareOptions { readonly workspaceFolderUri: string readonly ownership: DocumentOwnershipCoordinator + readonly isActive: () => boolean readonly getClient: () => LanguageClient readonly getDocument: (uri: Uri) => TextDocument | undefined } @@ -22,6 +23,8 @@ export function createPrismaNextClientMiddleware( const completionDocuments = new WeakMap() const isOwnedDocument = (document: TextDocument): boolean => { + if (!options.isActive()) return false + const settledOwner = options.ownership.getSettledOwner(document.uri) const desiredOwner = options.ownership.getDesiredOwner(document) return ( @@ -33,10 +36,13 @@ export function createPrismaNextClientMiddleware( } const clearDiagnostics = (uri: Uri): void => { + if (!options.isActive()) return options.getClient().diagnostics?.delete(uri) } const openDocument = (document: TextDocument): void => { + if (!options.isActive()) return + const documentUri = document.uri.toString() if (synchronizedDocuments.has(documentUri)) return @@ -51,6 +57,8 @@ export function createPrismaNextClientMiddleware( } const closeDocument = (document: TextDocument): void => { + if (!options.isActive()) return + const documentUri = document.uri.toString() if (!synchronizedDocuments.delete(documentUri)) return diff --git a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts index 299b4abf9d..8bede98da3 100644 --- a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts @@ -30,6 +30,7 @@ export interface PrismaNextLauncherOptions { export interface PrismaNextClientRegistryOptions { readonly workspace: PrismaNextClientRegistryWorkspace readonly ownership: DocumentOwnershipCoordinator + readonly isActive: () => boolean readonly getDocument: (uri: Uri) => TextDocument | undefined readonly createClient: ( id: string, @@ -50,11 +51,19 @@ interface PrismaNextClientEntry { export class PrismaNextClientRegistry { private readonly clients = new Map>() + private readonly startedClients = new Set() + private disposed = false + private deactivation: Promise | undefined constructor(private readonly options: PrismaNextClientRegistryOptions) {} ensureClientForDocument(document: TextDocument): Promise { - if (!this.options.workspace.isTrusted || document.uri.scheme !== 'file') { + if ( + this.disposed || + !this.options.isActive() || + !this.options.workspace.isTrusted || + document.uri.scheme !== 'file' + ) { return Promise.resolve(undefined) } @@ -67,8 +76,10 @@ export class PrismaNextClientRegistry { } async openDocument(workspaceFolderUri: string, document: TextDocument): Promise { + if (this.disposed || !this.options.isActive()) return false + const entry = await this.clients.get(workspaceFolderUri) - if (!entry || this.options.getDocument(document.uri) !== document) { + if (this.disposed || !this.options.isActive() || !entry || this.options.getDocument(document.uri) !== document) { return false } entry.middleware.openDocument(document) @@ -76,16 +87,32 @@ export class PrismaNextClientRegistry { } async closeDocument(workspaceFolderUri: string, document: TextDocument): Promise { + if (this.disposed || !this.options.isActive()) return + const entry = await this.clients.get(workspaceFolderUri) - entry?.middleware.closeDocument(document) + if (!this.disposed && this.options.isActive()) entry?.middleware.closeDocument(document) } async clearDiagnostics(workspaceFolderUri: string, uri: Uri): Promise { + if (this.disposed || !this.options.isActive()) return + const entry = await this.clients.get(workspaceFolderUri) - entry?.middleware.clearDiagnostics(uri) + if (!this.disposed && this.options.isActive()) entry?.middleware.clearDiagnostics(uri) + } + + dispose(): Promise { + if (this.deactivation) return this.deactivation + + this.disposed = true + this.deactivation = Promise.allSettled([...this.startedClients].map((client) => client.stop())).then( + () => undefined, + ) + return this.deactivation } private ensureClient(workspaceFolder: WorkspaceFolder): Promise { + if (this.disposed || !this.options.isActive()) return Promise.resolve(undefined) + const workspaceFolderUri = workspaceFolder.uri.toString() const existing = this.clients.get(workspaceFolderUri) if (existing) { @@ -102,7 +129,7 @@ export class PrismaNextClientRegistry { try { const exists = await (this.options.entrypointExists ?? isFile)(entrypoint) - if (!exists) { + if (!exists || this.disposed || !this.options.isActive()) { return undefined } @@ -110,9 +137,12 @@ export class PrismaNextClientRegistry { const middleware = createPrismaNextClientMiddleware({ workspaceFolderUri, ownership: this.options.ownership, + isActive: this.options.isActive, getClient: () => client, getDocument: this.options.getDocument, }) + if (this.disposed || !this.options.isActive()) return undefined + const client = this.options.createClient( `prisma-next:${workspaceFolderUri}`, `Prisma Next Language Server (${workspaceFolder.name})`, @@ -122,11 +152,19 @@ export class PrismaNextClientRegistry { }), createPrismaNextClientOptions(workspaceFolder, middleware), ) + this.startedClients.add(client) + if (this.disposed || !this.options.isActive()) { + await client.stop() + return undefined + } this.options.registerDisposable(client.start()) await client.onReady() + if (this.disposed || !this.options.isActive()) return undefined return { client, middleware } } catch (error) { - this.options.handleStartError?.(workspaceFolder, error) + if (!this.disposed && this.options.isActive()) { + this.options.handleStartError?.(workspaceFolder, error) + } return undefined } } diff --git a/packages/vscode/src/util.ts b/packages/vscode/src/util.ts index b0211efa19..b754ce2e21 100644 --- a/packages/vscode/src/util.ts +++ b/packages/vscode/src/util.ts @@ -116,6 +116,8 @@ export function createLegacyLanguageServer( }) } export interface RestartClientLifecycle { + assertActive?(): void + waitFor?(operation: Promise): Promise onClientStopped(): void onClientCreated(client: LanguageClient): void } @@ -128,11 +130,19 @@ export const restartClient = async ( lifecycle?: RestartClientLifecycle, ): Promise => { client?.diagnostics?.dispose() - if (client) await client.stop() + if (client) { + const stopping = client.stop() + await (lifecycle?.waitFor?.(stopping) ?? stopping) + } lifecycle?.onClientStopped() + lifecycle?.assertActive?.() client = createLegacyLanguageServer(serverOptions, clientOptions) + lifecycle?.assertActive?.() lifecycle?.onClientCreated(client) + lifecycle?.assertActive?.() context.subscriptions.push(client.start()) - await client.onReady() + const readiness = client.onReady() + await (lifecycle?.waitFor?.(readiness) ?? readiness) + lifecycle?.assertActive?.() return client } From 8a7a933525919bad099478b8c33b9cb25a007f7a Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 14:20:20 +0000 Subject: [PATCH 39/43] fix(vscode): await plugin deactivation --- packages/vscode/src/extension.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index ecad4027a9..9bbfc3d3b1 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -15,10 +15,12 @@ export function activate(context: ExtensionContext): void { }) } -export function deactivate(): void { - plugins.forEach((plugin) => { - if (plugin.deactivate) { - void plugin.deactivate() +export async function deactivate(): Promise { + const results = await Promise.allSettled(plugins.map((plugin) => Promise.resolve().then(() => plugin.deactivate?.()))) + + for (const [index, result] of results.entries()) { + if (result.status === 'rejected') { + console.error(`Failed to deactivate ${plugins[index].name}`, result.reason) } - }) + } } From 907fb2a799a7125f7665310e95fe2493353a2bf9 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 14:38:44 +0000 Subject: [PATCH 40/43] docs: simplify Prisma Next routing guidance --- docs/language-server.md | 57 +++++------------------------------------ 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/docs/language-server.md b/docs/language-server.md index ad0668bea9..b04c9ee7b0 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -41,62 +41,17 @@ When `prisma.pinToPrisma6` is disabled, the VS Code extension routes each open P | Document | Owner | | --------------------------------------------------------------------------------- | ------------------------------------------ | | No `// use prisma-next` directive | Legacy language server | -| Directive present, trusted file workspace, matching root, and local CLI available | Prisma Next client for that workspace root | -| Directive present but local execution is ineligible or unavailable | No active language-server synchronization | +| Directive present, trusted file workspace, matching root, and Prisma Next CLI available | Prisma Next client for that workspace root | +| Directive present but Prisma Next execution is ineligible or unavailable | No active language-server synchronization | -The directive is content based and applies per file. A marked file does not opt sibling files or the rest of a multi-file schema into Prisma Next tooling. +The directive is content-based and applies per file. A marked file does not opt sibling files or the rest of a multi-file schema into Prisma Next tooling. -### Coordinator and synchronization boundary - -`DocumentOwnershipCoordinator` is the authoritative per-URI state machine. `desiredOwner` is computed from the document's current text and workspace policy; `settledOwner` records the server synchronized only after a serialized transition's commit closure completes. Open and change events are serialized per document. A transfer performs these operations in order: - -1. Close the prior synchronized owner. -2. Clear that owner's diagnostics for only the transferred URI. -3. Reclassify current unsaved text. -4. Lazily ensure the candidate Prisma Next client for the exact workspace root when needed. -5. Reclassify after asynchronous startup. -6. Open the complete current document on the surviving owner. -7. Record the successfully synchronized candidate as the settled owner, or `unowned` if no candidate opened. - -A close event invalidates pending revisions immediately and queues final cleanup. Its settled owner becomes `unowned` only after the cleanup commit closure completes. Commit closures return a structured outcome containing the owner established by their completed stages and an optional operational error. A failed prior-owner close keeps the prior settled owner because middleware restores its ledger. Once close succeeds, diagnostic clearing, desired-owner validation, ownership lookup, client startup, and candidate open share one outcome-producing error boundary: any failure settles `unowned` before the coordinator surfaces the error. Later per-URI work therefore observes the established state and the queue continues. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed. - -Legacy and Prisma Next middleware maintain ledgers of documents actually synchronized to their client. Ledger insertion and removal are rolled back when notification dispatch throws synchronously. Raw editor notifications, feature requests, and diagnostics are forwarded only when `getSettledOwner(document.uri)` and `getDesiredOwner(document)` agree on the middleware's expected identity and, for Prisma Next, the exact workspace root. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use this same gate. Automatic Prisma Next client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to Prisma Next over LSP. - -Restart, pin, and unpin operations enter one shared promise queue before reading or changing Prisma pin configuration. Each caller receives its own operation result, while a failure is absorbed only by the queue tail so later operations still run. This serialization prevents one operation from resetting another's restart or legacy-availability flags. - -Within its queue slot, a legacy-client restart first marks legacy service temporarily unavailable, making legacy documents desired `unowned` so requests are gated immediately. It then serializes every document settled to legacy through coordinator close and cleanup, stops the old client, resets middleware state, and publishes and starts the replacement. Once the replacement is ready, legacy availability is restored and every currently open Prisma document is explicitly reconciled. Legacy ownership settles only after its explicit replacement-client open succeeds. Unaffected Prisma Next documents remain settled to their existing exact-root client; policy changes are applied during the all-document reconciliation. Replacement clients retain the stable `prisma` client ID for `prisma.trace.server` compatibility while using `Prisma Legacy Language Server` as their display and output-channel identity. - -### Workspace-root Prisma Next launch contract - -The Prisma Next client registry is keyed by `WorkspaceFolder.uri.toString()` and coalesces concurrent startup for one root. Discovery checks only: +For marked files, the extension uses only the Prisma CLI installed at: ```text /node_modules/prisma/dist/prisma.js ``` -The registry does not invoke a package manager, search parent directories, inspect package boundaries, or fall back to a global executable. Local execution requires `workspace.isTrusted` and a file-backed document in a file-backed workspace folder. - -The extension launches the module with the extension-host runtime using the exact process shape: - -```text -executable: process.execPath -argv: [/node_modules/prisma/dist/prisma.js, "lsp"] -cwd: -stdio: piped -shell: false -``` - -Electron extension hosts receive `ELECTRON_RUN_AS_NODE=1` and `ELECTRON_NO_ASAR=1`. The custom server-options launcher avoids transport arguments that `vscode-languageclient` would otherwise append. - -### Registry lifecycle contract - -The registry exposes a narrow lifecycle API used by routing and later workspace lifecycle handling: - -- `ensureClientForDocument(document)` — trust/root checks, exact discovery, and coalesced lazy startup. -- `openDocument(rootUri, document)` — verifies the document is still open before inserting it into the Prisma Next middleware ledger. -- `closeDocument(rootUri, document)` — idempotently balances an actually synchronized Prisma Next document. -- `clearDiagnostics(rootUri, uri)` — clears only the requested URI. - -A started Prisma Next client currently remains alive after its final marked document closes. Workspace-wide restart and rediscovery, runtime-failure recovery, workspace-folder removal, comprehensive deactivation, and live Prisma 6 pin transitions are separate lifecycle responsibilities that should build on this API rather than bypass the coordinator or middleware ledgers. +The workspace must be trusted. The extension does not invoke a package manager, search parent directories, or fall back to a global installation. If the CLI is unavailable, the marked file has no language-server features until a suitable Prisma Next server can be started. -Routing is covered end to end through public completion behavior. In one workspace root, the Electron integration test opens an unmarked document served by the legacy Prisma 7 language server beside a marked document served by the real Prisma Next server from the workspace-local Prisma 8 CLI. The legacy document offers `datasource`, `generator`, and `model` but not `namespace`; the marked document offers the Prisma 8 `namespace` keyword but not `datasource`. The test does not expose or inspect coordinator ownership, routing events, or client startup counts. +The extension starts at most one Prisma Next language server per workspace root. Adding or removing the directive in an open file transfers that file between the legacy and Prisma Next servers without requiring a save or restart. Prisma Next servers do not restart automatically after a failure; use **Prisma: Restart Language Server** to retry. Pinning the workspace to Prisma 6 routes every Prisma document to the legacy Prisma 6 server. From 6cde5032a0b17803480da8d8beae096431d84b85 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 14:38:49 +0000 Subject: [PATCH 41/43] refactor(vscode): share document owner equality --- .../documentOwnership.ts | 2 +- .../prisma-language-server/documentRouting.ts | 23 +++++++------------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts index 68a35406ed..65a1902be0 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentOwnership.ts @@ -174,7 +174,7 @@ export class DocumentOwnershipCoordinator { } } -function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { +export function ownersEqual(left: DocumentOwner, right: DocumentOwner): boolean { if (left.kind !== right.kind) { return false } diff --git a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts index edcd8a877f..210821b786 100644 --- a/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts +++ b/packages/vscode/src/plugins/prisma-language-server/documentRouting.ts @@ -1,9 +1,10 @@ import type { TextDocument, Uri } from 'vscode' -import type { - DocumentOwner, - DocumentOwnerCommitOutcome, - DocumentOwnershipCoordinator, - PrepareDocumentOwnerCommit, +import { + ownersEqual, + type DocumentOwner, + type DocumentOwnerCommitOutcome, + type DocumentOwnershipCoordinator, + type PrepareDocumentOwnerCommit, } from './documentOwnership' export interface LegacyDocumentSynchronization { @@ -29,7 +30,7 @@ export interface DocumentRoutingOptions { export function createPrepareDocumentRoutingCommit(options: DocumentRoutingOptions): PrepareDocumentOwnerCommit { return ({ document, previousSettledOwner, nextDesiredOwner }) => { - if (documentOwnersEqual(previousSettledOwner, nextDesiredOwner)) return undefined + if (ownersEqual(previousSettledOwner, nextDesiredOwner)) return undefined return async () => { if (!options.isActive()) return commitOutcome(previousSettledOwner) @@ -115,14 +116,6 @@ function isDesiredOpenCandidate( return ( options.isActive() && options.isDocumentOpen(document) && - documentOwnersEqual(options.getOwnership().getDesiredOwner(document), candidate) - ) -} - -export function documentOwnersEqual(left: DocumentOwner, right: DocumentOwner): boolean { - if (left.kind !== right.kind) return false - return ( - left.kind !== 'prisma-next' || - (right.kind === 'prisma-next' && left.workspaceFolderUri === right.workspaceFolderUri) + ownersEqual(options.getOwnership().getDesiredOwner(document), candidate) ) } From 857103770d3363a5644254dbf2506987e2ca78c3 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 14:38:56 +0000 Subject: [PATCH 42/43] fix(vscode): clean up failed Prisma Next clients --- .../prismaNextClientRegistry.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts index 8bede98da3..ca7a16408b 100644 --- a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts @@ -2,7 +2,7 @@ import path from 'node:path' import { stat } from 'node:fs/promises' import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process' import type { Disposable, TextDocument, Uri, WorkspaceFolder } from 'vscode' -import type { LanguageClientOptions } from 'vscode-languageclient' +import { CloseAction, ErrorAction, type LanguageClientOptions } from 'vscode-languageclient' import type { ChildProcessInfo, LanguageClient, ServerOptions } from 'vscode-languageclient/node' import type { DocumentOwnershipCoordinator } from './documentOwnership' import { createPrismaNextClientMiddleware, type PrismaNextClientMiddleware } from './prismaNextClientMiddleware' @@ -68,7 +68,7 @@ export class PrismaNextClientRegistry { } const workspaceFolder = this.options.workspace.getWorkspaceFolder(document.uri) - if (!workspaceFolder || workspaceFolder.uri.scheme !== 'file') { + if (workspaceFolder?.uri.scheme !== 'file') { return Promise.resolve(undefined) } @@ -121,11 +121,21 @@ export class PrismaNextClientRegistry { const pending = Promise.resolve().then(() => this.discoverAndStart(workspaceFolder)) this.clients.set(workspaceFolderUri, pending) + void pending.then((entry) => { + if (!entry && this.clients.get(workspaceFolderUri) === pending) { + this.clients.delete(workspaceFolderUri) + } + }) return pending } private async discoverAndStart(workspaceFolder: WorkspaceFolder): Promise { const entrypoint = getPrismaNextEntrypoint(workspaceFolder) + let client: LanguageClient | undefined + const getClient = (): LanguageClient => { + if (!client) throw new Error('Prisma Next language client is not initialized') + return client + } try { const exists = await (this.options.entrypointExists ?? isFile)(entrypoint) @@ -138,12 +148,12 @@ export class PrismaNextClientRegistry { workspaceFolderUri, ownership: this.options.ownership, isActive: this.options.isActive, - getClient: () => client, + getClient, getDocument: this.options.getDocument, }) if (this.disposed || !this.options.isActive()) return undefined - const client = this.options.createClient( + client = this.options.createClient( `prisma-next:${workspaceFolderUri}`, `Prisma Next Language Server (${workspaceFolder.name})`, createPrismaNextServerOptions(workspaceFolder, entrypoint, { @@ -162,6 +172,14 @@ export class PrismaNextClientRegistry { if (this.disposed || !this.options.isActive()) return undefined return { client, middleware } } catch (error) { + if (client) { + try { + await client.stop() + this.startedClients.delete(client) + } catch { + // Deactivation retries cleanup for clients that could not be stopped here. + } + } if (!this.disposed && this.options.isActive()) { this.options.handleStartError?.(workspaceFolder, error) } @@ -263,6 +281,11 @@ export function createPrismaNextClientOptions( documentSelector: [{ language: 'prisma', scheme: 'file', pattern: `${normalizedRoot}/**/*` }], workspaceFolder, middleware, + initializationFailedHandler: () => false, + errorHandler: { + error: () => ErrorAction.Shutdown, + closed: () => CloseAction.DoNotRestart, + }, } } From 5280f734101be2bf4eb1cfac60db92d8eb9575ff Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 25 Aug 2026 14:43:06 +0000 Subject: [PATCH 43/43] fix(vscode): escape workspace glob roots --- .../plugins/prisma-language-server/prismaNextClientRegistry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts index ca7a16408b..32ef4a4696 100644 --- a/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts +++ b/packages/vscode/src/plugins/prisma-language-server/prismaNextClientRegistry.ts @@ -277,8 +277,9 @@ export function createPrismaNextClientOptions( ): LanguageClientOptions { const rootPath = workspaceFolder.uri.fsPath.split('\\').join('/') const normalizedRoot = rootPath.endsWith('/') ? rootPath.slice(0, -1) : rootPath + const escapedRoot = normalizedRoot.replace(/([?*[\]])/g, '[$1]') return { - documentSelector: [{ language: 'prisma', scheme: 'file', pattern: `${normalizedRoot}/**/*` }], + documentSelector: [{ language: 'prisma', scheme: 'file', pattern: `${escapedRoot}/**/*` }], workspaceFolder, middleware, initializationFailedHandler: () => false,