@openrewrite/rewrite 8.70.3 → 8.70.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/javascript/add-import.d.ts +5 -0
  2. package/dist/javascript/add-import.d.ts.map +1 -1
  3. package/dist/javascript/add-import.js +22 -9
  4. package/dist/javascript/add-import.js.map +1 -1
  5. package/dist/javascript/assertions.d.ts.map +1 -1
  6. package/dist/javascript/assertions.js +45 -13
  7. package/dist/javascript/assertions.js.map +1 -1
  8. package/dist/javascript/dependency-workspace.d.ts +5 -0
  9. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  10. package/dist/javascript/dependency-workspace.js +47 -13
  11. package/dist/javascript/dependency-workspace.js.map +1 -1
  12. package/dist/javascript/package-json-parser.d.ts +24 -0
  13. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  14. package/dist/javascript/package-json-parser.js +137 -31
  15. package/dist/javascript/package-json-parser.js.map +1 -1
  16. package/dist/javascript/package-manager.d.ts +45 -7
  17. package/dist/javascript/package-manager.d.ts.map +1 -1
  18. package/dist/javascript/package-manager.js +83 -45
  19. package/dist/javascript/package-manager.js.map +1 -1
  20. package/dist/javascript/recipes/add-dependency.d.ts +7 -3
  21. package/dist/javascript/recipes/add-dependency.d.ts.map +1 -1
  22. package/dist/javascript/recipes/add-dependency.js +71 -13
  23. package/dist/javascript/recipes/add-dependency.js.map +1 -1
  24. package/dist/javascript/recipes/upgrade-dependency-version.d.ts +7 -3
  25. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  26. package/dist/javascript/recipes/upgrade-dependency-version.js +71 -13
  27. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  28. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts +7 -3
  29. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
  30. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +71 -13
  31. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
  32. package/dist/path-utils.d.ts +45 -0
  33. package/dist/path-utils.d.ts.map +1 -0
  34. package/dist/path-utils.js +210 -0
  35. package/dist/path-utils.js.map +1 -0
  36. package/dist/version.txt +1 -1
  37. package/package.json +1 -1
  38. package/src/javascript/add-import.ts +28 -7
  39. package/src/javascript/assertions.ts +48 -6
  40. package/src/javascript/dependency-workspace.ts +66 -13
  41. package/src/javascript/package-json-parser.ts +169 -39
  42. package/src/javascript/package-manager.ts +120 -52
  43. package/src/javascript/recipes/add-dependency.ts +89 -17
  44. package/src/javascript/recipes/upgrade-dependency-version.ts +89 -17
  45. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +89 -17
  46. package/src/path-utils.ts +208 -0
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.DEFAULT_DIR_EXCLUSIONS = void 0;
46
+ exports.isGitIgnored = isGitIgnored;
47
+ exports.isInGitRepo = isInGitRepo;
48
+ exports.walkDirs = walkDirs;
49
+ exports.getGitTrackedFiles = getGitTrackedFiles;
50
+ /*
51
+ * Copyright 2025 the original author or authors.
52
+ * <p>
53
+ * Licensed under the Moderne Source Available License (the "License");
54
+ * you may not use this file except in compliance with the License.
55
+ * You may obtain a copy of the License at
56
+ * <p>
57
+ * https://docs.moderne.io/licensing/moderne-source-available-license
58
+ * <p>
59
+ * Unless required by applicable law or agreed to in writing, software
60
+ * distributed under the License is distributed on an "AS IS" BASIS,
61
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
62
+ * See the License for the specific language governing permissions and
63
+ * limitations under the License.
64
+ */
65
+ const fs = __importStar(require("fs"));
66
+ const fsp = __importStar(require("fs/promises"));
67
+ const path = __importStar(require("path"));
68
+ const child_process_1 = require("child_process");
69
+ /**
70
+ * Default directory exclusions for file/directory discovery.
71
+ */
72
+ exports.DEFAULT_DIR_EXCLUSIONS = new Set([
73
+ "node_modules",
74
+ ".git",
75
+ ".svn",
76
+ ".hg",
77
+ "dist",
78
+ "build",
79
+ "out",
80
+ "coverage",
81
+ ".next",
82
+ ".nuxt",
83
+ ".output"
84
+ ]);
85
+ /**
86
+ * Checks if a path is ignored by Git.
87
+ * Returns true if the path is ignored, false otherwise.
88
+ * Falls back to false if not in a git repository.
89
+ */
90
+ function isGitIgnored(filePath, cwd) {
91
+ const result = (0, child_process_1.spawnSync)("git", ["check-ignore", "-q", filePath], {
92
+ cwd: cwd || path.dirname(filePath),
93
+ encoding: "utf8"
94
+ });
95
+ // Exit code 0 means the file IS ignored
96
+ // Exit code 1 means the file is NOT ignored
97
+ // Other exit codes indicate git errors (not a repo, etc.)
98
+ return result.status === 0;
99
+ }
100
+ /**
101
+ * Checks if we're in a git repository.
102
+ */
103
+ function isInGitRepo(dir) {
104
+ const result = (0, child_process_1.spawnSync)("git", ["rev-parse", "--git-dir"], {
105
+ cwd: dir,
106
+ encoding: "utf8"
107
+ });
108
+ return result.status === 0;
109
+ }
110
+ /**
111
+ * Recursively walks directories starting from a given path.
112
+ * Returns all subdirectory paths (not including the root).
113
+ */
114
+ function walkDirs(rootDir_1) {
115
+ return __awaiter(this, arguments, void 0, function* (rootDir, options = {}) {
116
+ const { maxDepth, excludeDirs = exports.DEFAULT_DIR_EXCLUSIONS, respectGitIgnore = false, mustContainFile } = options;
117
+ const results = [];
118
+ const inGitRepo = respectGitIgnore && isInGitRepo(rootDir);
119
+ function walk(dir, depth) {
120
+ return __awaiter(this, void 0, void 0, function* () {
121
+ if (maxDepth !== undefined && depth > maxDepth) {
122
+ return;
123
+ }
124
+ let entries;
125
+ try {
126
+ entries = yield fsp.readdir(dir, { withFileTypes: true });
127
+ }
128
+ catch (_a) {
129
+ return;
130
+ }
131
+ for (const entry of entries) {
132
+ if (!entry.isDirectory()) {
133
+ continue;
134
+ }
135
+ // Skip excluded directories
136
+ if (excludeDirs.has(entry.name)) {
137
+ continue;
138
+ }
139
+ // Skip hidden directories
140
+ if (entry.name.startsWith('.')) {
141
+ continue;
142
+ }
143
+ const fullPath = path.join(dir, entry.name);
144
+ // Check git ignore if enabled
145
+ if (inGitRepo && isGitIgnored(fullPath, rootDir)) {
146
+ continue;
147
+ }
148
+ // Check for required file if specified
149
+ if (mustContainFile) {
150
+ const requiredFile = path.join(fullPath, mustContainFile);
151
+ if (fs.existsSync(requiredFile)) {
152
+ results.push(fullPath);
153
+ }
154
+ }
155
+ else {
156
+ results.push(fullPath);
157
+ }
158
+ // Recurse into subdirectory
159
+ yield walk(fullPath, depth + 1);
160
+ }
161
+ });
162
+ }
163
+ yield walk(rootDir, 0);
164
+ return results;
165
+ });
166
+ }
167
+ /**
168
+ * Gets files tracked by git (and untracked but not ignored files).
169
+ * Falls back to empty array if not in a git repository.
170
+ */
171
+ function getGitTrackedFiles(dir) {
172
+ const files = [];
173
+ // Get tracked files
174
+ const tracked = (0, child_process_1.spawnSync)("git", ["ls-files"], {
175
+ cwd: dir,
176
+ encoding: "utf8"
177
+ });
178
+ if (tracked.status !== 0 || tracked.error) {
179
+ return [];
180
+ }
181
+ if (tracked.stdout) {
182
+ for (const line of tracked.stdout.split("\n")) {
183
+ const trimmed = line.trim();
184
+ if (trimmed) {
185
+ const fullPath = path.join(dir, trimmed);
186
+ if (fs.existsSync(fullPath)) {
187
+ files.push(fullPath);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ // Get untracked but not ignored files
193
+ const untracked = (0, child_process_1.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
194
+ cwd: dir,
195
+ encoding: "utf8"
196
+ });
197
+ if (untracked.stdout) {
198
+ for (const line of untracked.stdout.split("\n")) {
199
+ const trimmed = line.trim();
200
+ if (trimmed) {
201
+ const fullPath = path.join(dir, trimmed);
202
+ if (fs.existsSync(fullPath)) {
203
+ files.push(fullPath);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ return files;
209
+ }
210
+ //# sourceMappingURL=path-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"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"}
package/dist/version.txt CHANGED
@@ -1 +1 @@
1
- 8.70.3
1
+ 8.70.4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openrewrite/rewrite",
3
- "version": "8.70.3",
3
+ "version": "8.70.4",
4
4
  "license": "Moderne Source Available License",
5
5
  "description": "OpenRewrite JavaScript.",
6
6
  "homepage": "https://github.com/openrewrite/rewrite",
@@ -39,6 +39,11 @@ export interface AddImportOptions {
39
39
  * Cannot be combined with `member`, `alias`, or `onlyIfReferenced`. */
40
40
  sideEffectOnly?: boolean;
41
41
 
42
+ /** If true, adds a type-only import (e.g., `import type { Foo } from 'module'`).
43
+ * Type-only imports are erased at compile time and do not generate runtime code.
44
+ * Cannot be combined with `sideEffectOnly`. */
45
+ typeOnly?: boolean;
46
+
42
47
  /** Optional import style to use. If not specified, auto-detects from file and existing imports */
43
48
  style?: ImportStyle;
44
49
  }
@@ -77,7 +82,8 @@ export function maybeAddImport(
77
82
  v.module === options.module &&
78
83
  v.member === options.member &&
79
84
  v.alias === options.alias &&
80
- v.sideEffectOnly === (options.sideEffectOnly ?? false)) {
85
+ v.sideEffectOnly === (options.sideEffectOnly ?? false) &&
86
+ v.typeOnly === (options.typeOnly ?? false)) {
81
87
  return;
82
88
  }
83
89
  }
@@ -90,6 +96,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
90
96
  readonly alias?: string;
91
97
  readonly onlyIfReferenced: boolean;
92
98
  readonly sideEffectOnly: boolean;
99
+ readonly typeOnly: boolean;
93
100
  readonly style?: ImportStyle;
94
101
 
95
102
  constructor(options: AddImportOptions) {
@@ -116,6 +123,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
116
123
  if (options.onlyIfReferenced !== undefined) {
117
124
  throw new Error("Cannot combine sideEffectOnly with onlyIfReferenced");
118
125
  }
126
+ if (options.typeOnly) {
127
+ throw new Error("Cannot combine sideEffectOnly with typeOnly");
128
+ }
119
129
  }
120
130
 
121
131
  this.module = options.module;
@@ -123,6 +133,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
123
133
  this.alias = options.alias;
124
134
  this.onlyIfReferenced = options.onlyIfReferenced ?? true;
125
135
  this.sideEffectOnly = options.sideEffectOnly ?? false;
136
+ this.typeOnly = options.typeOnly ?? false;
126
137
  this.style = options.style;
127
138
  }
128
139
 
@@ -444,6 +455,11 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
444
455
  continue;
445
456
  }
446
457
 
458
+ // Only merge into imports with matching typeOnly - don't mix type and value imports
459
+ if (importClause.typeOnly !== this.typeOnly) {
460
+ continue;
461
+ }
462
+
447
463
  // Case 1: Existing import has named bindings - merge into them
448
464
  if (importClause.namedBindings) {
449
465
  // Only merge into NamedImports, not namespace imports
@@ -631,6 +647,11 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
631
647
  return false;
632
648
  }
633
649
 
650
+ // Check if the typeOnly flag matches - type-only and value imports are separate
651
+ if (importClause.typeOnly !== this.typeOnly) {
652
+ return false;
653
+ }
654
+
634
655
  // Check if the specific member or default import already exists
635
656
  if (this.member === '*') {
636
657
  // We're adding a namespace import, check if one exists
@@ -1053,9 +1074,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1053
1074
  importClause = {
1054
1075
  id: randomId(),
1055
1076
  kind: JS.Kind.ImportClause,
1056
- prefix: emptySpace,
1077
+ prefix: this.typeOnly ? singleSpace : emptySpace,
1057
1078
  markers: emptyMarkers,
1058
- typeOnly: false,
1079
+ typeOnly: this.typeOnly,
1059
1080
  name: undefined,
1060
1081
  namedBindings: namespaceBinding
1061
1082
  };
@@ -1076,9 +1097,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1076
1097
  importClause = {
1077
1098
  id: randomId(),
1078
1099
  kind: JS.Kind.ImportClause,
1079
- prefix: emptySpace,
1100
+ prefix: this.typeOnly ? singleSpace : emptySpace,
1080
1101
  markers: emptyMarkers,
1081
- typeOnly: false,
1102
+ typeOnly: this.typeOnly,
1082
1103
  name: rightPadded(defaultName, emptySpace),
1083
1104
  namedBindings: undefined
1084
1105
  };
@@ -1110,9 +1131,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1110
1131
  importClause = {
1111
1132
  id: randomId(),
1112
1133
  kind: JS.Kind.ImportClause,
1113
- prefix: emptySpace,
1134
+ prefix: this.typeOnly ? singleSpace : emptySpace,
1114
1135
  markers: emptyMarkers,
1115
- typeOnly: false,
1136
+ typeOnly: this.typeOnly,
1116
1137
  name: undefined,
1117
1138
  namedBindings: namedImports
1118
1139
  };
@@ -55,9 +55,14 @@ export async function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]
55
55
  );
56
56
  }
57
57
 
58
- // Write non-JS/TS files to disk FIRST so Prettier config detection can find them
58
+ // Write non-JS/TS/JSON files to disk FIRST so Prettier config detection can find them
59
+ // Exclude all package.json files (root and workspace members) - they're handled separately
59
60
  for (const spec of sourceSpecs) {
60
- if (spec.path !== 'package.json' && spec.path !== 'package-lock.json' && spec.kind !== JS.Kind.CompilationUnit) {
61
+ const isPackageJson = spec.path?.endsWith('package.json');
62
+ const isPackageLock = spec.path === 'package-lock.json';
63
+ const isJsTs = spec.kind === JS.Kind.CompilationUnit;
64
+
65
+ if (!isPackageJson && !isPackageLock && !isJsTs) {
61
66
  if (spec.before && spec.path) {
62
67
  const filePath = path.join(relativeTo, spec.path);
63
68
  const dir = path.dirname(filePath);
@@ -69,22 +74,37 @@ export async function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]
69
74
  }
70
75
  }
71
76
 
77
+ // Collect all package.json specs (root and workspace members)
78
+ const allPackageJsonSpecs = sourceSpecs.filter(spec => spec.path?.endsWith('package.json'));
79
+
72
80
  // Yield package.json FIRST so its PackageJsonParser is used for all JSON specs
73
81
  // (The test framework uses the first spec's parser for all specs of the same kind)
74
82
  if (packageJsonSpec) {
75
- // Parse package.json to check if there are dependencies
83
+ // Parse package.json to check if there are dependencies or workspaces
76
84
  const packageJsonContent = JSON.parse(packageJsonSpec.before!);
77
85
  const hasDependencies = Object.keys({
78
86
  ...packageJsonContent.dependencies,
79
87
  ...packageJsonContent.devDependencies
80
88
  }).length > 0;
89
+ const hasWorkspaces = Array.isArray(packageJsonContent.workspaces) && packageJsonContent.workspaces.length > 0;
81
90
 
82
91
  // Get or create cached workspace with node_modules
83
92
  // If packageLockSpec is provided, use it for deterministic installs with npm ci
84
- if (hasDependencies) {
93
+ // For workspaces, include all workspace member package.json files
94
+ if (hasDependencies || hasWorkspaces) {
95
+ // Build workspace packages map for DependencyWorkspace
96
+ const workspacePackages: Record<string, string> | undefined = hasWorkspaces
97
+ ? Object.fromEntries(
98
+ allPackageJsonSpecs
99
+ .filter(spec => spec.path !== 'package.json' && spec.before)
100
+ .map(spec => [spec.path!, spec.before!])
101
+ )
102
+ : undefined;
103
+
85
104
  const cachedWorkspace = await DependencyWorkspace.getOrCreateWorkspace({
86
105
  packageJsonContent: packageJsonSpec.before!,
87
- packageLockContent: packageLockSpec?.before ?? undefined
106
+ packageLockContent: packageLockSpec?.before ?? undefined,
107
+ workspacePackages
88
108
  });
89
109
 
90
110
  // Symlink node_modules from cached workspace to test directory
@@ -113,6 +133,26 @@ export async function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]
113
133
  };
114
134
  }
115
135
 
136
+ // Write and yield workspace member package.json files with PackageJsonParser
137
+ for (const spec of allPackageJsonSpecs) {
138
+ if (spec.path !== 'package.json') {
139
+ // Write workspace member package.json to disk
140
+ if (spec.before && spec.path) {
141
+ const filePath = path.join(relativeTo, spec.path);
142
+ const dir = path.dirname(filePath);
143
+ if (!fs.existsSync(dir)) {
144
+ fs.mkdirSync(dir, { recursive: true });
145
+ }
146
+ fs.writeFileSync(filePath, spec.before);
147
+ }
148
+
149
+ yield {
150
+ ...spec,
151
+ parser: () => new PackageJsonParser({relativeTo})
152
+ };
153
+ }
154
+ }
155
+
116
156
  // Yield package-lock.json after package.json (so PackageJsonParser is used as the group parser)
117
157
  if (packageLockSpec) {
118
158
  yield packageLockSpec;
@@ -150,7 +190,9 @@ export async function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]
150
190
  }
151
191
 
152
192
  for (const spec of sourceSpecs) {
153
- if (spec.path !== 'package.json' && spec.path !== 'package-lock.json') {
193
+ const isPackageJson = spec.path?.endsWith('package.json');
194
+ const isPackageLock = spec.path === 'package-lock.json';
195
+ if (!isPackageJson && !isPackageLock) {
154
196
  if (spec.kind === JS.Kind.CompilationUnit) {
155
197
  // For JS/TS files, use a parser that adds style marker if available
156
198
  // spec.path may be undefined, so generate a reasonable path from the extension
@@ -49,6 +49,11 @@ interface PackageJsonWorkspaceOptions extends BaseWorkspaceOptions {
49
49
  * - `npm ci` is used instead of `npm install` (faster, deterministic)
50
50
  */
51
51
  packageLockContent?: string;
52
+ /**
53
+ * Optional workspace member package.json files.
54
+ * Keys are relative paths (e.g., "packages/foo/package.json"), values are content.
55
+ */
56
+ workspacePackages?: Record<string, string>;
52
57
  }
53
58
 
54
59
  /**
@@ -77,20 +82,44 @@ export class DependencyWorkspace {
77
82
  // Extract dependencies from package.json content if provided
78
83
  let dependencies: Record<string, string> | undefined = options.dependencies;
79
84
  let parsedPackageJson: Record<string, any> | undefined;
85
+ let workspacePackages: Record<string, string> | undefined;
86
+
80
87
  if (options.packageJsonContent) {
81
88
  parsedPackageJson = JSON.parse(options.packageJsonContent);
82
89
  dependencies = {
83
90
  ...parsedPackageJson?.dependencies,
84
91
  ...parsedPackageJson?.devDependencies
85
92
  };
93
+ workspacePackages = options.workspacePackages;
94
+
95
+ // For workspaces, also collect dependencies from workspace members
96
+ if (workspacePackages) {
97
+ for (const content of Object.values(workspacePackages)) {
98
+ const memberPkg = JSON.parse(content);
99
+ dependencies = {
100
+ ...dependencies,
101
+ ...memberPkg?.dependencies,
102
+ ...memberPkg?.devDependencies
103
+ };
104
+ }
105
+ }
86
106
  }
87
107
 
88
- if (!dependencies || Object.keys(dependencies).length === 0) {
108
+ // For workspaces without explicit dependencies in root, we still need to run install
109
+ const hasWorkspaces = parsedPackageJson?.workspaces && Array.isArray(parsedPackageJson.workspaces);
110
+ if ((!dependencies || Object.keys(dependencies).length === 0) && !hasWorkspaces) {
89
111
  throw new Error('No dependencies provided');
90
112
  }
91
113
 
92
114
  // Use the refactored internal method
93
- return this.createWorkspace(dependencies, parsedPackageJson, options.packageJsonContent, options.packageLockContent, options.targetDir);
115
+ return this.createWorkspace(
116
+ dependencies || {},
117
+ parsedPackageJson,
118
+ options.packageJsonContent,
119
+ options.packageLockContent,
120
+ options.targetDir,
121
+ workspacePackages
122
+ );
94
123
  }
95
124
 
96
125
  /**
@@ -101,14 +130,23 @@ export class DependencyWorkspace {
101
130
  parsedPackageJson: Record<string, any> | undefined,
102
131
  packageJsonContent: string | undefined,
103
132
  packageLockContent: string | undefined,
104
- targetDir: string | undefined
133
+ targetDir: string | undefined,
134
+ workspacePackages?: Record<string, string>
105
135
  ): Promise<string> {
106
136
  // Determine hash based on lock file (most precise) or dependencies
107
137
  // Note: We always hash dependencies (not packageJsonContent) because whitespace/formatting
108
138
  // differences in package.json shouldn't create different workspaces
109
- const hash = packageLockContent
110
- ? this.hashContent(packageLockContent)
111
- : this.hashDependencies(dependencies);
139
+ // For workspaces, include workspace package paths in the hash
140
+ let hash: string;
141
+ if (packageLockContent) {
142
+ hash = this.hashContent(packageLockContent);
143
+ } else if (workspacePackages) {
144
+ // Include workspace package paths in hash for workspace setups
145
+ const workspacePaths = Object.keys(workspacePackages).sort().join(',');
146
+ hash = this.hashContent(this.hashDependencies(dependencies) + ':' + workspacePaths);
147
+ } else {
148
+ hash = this.hashDependencies(dependencies);
149
+ }
112
150
 
113
151
  // Determine npm command: use `npm ci` when lock file is provided (faster, deterministic)
114
152
  const npmCommand = packageLockContent ? 'npm ci --silent' : 'npm install --silent';
@@ -134,11 +172,26 @@ export class DependencyWorkspace {
134
172
  if (packageLockContent) {
135
173
  fs.writeFileSync(path.join(dir, 'package-lock.json'), packageLockContent);
136
174
  }
175
+
176
+ // Write workspace member package.json files
177
+ if (workspacePackages) {
178
+ for (const [relativePath, content] of Object.entries(workspacePackages)) {
179
+ const fullPath = path.join(dir, relativePath);
180
+ const memberDir = path.dirname(fullPath);
181
+ if (!fs.existsSync(memberDir)) {
182
+ fs.mkdirSync(memberDir, {recursive: true});
183
+ }
184
+ fs.writeFileSync(fullPath, content);
185
+ }
186
+ }
137
187
  };
138
188
 
189
+ // For workspaces, skip dependency validation (combined deps don't match root package.json)
190
+ const depsForValidation = workspacePackages ? undefined : dependencies;
191
+
139
192
  if (targetDir) {
140
193
  // Use provided directory - check if it's already valid
141
- if (this.isWorkspaceValid(targetDir, dependencies)) {
194
+ if (this.isWorkspaceValid(targetDir, depsForValidation)) {
142
195
  return targetDir;
143
196
  }
144
197
 
@@ -149,7 +202,7 @@ export class DependencyWorkspace {
149
202
  const cachedWorkspaceDir = path.join(this.WORKSPACE_BASE, hash);
150
203
  const cachedNodeModules = path.join(cachedWorkspaceDir, 'node_modules');
151
204
 
152
- if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, dependencies)) {
205
+ if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, depsForValidation)) {
153
206
  // Symlink node_modules from cached workspace
154
207
  try {
155
208
  const targetNodeModules = path.join(targetDir, 'node_modules');
@@ -190,7 +243,7 @@ export class DependencyWorkspace {
190
243
 
191
244
  // Check cache
192
245
  const cached = this.cache.get(hash);
193
- if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, dependencies)) {
246
+ if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, depsForValidation)) {
194
247
  return cached;
195
248
  }
196
249
 
@@ -198,7 +251,7 @@ export class DependencyWorkspace {
198
251
  const workspaceDir = path.join(this.WORKSPACE_BASE, hash);
199
252
 
200
253
  // Check if valid workspace already exists on disk (cross-VM reuse)
201
- if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, dependencies)) {
254
+ if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, depsForValidation)) {
202
255
  this.cache.set(hash, workspaceDir);
203
256
  return workspaceDir;
204
257
  }
@@ -241,7 +294,7 @@ export class DependencyWorkspace {
241
294
  if (error.code === 'EEXIST' || error.code === 'ENOTEMPTY' || error.code === 'EISDIR' ||
242
295
  (error.code === 'EPERM' && fs.existsSync(workspaceDir))) {
243
296
  // Target exists - check if it's valid
244
- if (this.isWorkspaceValid(workspaceDir, dependencies)) {
297
+ if (this.isWorkspaceValid(workspaceDir, depsForValidation)) {
245
298
  // Another process created a valid workspace - use theirs
246
299
  moved = true; // Don't try again
247
300
  } else {
@@ -261,7 +314,7 @@ export class DependencyWorkspace {
261
314
  moved = true;
262
315
  } catch (copyError) {
263
316
  // Check if another process created it while we were copying
264
- if (this.isWorkspaceValid(workspaceDir, dependencies)) {
317
+ if (this.isWorkspaceValid(workspaceDir, depsForValidation)) {
265
318
  moved = true;
266
319
  } else {
267
320
  throw error;
@@ -284,7 +337,7 @@ export class DependencyWorkspace {
284
337
  }
285
338
 
286
339
  // Verify final workspace is valid (might be from another process)
287
- if (!this.isWorkspaceValid(workspaceDir, dependencies)) {
340
+ if (!this.isWorkspaceValid(workspaceDir, depsForValidation)) {
288
341
  throw new Error('Failed to create valid workspace due to concurrent modifications');
289
342
  }
290
343