Skip to content

Commit d30eec4

Browse files
authored
Use detect-monorepo for workspace root and rush detection (#175)
Use `detect-monorepo` to auto-detect the workspace root, replacing the hardcoded `workspaceRoot: "../.."` default. When `workspaceRoot` is not set explicitly (and `targetPackagePath` is not used), `resolveWorkspacePaths` now walks upward from the target package directory looking for `pnpm-workspace.yaml`, a `package.json` with a `workspaces` field, or `rush.json`. If detection fails and no `workspaceRoot` is configured, a clear error asks the user to set it explicitly. The `workspaceRoot` config option remains supported as an override for cases where auto-detection fails (e.g. unusually deep nesting). The existing `isRushWorkspace` helper is kept as a strict `rush.json` check at the passed-in directory. It is intentionally not replaced with `detectMonorepo`, because `detectMonorepo` walks upward and its rootDir can diverge from `workspaceRootDir`, which would break callers that compute lockfile importer ids or other paths relative to the same directory. Docs and CLI help text updated to reflect the new auto-detect behavior. Scope: packages (isolate-package) Visibility: user-facing
1 parent 2495daa commit d30eec4

7 files changed

Lines changed: 63 additions & 34 deletions

File tree

docs/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ or you can pass a configuration object directly:
3636

3737
```ts
3838
const packageNames = await getInternalPackageNames({
39-
workspaceRoot: "../..",
39+
includeDevDependencies: true,
4040
});
4141
```
4242

@@ -60,6 +60,6 @@ A helper for type-checked configuration files. Use it in `isolate.config.ts` or
6060
import { defineConfig } from "isolate-package";
6161

6262
export default defineConfig({
63-
workspaceRoot: "../..",
63+
includeDevDependencies: true,
6464
});
6565
```

docs/configuration.md

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ the `defineConfig` helper for type checking:
2222
import { defineConfig } from "isolate-package";
2323

2424
export default defineConfig({
25-
workspaceRoot: "../..",
25+
includeDevDependencies: true,
2626
});
2727
```
2828

@@ -156,29 +156,17 @@ from the root of the workspace.
156156

157157
### workspaceRoot
158158

159-
Type: `string`, default: `"../.."`
160-
161-
The relative path to the root of the workspace / monorepo. In a typical setup
162-
you will have a `packages` directory and possibly also an `apps` and a
163-
`services` directory, all of which contain packages. So any package you would
164-
want to isolate is located 2 levels up from the root.
159+
Type: `string | undefined`, default: `undefined`
165160

166-
For example
161+
The relative path from the target package directory to the root of the
162+
workspace / monorepo. When omitted, the workspace root is auto-detected by
163+
walking upward from the target package directory and looking for a
164+
`pnpm-workspace.yaml`, a `package.json` with a `workspaces` field, or a
165+
`rush.json`.
167166

168-
```
169-
packages
170-
├─ backend
171-
│ └─ package.json
172-
└─ ui
173-
└─ package.json
174-
apps
175-
├─ admin
176-
│ └─ package.json
177-
└─ web
178-
└─ package.json
179-
services
180-
└─ api
181-
└─ package.json
182-
```
167+
You only need to set this explicitly when auto-detection fails, for example
168+
when the target package is nested more than a few levels deep inside the
169+
workspace.
183170

184-
When you use the `targetPackagePath` option, this setting will be ignored.
171+
When you use the `targetPackagePath` option, this setting is ignored and the
172+
workspace root is assumed to be the current working directory.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@pnpm/logger": "^1001.0.1",
5656
"@pnpm/types": "^1001.3.0",
5757
"consola": "^3.4.2",
58+
"detect-monorepo": "^1.0.0",
5859
"fs-extra": "^11.3.4",
5960
"get-or-throw": "^3.0.1",
6061
"get-tsconfig": "^4.13.7",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/isolate-bin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ const cli = meow(
2424
-t, --target-package-path <path> Path to the target package
2525
-c, --tsconfig-path <path> Path to tsconfig.json (default: ./tsconfig.json)
2626
-w, --workspace-packages <glob> Workspace package globs (repeatable)
27-
-r, --workspace-root <path> Path to the workspace root (default: ../..)
27+
-r, --workspace-root <path> Path to the workspace root (auto-detected by default)
2828
--force-npm Force npm lockfile generation
2929
-p, --pick-from-scripts <name> Scripts to include (repeatable)
3030
--omit-from-scripts <name> Scripts to exclude (repeatable)
3131
--omit-package-manager Omit the packageManager field from the manifest
3232
3333
Examples
3434
$ isolate --log-level debug
35-
$ isolate --force-npm --workspace-root ../..
35+
$ isolate --force-npm
3636
$ isolate --pick-from-scripts build --pick-from-scripts start
3737
`,
3838
{

src/lib/config.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { execFileSync } from "node:child_process";
2+
import { detectMonorepo } from "detect-monorepo";
23
import fs from "fs-extra";
34
import path from "node:path";
45
import { pathToFileURL } from "node:url";
@@ -14,7 +15,13 @@ export type IsolateConfigResolved = {
1415
targetPackagePath?: string;
1516
tsconfigPath: string;
1617
workspacePackages?: string[];
17-
workspaceRoot: string;
18+
/**
19+
* Path to the workspace root, relative to the target package directory.
20+
* When omitted, the workspace root is auto-detected by walking upward from
21+
* the target package directory looking for a pnpm-workspace.yaml, a
22+
* package.json with a `workspaces` field, or a rush.json.
23+
*/
24+
workspaceRoot?: string;
1825
forceNpm: boolean;
1926
pickFromScripts?: string[];
2027
omitFromScripts?: string[];
@@ -31,7 +38,7 @@ const configDefaults: IsolateConfigResolved = {
3138
targetPackagePath: undefined,
3239
tsconfigPath: "./tsconfig.json",
3340
workspacePackages: undefined,
34-
workspaceRoot: "../..",
41+
workspaceRoot: undefined,
3542
forceNpm: false,
3643
pickFromScripts: undefined,
3744
omitFromScripts: undefined,
@@ -170,17 +177,35 @@ function validateConfig(config: IsolateConfig) {
170177
* Resolve the target package directory and workspace root directory from the
171178
* configuration. When targetPackagePath is set, the config is assumed to live
172179
* at the workspace root. Otherwise it lives in the target package directory.
180+
*
181+
* When `workspaceRoot` is not explicitly set, auto-detect the monorepo root by
182+
* walking upward from the target package directory.
173183
*/
174184
export function resolveWorkspacePaths(config: IsolateConfigResolved) {
175185
const targetPackageDir = config.targetPackagePath
176186
? path.join(process.cwd(), config.targetPackagePath)
177187
: process.cwd();
178188

179-
const workspaceRootDir = config.targetPackagePath
180-
? process.cwd()
181-
: path.join(targetPackageDir, config.workspaceRoot);
189+
if (config.targetPackagePath) {
190+
return { targetPackageDir, workspaceRootDir: process.cwd() };
191+
}
192+
193+
if (config.workspaceRoot !== undefined) {
194+
return {
195+
targetPackageDir,
196+
workspaceRootDir: path.join(targetPackageDir, config.workspaceRoot),
197+
};
198+
}
199+
200+
const detected = detectMonorepo(targetPackageDir);
201+
202+
if (!detected) {
203+
throw new Error(
204+
`Failed to auto-detect monorepo workspace root from ${targetPackageDir}. Set the 'workspaceRoot' config option explicitly.`,
205+
);
206+
}
182207

183-
return { targetPackageDir, workspaceRootDir };
208+
return { targetPackageDir, workspaceRootDir: detected.rootDir };
184209
}
185210

186211
export function resolveConfig(

src/lib/utils/is-rush-workspace.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import path from "node:path";
44
/**
55
* Detect if this is a Rush monorepo. They use a very different structure so
66
* there are multiple places where we need to make exceptions based on this.
7+
*
8+
* This intentionally only checks the passed-in directory. Using the upward
9+
* walk of `detectMonorepo` here would break callers that pass a subdirectory
10+
* of the actual Rush root, because downstream code builds paths (like
11+
* `common/config/rush`) and lockfile importer ids relative to the same
12+
* directory it gets.
713
*/
814
export function isRushWorkspace(workspaceRootDir: string) {
915
return fs.existsSync(path.join(workspaceRootDir, "rush.json"));

0 commit comments

Comments
 (0)