From d66dabbfc69480650702f668570a6f6eccdf4402 Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Sat, 16 May 2026 10:21:44 +0200 Subject: [PATCH 1/2] Fix vscode-knip build: pin native oxc bindings to bundled JS version --- packages/vscode-knip/scripts/publish.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/vscode-knip/scripts/publish.js b/packages/vscode-knip/scripts/publish.js index 509f96a2b..9b1671707 100644 --- a/packages/vscode-knip/scripts/publish.js +++ b/packages/vscode-knip/scripts/publish.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import { execSync } from 'node:child_process'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -69,6 +70,16 @@ await bundle('src/index.js', 'extension.js', [...extSession, '@knip/language-ser const knipNm = join(dirname(fileURLToPath(import.meta.resolve('knip'))), '..', 'node_modules'); +// Pin native bindings to the exact version of the bundled JS wrapper. `oxc-parser` +// is copied as-is and `oxc-resolver` is bundled by rolldown, both at the version +// resolved from knip's deps. `npm pack` without a version pulls the *latest* +// binding, which silently mismatches the JS wrapper and corrupts parsing. +const knipRequire = createRequire(join(knipNm, '..', 'package.json')); +const bindingVersion = pkgName => JSON.parse(readFileSync(knipRequire.resolve(`${pkgName}/package.json`), 'utf8')).version; +const oxcParserVersion = bindingVersion('oxc-parser'); +const oxcResolverVersion = bindingVersion('oxc-resolver'); +console.log(`Pinning bindings: @oxc-parser@${oxcParserVersion}, @oxc-resolver@${oxcResolverVersion}`); + cpSync(join(knipNm, 'jiti'), join(nm, 'jiti'), { recursive: true, dereference: true }); cpSync(join(knipNm, 'oxc-parser'), join(nm, 'oxc-parser'), { recursive: true, dereference: true }); @@ -91,11 +102,11 @@ const selectedTargets = args.target ? Object.entries(targets) : [[currentTarget, targets[currentTarget]]]; -const packNativeBinding = (scope, name, binding) => { +const packNativeBinding = (scope, name, binding, version) => { rmSync(join(nm, scope), { recursive: true, force: true }); mkdirSync(join(nm, `${scope}/binding-${binding}`), { recursive: true }); const tmp = mkdtempSync(join(tmpdir(), 'oxc-')); - execSync(`npm pack ${scope}/binding-${binding}`, { cwd: tmp, stdio: 'pipe' }); + execSync(`npm pack ${scope}/binding-${binding}@${version}`, { cwd: tmp, stdio: 'pipe' }); execSync('tar -xzf *.tgz', { cwd: tmp, stdio: 'pipe' }); cpSync( execSync(`find ${tmp}/package -name "*.node"`, { encoding: 'utf-8' }).trim(), @@ -106,8 +117,8 @@ const packNativeBinding = (scope, name, binding) => { }; for (const [target, binding] of selectedTargets) { - packNativeBinding('@oxc-parser', 'parser', binding); - packNativeBinding('@oxc-resolver', 'resolver', binding); + packNativeBinding('@oxc-parser', 'parser', binding, oxcParserVersion); + packNativeBinding('@oxc-resolver', 'resolver', binding, oxcResolverVersion); execSync(`pnpm vsce package ${flags} --target ${target}`, { cwd: root, stdio: 'inherit' }); From 1a7781251f3b34e322ae5be3a65ac2e18f9f9057 Mon Sep 17 00:00:00 2001 From: Albert Miller Date: Sat, 16 May 2026 10:54:38 +0200 Subject: [PATCH 2/2] feat(vscode): add `knip.cwd` to run Knip from a sub-directory (#1667) --- packages/language-server/src/server.js | 2 +- packages/vscode-knip/package.json | 5 ++ packages/vscode-knip/src/index.js | 51 ++++++++++++++++--- templates/vscode-nested-root/README.md | 29 +++++++++++ .../repository.code-workspace | 11 ++++ .../repository/frontend/knip.json | 5 ++ .../repository/frontend/package.json | 6 +++ .../repository/frontend/src/index.ts | 3 ++ .../repository/frontend/src/unused.ts | 1 + .../repository/frontend/src/used.ts | 1 + .../repository/frontend/tsconfig.json | 8 +++ 11 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 templates/vscode-nested-root/README.md create mode 100644 templates/vscode-nested-root/repository.code-workspace create mode 100644 templates/vscode-nested-root/repository/frontend/knip.json create mode 100644 templates/vscode-nested-root/repository/frontend/package.json create mode 100644 templates/vscode-nested-root/repository/frontend/src/index.ts create mode 100644 templates/vscode-nested-root/repository/frontend/src/unused.ts create mode 100644 templates/vscode-nested-root/repository/frontend/src/used.ts create mode 100644 templates/vscode-nested-root/repository/frontend/tsconfig.json diff --git a/packages/language-server/src/server.js b/packages/language-server/src/server.js index ed0ff5ce6..5ee20682c 100644 --- a/packages/language-server/src/server.js +++ b/packages/language-server/src/server.js @@ -132,7 +132,7 @@ export class LanguageServer { if (!uri) return { capabilities: {} }; - this.cwd = fileURLToPath(uri); + this.cwd = params.initializationOptions?.cwd ?? fileURLToPath(uri); this.initConfig = params.initializationOptions?.config; diff --git a/packages/vscode-knip/package.json b/packages/vscode-knip/package.json index 2b492c1aa..1ba3a9c81 100644 --- a/packages/vscode-knip/package.json +++ b/packages/vscode-knip/package.json @@ -182,6 +182,11 @@ "default": "", "description": "Path to Knip configuration file (relative to workspace root)" }, + "knip.cwd": { + "type": "string", + "default": "", + "description": "Directory to run Knip from (its cwd), relative to the VS Code workspace folder. Use when the folder isn't the Knip project root, e.g. \"./typescript\", or \"../..\" for a monorepo root." + }, "knip.nodeRuntimePath": { "type": "string", "default": "", diff --git a/packages/vscode-knip/src/index.js b/packages/vscode-knip/src/index.js index 5d0b148bd..0a9c54c5c 100644 --- a/packages/vscode-knip/src/index.js +++ b/packages/vscode-knip/src/index.js @@ -34,6 +34,20 @@ const require = createRequire(import.meta.url); /** @param {string} value */ const toPosix = value => value.split(path.sep).join(path.posix.sep); +/** + * Directory Knip runs in (its cwd), from the `knip.cwd` setting. Absolute, or + * relative to the VS Code workspace folder; defaults to the folder itself. + * @param {import('vscode').WorkspaceFolder} folder + * @returns {string} + */ +function resolveKnipCwd(folder) { + const cwd = vscode.workspace.getConfiguration('knip', folder.uri).get('cwd', ''); + const base = folder.uri.fsPath; + const trimmed = typeof cwd === 'string' ? cwd.trim() : ''; + if (!trimmed) return base; + return path.isAbsolute(trimmed) ? path.normalize(trimmed) : path.resolve(base, trimmed); +} + export class Extension { /** @type {Extension | undefined} */ static #instance; @@ -145,7 +159,18 @@ export class Extension { } } - this.#outputChannel.info(`Starting Knip Language Server for ${folder.name}`); + const cwd = resolveKnipCwd(folder); + if (cwd !== folder.uri.fsPath && !existsSync(path.join(cwd, 'package.json'))) { + this.#outputChannel.warn( + `knip.cwd for ${folder.name} resolved to ${cwd}, but no package.json is there — Knip will fail. Check the "knip.cwd" setting.` + ); + } + + this.#outputChannel.info( + cwd === folder.uri.fsPath + ? `Starting Knip Language Server for ${folder.name}` + : `Starting Knip Language Server for ${folder.name} (cwd: ${cwd})` + ); const runtime = config.get('nodeRuntimePath', '') || 'node'; @@ -167,7 +192,7 @@ export class Extension { fileEvents: [vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, '**/*'))], }, workspaceFolder: folder, - initializationOptions: { config }, + initializationOptions: { config, cwd }, outputChannel: this.#outputChannel, outputChannelName: 'Knip', }; @@ -217,8 +242,18 @@ export class Extension { } #logManagedWorkspaces() { - const names = [...this.#clients.keys()].map(uri => fileURLToPath(uri)); - this.#outputChannel.info(`Managing ${this.#clients.size} workspace(s): ${names.join(', ')}`); + const parts = []; + for (const key of this.#clients.keys()) { + const folder = vscode.workspace.workspaceFolders?.find(f => f.uri.toString() === key); + if (folder) { + const cwd = resolveKnipCwd(folder); + const ws = folder.uri.fsPath; + parts.push(cwd === ws ? cwd : `${ws} (Knip cwd: ${cwd})`); + } else { + parts.push(vscode.Uri.parse(key).fsPath); + } + } + this.#outputChannel.info(`Managing ${this.#clients.size} workspace(s): ${parts.join(', ')}`); } /** @@ -450,7 +485,7 @@ export class Extension { const folder = vscode.workspace.getWorkspaceFolder(document.uri); if (!folder) return null; - const root = toPosix(folder.uri.fsPath); + const root = toPosix(resolveKnipCwd(folder)); if (path.basename(document.uri.fsPath) === 'package.json') { if (!config.get('editor.dependencies.hover.enabled', true)) return null; @@ -603,12 +638,14 @@ export class Extension { * @returns {Promise} */ async #hasKnipConfig(folder) { - const config = vscode.workspace.getConfiguration('knip'); + const config = vscode.workspace.getConfiguration('knip', folder.uri); const configFile = config.get('configFilePath', ''); const locations = configFile ? [configFile] : KNIP_CONFIG_LOCATIONS; + const rootUri = vscode.Uri.file(resolveKnipCwd(folder)); + for (const location of locations) { - const candidate = vscode.Uri.joinPath(folder.uri, location); + const candidate = vscode.Uri.joinPath(rootUri, location); try { await vscode.workspace.fs.stat(candidate); return true; diff --git a/templates/vscode-nested-root/README.md b/templates/vscode-nested-root/README.md new file mode 100644 index 000000000..94e7ae96c --- /dev/null +++ b/templates/vscode-nested-root/README.md @@ -0,0 +1,29 @@ +# vscode-nested-root — repro for PR #1667 + +The VS Code workspace folder (`repository/`) has **no `package.json`**. The +actual project lives one level down in `repository/frontend/`. + +- `repository.code-workspace` opens `./repository` and sets + `"knip.cwd": "./frontend"`. + +## Expected + +| Knip root | Result | +| -------------------- | ------ | +| `repository/` (no `knip.cwd`, current `main`) | ❌ `ERROR: Unable to find package.json` in the Knip Output channel | +| `repository/frontend/` (with `knip.cwd`, PR #1667) | ✅ runs; reports `src/unused.ts` (genuine unused file) | + +## CLI check + +```sh +cd repository && knip # → ERROR: Unable to find package.json +cd repository/frontend && knip # → src/unused.ts unused (correct) +``` + +## Test in the editor + +```sh +packages/vscode-knip/scripts/dev-install.sh +``` + +Then open `repository.code-workspace` and watch View → Output → "Knip". diff --git a/templates/vscode-nested-root/repository.code-workspace b/templates/vscode-nested-root/repository.code-workspace new file mode 100644 index 000000000..5f7b7078e --- /dev/null +++ b/templates/vscode-nested-root/repository.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "name": "repository", + "path": "./repository" + } + ], + "settings": { + "knip.cwd": "./frontend" + } +} diff --git a/templates/vscode-nested-root/repository/frontend/knip.json b/templates/vscode-nested-root/repository/frontend/knip.json new file mode 100644 index 000000000..d154c7431 --- /dev/null +++ b/templates/vscode-nested-root/repository/frontend/knip.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"] +} diff --git a/templates/vscode-nested-root/repository/frontend/package.json b/templates/vscode-nested-root/repository/frontend/package.json new file mode 100644 index 000000000..85d779bdc --- /dev/null +++ b/templates/vscode-nested-root/repository/frontend/package.json @@ -0,0 +1,6 @@ +{ + "name": "haven", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/templates/vscode-nested-root/repository/frontend/src/index.ts b/templates/vscode-nested-root/repository/frontend/src/index.ts new file mode 100644 index 000000000..8988bf9dc --- /dev/null +++ b/templates/vscode-nested-root/repository/frontend/src/index.ts @@ -0,0 +1,3 @@ +import { shelter } from './used.ts'; + +console.log(shelter()); diff --git a/templates/vscode-nested-root/repository/frontend/src/unused.ts b/templates/vscode-nested-root/repository/frontend/src/unused.ts new file mode 100644 index 000000000..1a90c67e0 --- /dev/null +++ b/templates/vscode-nested-root/repository/frontend/src/unused.ts @@ -0,0 +1 @@ +export const stranded = () => 'nobody imports this'; diff --git a/templates/vscode-nested-root/repository/frontend/src/used.ts b/templates/vscode-nested-root/repository/frontend/src/used.ts new file mode 100644 index 000000000..69d4a2f6f --- /dev/null +++ b/templates/vscode-nested-root/repository/frontend/src/used.ts @@ -0,0 +1 @@ +export const shelter = () => 'safe'; diff --git a/templates/vscode-nested-root/repository/frontend/tsconfig.json b/templates/vscode-nested-root/repository/frontend/tsconfig.json new file mode 100644 index 000000000..d8e497c91 --- /dev/null +++ b/templates/vscode-nested-root/repository/frontend/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true + }, + "include": ["src"] +}