diff --git a/README.md b/README.md index e797cf17..52032ecb 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,16 @@ pnpm install scip-typescript index --pnpm-workspaces ``` +### Index a TypeScript project using npm workspaces + +Navigate to the project root, containing `package.json` with a `workspaces` field. + +```sh +npm install + +scip-typescript index --npm-workspaces +``` + ### Indexing in CI Add the following run steps to your CI pipeline: diff --git a/snapshots/input/npm-workspaces/package.json b/snapshots/input/npm-workspaces/package.json new file mode 100644 index 00000000..32e3e5ed --- /dev/null +++ b/snapshots/input/npm-workspaces/package.json @@ -0,0 +1,16 @@ +{ + "name": "npm-workspaces", + "version": "1.0.0", + "description": "Example TS/JS project", + "main": "src/main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "private": true, + "packageManager": "npm@10.0.0", + "workspaces": [ + "packages/*" + ] +} diff --git a/snapshots/input/npm-workspaces/packages/a/package.json b/snapshots/input/npm-workspaces/packages/a/package.json new file mode 100644 index 00000000..0c881692 --- /dev/null +++ b/snapshots/input/npm-workspaces/packages/a/package.json @@ -0,0 +1,11 @@ +{ + "name": "@example/a", + "version": "1.0.0", + "description": "Example TS/JS project", + "main": "src/a.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/snapshots/input/npm-workspaces/packages/a/src/a.ts b/snapshots/input/npm-workspaces/packages/a/src/a.ts new file mode 100644 index 00000000..f7534476 --- /dev/null +++ b/snapshots/input/npm-workspaces/packages/a/src/a.ts @@ -0,0 +1,3 @@ +export function a(): string { + return '' +} diff --git a/snapshots/input/npm-workspaces/packages/a/tsconfig.json b/snapshots/input/npm-workspaces/packages/a/tsconfig.json new file mode 100644 index 00000000..6950116c --- /dev/null +++ b/snapshots/input/npm-workspaces/packages/a/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "outDir": "dist" + }, + "include": ["src/*"] +} diff --git a/snapshots/input/npm-workspaces/packages/b/package.json b/snapshots/input/npm-workspaces/packages/b/package.json new file mode 100644 index 00000000..28b64d44 --- /dev/null +++ b/snapshots/input/npm-workspaces/packages/b/package.json @@ -0,0 +1,14 @@ +{ + "name": "@example/b", + "version": "1.0.0", + "description": "Example TS/JS project", + "main": "src/b.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@example/a": "1.0.0" + } +} diff --git a/snapshots/input/npm-workspaces/packages/b/src/b.ts b/snapshots/input/npm-workspaces/packages/b/src/b.ts new file mode 100644 index 00000000..8dcbedc0 --- /dev/null +++ b/snapshots/input/npm-workspaces/packages/b/src/b.ts @@ -0,0 +1,5 @@ +import { a } from '@example/a' + +export function b() { + return a() +} diff --git a/snapshots/input/npm-workspaces/packages/b/tsconfig.json b/snapshots/input/npm-workspaces/packages/b/tsconfig.json new file mode 100644 index 00000000..61cd48e2 --- /dev/null +++ b/snapshots/input/npm-workspaces/packages/b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": "./src", + "sourceRoot": "src", + "outDir": "dist", + "paths": { + "@example/a": ["../../a/src/a.ts"] + } + }, + "include": ["src/*"], + "references": [{ "path": "../a" }] +} diff --git a/snapshots/input/npm-workspaces/tsconfig.json b/snapshots/input/npm-workspaces/tsconfig.json new file mode 100644 index 00000000..ae727a12 --- /dev/null +++ b/snapshots/input/npm-workspaces/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@sourcegraph/tsconfig", + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "allowJs": false, + "moduleResolution": "node", + "esModuleInterop": true, + "lib": ["esnext", "dom", "dom.iterable"], + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "noErrorTruncation": true, + "importHelpers": true, + "resolveJsonModule": true, + "composite": true, + "outDir": "out", + "rootDir": "." + }, + "include": [], + "exclude": ["out", "node_modules", "dist"] +} diff --git a/snapshots/output/npm-workspaces/packages/a/src/a.ts b/snapshots/output/npm-workspaces/packages/a/src/a.ts new file mode 100644 index 00000000..b04e75fd --- /dev/null +++ b/snapshots/output/npm-workspaces/packages/a/src/a.ts @@ -0,0 +1,8 @@ +// language TypeScript +// < definition @example/a 1.0.0 src/`a.ts`/ + +export function a(): string { +// ^ definition @example/a 1.0.0 src/`a.ts`/a(). + return '' +} + diff --git a/snapshots/output/npm-workspaces/packages/b/src/b.ts b/snapshots/output/npm-workspaces/packages/b/src/b.ts new file mode 100644 index 00000000..b5efc02a --- /dev/null +++ b/snapshots/output/npm-workspaces/packages/b/src/b.ts @@ -0,0 +1,13 @@ +// language TypeScript +// < definition @example/b 1.0.0 src/`b.ts`/ + +import { a } from '@example/a' +// ^ reference @example/a 1.0.0 src/`a.ts`/a(). +// ^^^^^^^^^^^^ reference @example/a 1.0.0 src/`a.ts`/ + +export function b() { +// ^ definition @example/b 1.0.0 src/`b.ts`/b(). + return a() +// ^ reference @example/a 1.0.0 src/`a.ts`/a(). +} + diff --git a/src/CommandLineOptions.test.ts b/src/CommandLineOptions.test.ts index aed2c31c..572042c9 100644 --- a/src/CommandLineOptions.test.ts +++ b/src/CommandLineOptions.test.ts @@ -33,6 +33,7 @@ checkIndexParser([], { checkIndexParser(['--cwd', 'qux'], { cwd: 'qux' }) checkIndexParser(['--yarn-workspaces'], { yarnWorkspaces: true }) checkIndexParser(['--pnpm-workspaces'], { pnpmWorkspaces: true }) +checkIndexParser(['--npm-workspaces'], { npmWorkspaces: true }) checkIndexParser(['--infer-tsconfig'], { inferTsconfig: true }) checkIndexParser(['--no-progress-bar'], { progressBar: false }) checkIndexParser(['--progress-bar'], { progressBar: true }) diff --git a/src/CommandLineOptions.ts b/src/CommandLineOptions.ts index 4892586f..6173c245 100644 --- a/src/CommandLineOptions.ts +++ b/src/CommandLineOptions.ts @@ -14,6 +14,7 @@ export interface MultiProjectOptions { yarnWorkspaces: boolean yarnBerryWorkspaces: boolean pnpmWorkspaces: boolean + npmWorkspaces: boolean globalCaches: boolean maxFileByteSize?: string maxFileByteSizeNumber?: number @@ -54,6 +55,7 @@ export function mainCommand( .command('index') .option('--cwd ', 'the working directory', process.cwd()) .option('--pnpm-workspaces', 'whether to index all pnpm workspaces', false) + .option('--npm-workspaces', 'whether to index all npm workspaces', false) .option('--yarn-workspaces', 'whether to index all yarn workspaces', false) .option( '--yarn-berry-workspaces', diff --git a/src/FileIndexer.test.ts b/src/FileIndexer.test.ts index ea8c7312..50c5294b 100644 --- a/src/FileIndexer.test.ts +++ b/src/FileIndexer.test.ts @@ -35,6 +35,7 @@ test('local prototype owners stay out of the global symbol table', () => { yarnWorkspaces: false, yarnBerryWorkspaces: false, pnpmWorkspaces: false, + npmWorkspaces: false, globalCaches: true, indexedProjects: new Set(), writeIndex: () => {}, @@ -88,6 +89,7 @@ test('lone surrogates in documentation and symbols survive serialization', () => yarnWorkspaces: false, yarnBerryWorkspaces: false, pnpmWorkspaces: false, + npmWorkspaces: false, globalCaches: true, indexedProjects: new Set(), writeIndex: () => {}, diff --git a/src/ProjectIndexer.test.ts b/src/ProjectIndexer.test.ts index 5df7f843..129450de 100644 --- a/src/ProjectIndexer.test.ts +++ b/src/ProjectIndexer.test.ts @@ -80,6 +80,7 @@ test('only deduplicates documents after successful emission', () => { yarnWorkspaces: false, yarnBerryWorkspaces: false, pnpmWorkspaces: false, + npmWorkspaces: false, globalCaches: false, indexedProjects: new Set(), writeIndex: () => { diff --git a/src/Svelte.test.ts b/src/Svelte.test.ts index a82c2c92..4d7a3a68 100644 --- a/src/Svelte.test.ts +++ b/src/Svelte.test.ts @@ -151,6 +151,7 @@ test('no-global-caches keeps Svelte source information project-local', () => { yarnWorkspaces: false, yarnBerryWorkspaces: false, pnpmWorkspaces: false, + npmWorkspaces: false, globalCaches: false, indexedProjects: new Set(), writeIndex: index => documents.push(...index.documents), diff --git a/src/main.test.ts b/src/main.test.ts index 4a0076ee..0a509399 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -90,13 +90,15 @@ for (const snapshotDirectory of snapshotDirectories) { const tsconfigJsonPath = path.join(inputRoot, 'tsconfig.json') const inferTsconfig = !fs.existsSync(tsconfigJsonPath) const output = path.join(inputRoot, 'index.scip') + const isNpm = Boolean(packageJson.packageManager?.startsWith('npm@')) indexCommand([], { cwd: inputRoot, inferTsconfig, output, - yarnWorkspaces: Boolean(packageJson.workspaces), + yarnWorkspaces: Boolean(packageJson.workspaces) && !isNpm, yarnBerryWorkspaces: false, pnpmWorkspaces: Boolean(packageJson.packageManager?.includes('pnpm')), + npmWorkspaces: isNpm, progressBar: false, indexedProjects: new Set(), globalCaches: true, diff --git a/src/main.ts b/src/main.ts index 90dc05c4..f107fa99 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,8 @@ export function indexCommand( projects.push(...listYarnWorkspaces(options.cwd, 'yarn2Plus')) } else if (options.pnpmWorkspaces) { projects.push(...listPnpmWorkspaces(options.cwd)) + } else if (options.npmWorkspaces) { + projects.push(...listNpmWorkspaces(options.cwd)) } else if (projects.length === 0) { projects.push(options.cwd) } @@ -251,6 +253,21 @@ function defaultCompilerOptions(configFileName?: string): ts.CompilerOptions { return options } +function listNpmWorkspaces(directory: string): string[] { + const { workspaces = [] } = JSON.parse( + fs.readFileSync(path.join(directory, 'package.json'), 'utf-8') + ) as { workspaces?: string[] | { packages?: string[] } } + const patterns = Array.isArray(workspaces) + ? workspaces + : (workspaces.packages ?? []) + return fs + .globSync( + patterns.map(pattern => path.posix.join(pattern, 'package.json')), + { cwd: directory, exclude: name => name === 'node_modules' } + ) + .map(packageJsonPath => path.join(directory, path.dirname(packageJsonPath))) +} + function listPnpmWorkspaces(directory: string): string[] { /** * Returns the list of projects formatted as: