@run0/jiki 0.1.0

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 (152) hide show
  1. package/dist/browser-bundle.d.ts +40 -0
  2. package/dist/builtins.d.ts +22 -0
  3. package/dist/code-transform.d.ts +7 -0
  4. package/dist/config/cdn.d.ts +13 -0
  5. package/dist/container.d.ts +101 -0
  6. package/dist/dev-server.d.ts +69 -0
  7. package/dist/errors.d.ts +19 -0
  8. package/dist/frameworks/code-transforms.d.ts +32 -0
  9. package/dist/frameworks/next-api-handler.d.ts +72 -0
  10. package/dist/frameworks/next-dev-server.d.ts +141 -0
  11. package/dist/frameworks/next-html-generator.d.ts +36 -0
  12. package/dist/frameworks/next-route-resolver.d.ts +19 -0
  13. package/dist/frameworks/next-shims.d.ts +78 -0
  14. package/dist/frameworks/remix-dev-server.d.ts +47 -0
  15. package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
  16. package/dist/frameworks/vite-dev-server.d.ts +50 -0
  17. package/dist/fs-errors.d.ts +36 -0
  18. package/dist/index.cjs +14916 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +61 -0
  21. package/dist/index.mjs +14898 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/kernel.d.ts +48 -0
  24. package/dist/memfs.d.ts +144 -0
  25. package/dist/metrics.d.ts +78 -0
  26. package/dist/module-resolver.d.ts +60 -0
  27. package/dist/network-interceptor.d.ts +71 -0
  28. package/dist/npm/cache.d.ts +76 -0
  29. package/dist/npm/index.d.ts +60 -0
  30. package/dist/npm/lockfile-reader.d.ts +32 -0
  31. package/dist/npm/pnpm.d.ts +18 -0
  32. package/dist/npm/registry.d.ts +45 -0
  33. package/dist/npm/resolver.d.ts +39 -0
  34. package/dist/npm/sync-installer.d.ts +18 -0
  35. package/dist/npm/tarball.d.ts +4 -0
  36. package/dist/npm/workspaces.d.ts +46 -0
  37. package/dist/persistence.d.ts +94 -0
  38. package/dist/plugin.d.ts +156 -0
  39. package/dist/polyfills/assert.d.ts +30 -0
  40. package/dist/polyfills/child_process.d.ts +116 -0
  41. package/dist/polyfills/chokidar.d.ts +18 -0
  42. package/dist/polyfills/crypto.d.ts +49 -0
  43. package/dist/polyfills/events.d.ts +28 -0
  44. package/dist/polyfills/fs.d.ts +82 -0
  45. package/dist/polyfills/http.d.ts +147 -0
  46. package/dist/polyfills/module.d.ts +29 -0
  47. package/dist/polyfills/net.d.ts +53 -0
  48. package/dist/polyfills/os.d.ts +91 -0
  49. package/dist/polyfills/path.d.ts +96 -0
  50. package/dist/polyfills/perf_hooks.d.ts +21 -0
  51. package/dist/polyfills/process.d.ts +99 -0
  52. package/dist/polyfills/querystring.d.ts +15 -0
  53. package/dist/polyfills/readdirp.d.ts +18 -0
  54. package/dist/polyfills/readline.d.ts +32 -0
  55. package/dist/polyfills/stream.d.ts +106 -0
  56. package/dist/polyfills/stubs.d.ts +737 -0
  57. package/dist/polyfills/tty.d.ts +25 -0
  58. package/dist/polyfills/url.d.ts +41 -0
  59. package/dist/polyfills/util.d.ts +61 -0
  60. package/dist/polyfills/v8.d.ts +43 -0
  61. package/dist/polyfills/vm.d.ts +76 -0
  62. package/dist/polyfills/worker-threads.d.ts +77 -0
  63. package/dist/polyfills/ws.d.ts +32 -0
  64. package/dist/polyfills/zlib.d.ts +87 -0
  65. package/dist/runtime-helpers.d.ts +4 -0
  66. package/dist/runtime-interface.d.ts +39 -0
  67. package/dist/sandbox.d.ts +69 -0
  68. package/dist/server-bridge.d.ts +55 -0
  69. package/dist/shell-commands.d.ts +2 -0
  70. package/dist/shell.d.ts +101 -0
  71. package/dist/transpiler.d.ts +47 -0
  72. package/dist/type-checker.d.ts +57 -0
  73. package/dist/types/package-json.d.ts +17 -0
  74. package/dist/utils/binary-encoding.d.ts +4 -0
  75. package/dist/utils/hash.d.ts +6 -0
  76. package/dist/utils/safe-path.d.ts +6 -0
  77. package/dist/worker-runtime.d.ts +34 -0
  78. package/package.json +59 -0
  79. package/src/browser-bundle.ts +498 -0
  80. package/src/builtins.ts +222 -0
  81. package/src/code-transform.ts +183 -0
  82. package/src/config/cdn.ts +17 -0
  83. package/src/container.ts +343 -0
  84. package/src/dev-server.ts +322 -0
  85. package/src/errors.ts +604 -0
  86. package/src/frameworks/code-transforms.ts +667 -0
  87. package/src/frameworks/next-api-handler.ts +366 -0
  88. package/src/frameworks/next-dev-server.ts +1252 -0
  89. package/src/frameworks/next-html-generator.ts +585 -0
  90. package/src/frameworks/next-route-resolver.ts +521 -0
  91. package/src/frameworks/next-shims.ts +1084 -0
  92. package/src/frameworks/remix-dev-server.ts +163 -0
  93. package/src/frameworks/sveltekit-dev-server.ts +197 -0
  94. package/src/frameworks/vite-dev-server.ts +370 -0
  95. package/src/fs-errors.ts +118 -0
  96. package/src/index.ts +188 -0
  97. package/src/kernel.ts +381 -0
  98. package/src/memfs.ts +1006 -0
  99. package/src/metrics.ts +140 -0
  100. package/src/module-resolver.ts +511 -0
  101. package/src/network-interceptor.ts +143 -0
  102. package/src/npm/cache.ts +172 -0
  103. package/src/npm/index.ts +377 -0
  104. package/src/npm/lockfile-reader.ts +105 -0
  105. package/src/npm/pnpm.ts +108 -0
  106. package/src/npm/registry.ts +120 -0
  107. package/src/npm/resolver.ts +339 -0
  108. package/src/npm/sync-installer.ts +217 -0
  109. package/src/npm/tarball.ts +136 -0
  110. package/src/npm/workspaces.ts +255 -0
  111. package/src/persistence.ts +235 -0
  112. package/src/plugin.ts +293 -0
  113. package/src/polyfills/assert.ts +164 -0
  114. package/src/polyfills/child_process.ts +535 -0
  115. package/src/polyfills/chokidar.ts +52 -0
  116. package/src/polyfills/crypto.ts +433 -0
  117. package/src/polyfills/events.ts +178 -0
  118. package/src/polyfills/fs.ts +297 -0
  119. package/src/polyfills/http.ts +478 -0
  120. package/src/polyfills/module.ts +97 -0
  121. package/src/polyfills/net.ts +123 -0
  122. package/src/polyfills/os.ts +108 -0
  123. package/src/polyfills/path.ts +169 -0
  124. package/src/polyfills/perf_hooks.ts +30 -0
  125. package/src/polyfills/process.ts +349 -0
  126. package/src/polyfills/querystring.ts +66 -0
  127. package/src/polyfills/readdirp.ts +72 -0
  128. package/src/polyfills/readline.ts +80 -0
  129. package/src/polyfills/stream.ts +610 -0
  130. package/src/polyfills/stubs.ts +600 -0
  131. package/src/polyfills/tty.ts +43 -0
  132. package/src/polyfills/url.ts +97 -0
  133. package/src/polyfills/util.ts +173 -0
  134. package/src/polyfills/v8.ts +62 -0
  135. package/src/polyfills/vm.ts +111 -0
  136. package/src/polyfills/worker-threads.ts +189 -0
  137. package/src/polyfills/ws.ts +73 -0
  138. package/src/polyfills/zlib.ts +244 -0
  139. package/src/runtime-helpers.ts +83 -0
  140. package/src/runtime-interface.ts +46 -0
  141. package/src/sandbox.ts +178 -0
  142. package/src/server-bridge.ts +473 -0
  143. package/src/service-worker.ts +153 -0
  144. package/src/shell-commands.ts +708 -0
  145. package/src/shell.ts +795 -0
  146. package/src/transpiler.ts +282 -0
  147. package/src/type-checker.ts +241 -0
  148. package/src/types/package-json.ts +17 -0
  149. package/src/utils/binary-encoding.ts +38 -0
  150. package/src/utils/hash.ts +24 -0
  151. package/src/utils/safe-path.ts +38 -0
  152. package/src/worker-runtime.ts +42 -0
@@ -0,0 +1,217 @@
1
+ import pako from "pako";
2
+ import { MemFS } from "../memfs";
3
+ import * as pathShim from "../polyfills/path";
4
+ import type { AutoInstallProvider } from "../module-resolver";
5
+ import type { LayoutStrategy } from "./index";
6
+
7
+ const BLOCK = 512;
8
+ const decoder = new TextDecoder();
9
+
10
+ function decodeField(buf: Uint8Array, start: number, len: number): string {
11
+ let i = start;
12
+ const limit = start + len;
13
+ while (i < limit && buf[i] !== 0) i++;
14
+ return decoder.decode(buf.subarray(start, i));
15
+ }
16
+
17
+ function readSize(buf: Uint8Array, offset: number, len: number): number {
18
+ return parseInt(decodeField(buf, offset, len), 8) || 0;
19
+ }
20
+
21
+ function isZeroBlock(buf: Uint8Array): boolean {
22
+ for (let i = 0; i < buf.length; i++) {
23
+ if (buf[i] !== 0) return false;
24
+ }
25
+ return true;
26
+ }
27
+
28
+ function unpackTarSync(tar: Uint8Array, vfs: MemFS, dest: string): void {
29
+ vfs.mkdirSync(dest, { recursive: true });
30
+ let pos = 0;
31
+
32
+ while (pos + BLOCK <= tar.length) {
33
+ const hdr = tar.subarray(pos, pos + BLOCK);
34
+ pos += BLOCK;
35
+ if (isZeroBlock(hdr)) break;
36
+
37
+ const rawName = decodeField(hdr, 0, 100);
38
+ if (!rawName) break;
39
+
40
+ const size = readSize(hdr, 124, 12);
41
+ const type = hdr[156];
42
+ const linkTarget = decodeField(hdr, 157, 100);
43
+ const prefix = decodeField(hdr, 345, 155);
44
+ const fullName = prefix ? `${prefix}/${rawName}` : rawName;
45
+
46
+ const relative = fullName.split("/").slice(1).join("/");
47
+ if (!relative || relative === ".") {
48
+ pos += Math.ceil(size / BLOCK) * BLOCK;
49
+ continue;
50
+ }
51
+
52
+ const target = pathShim.join(dest, relative);
53
+ const parentDir = pathShim.dirname(target);
54
+
55
+ if (type === 53 || fullName.endsWith("/")) {
56
+ vfs.mkdirSync(target, { recursive: true });
57
+ } else if (type === 0 || type === 48) {
58
+ vfs.mkdirSync(parentDir, { recursive: true });
59
+ const data =
60
+ size > 0 && pos + size <= tar.length
61
+ ? new Uint8Array(tar.subarray(pos, pos + size))
62
+ : new Uint8Array(0);
63
+ vfs.writeFileSync(target, data);
64
+ } else if (type === 50 && linkTarget) {
65
+ try {
66
+ vfs.mkdirSync(parentDir, { recursive: true });
67
+ vfs.symlinkSync(linkTarget, target);
68
+ } catch {
69
+ /* skip symlink errors */
70
+ }
71
+ }
72
+
73
+ pos += Math.ceil(size / BLOCK) * BLOCK;
74
+ }
75
+ }
76
+
77
+ function syncFetch(url: string): Uint8Array {
78
+ if (typeof XMLHttpRequest === "undefined") {
79
+ throw new Error(
80
+ "Synchronous auto-install requires XMLHttpRequest (browser environment)",
81
+ );
82
+ }
83
+ const xhr = new XMLHttpRequest();
84
+ xhr.open("GET", url, false);
85
+ xhr.responseType = "arraybuffer";
86
+ xhr.send();
87
+ if (xhr.status < 200 || xhr.status >= 300) {
88
+ throw new Error(`Failed to fetch ${url}: ${xhr.status}`);
89
+ }
90
+ return new Uint8Array(xhr.response as ArrayBuffer);
91
+ }
92
+
93
+ interface ManifestVersion {
94
+ name: string;
95
+ version: string;
96
+ dependencies?: Record<string, string>;
97
+ dist: { tarball: string; shasum: string };
98
+ bin?: string | Record<string, string>;
99
+ }
100
+
101
+ interface Manifest {
102
+ name: string;
103
+ "dist-tags": Record<string, string>;
104
+ versions: Record<string, ManifestVersion>;
105
+ }
106
+
107
+ function normalizeBin(
108
+ pkgName: string,
109
+ bin?: Record<string, string> | string,
110
+ ): Record<string, string> {
111
+ if (!bin) return {};
112
+ if (typeof bin === "string") {
113
+ const cmdName = pkgName.includes("/") ? pkgName.split("/").pop()! : pkgName;
114
+ return { [cmdName]: bin };
115
+ }
116
+ return bin;
117
+ }
118
+
119
+ export class SyncAutoInstaller implements AutoInstallProvider {
120
+ private vfs: MemFS;
121
+ private registryUrl: string;
122
+ private cwd: string;
123
+ private layout: LayoutStrategy;
124
+ private manifestCache = new Map<string, Manifest>();
125
+ private installed = new Set<string>();
126
+
127
+ constructor(
128
+ vfs: MemFS,
129
+ layout: LayoutStrategy,
130
+ options: { cwd?: string; registry?: string } = {},
131
+ ) {
132
+ this.vfs = vfs;
133
+ this.cwd = options.cwd || "/";
134
+ this.registryUrl = (
135
+ options.registry || "https://registry.npmjs.org"
136
+ ).replace(/\/$/, "");
137
+ this.layout = layout;
138
+ }
139
+
140
+ installSync(name: string): void {
141
+ if (this.installed.has(name)) return;
142
+ const destDir = this.layout.getPackageDir(this.cwd, name, "latest");
143
+ if (this.vfs.existsSync(destDir)) {
144
+ this.installed.add(name);
145
+ return;
146
+ }
147
+
148
+ const manifest = this.fetchManifestSync(name);
149
+ const latestTag = manifest["dist-tags"]?.latest;
150
+ if (!latestTag) throw new Error(`No latest version for ${name}`);
151
+
152
+ const version = manifest.versions[latestTag];
153
+ if (!version) throw new Error(`Version ${latestTag} not found for ${name}`);
154
+
155
+ this.installPackageSync(version);
156
+ this.installed.add(name);
157
+ }
158
+
159
+ private installPackageSync(pkg: ManifestVersion): void {
160
+ const destDir = this.layout.getPackageDir(this.cwd, pkg.name, pkg.version);
161
+ if (this.vfs.existsSync(destDir)) return;
162
+
163
+ const compressed = syncFetch(pkg.dist.tarball);
164
+ let tar: Uint8Array;
165
+ try {
166
+ tar = pako.ungzip(compressed);
167
+ } catch {
168
+ tar = compressed;
169
+ }
170
+
171
+ unpackTarSync(tar, this.vfs, destDir);
172
+ this.layout.createTopLevelLink(this.vfs, this.cwd, pkg.name, pkg.version);
173
+
174
+ const binEntries = normalizeBin(pkg.name, pkg.bin);
175
+ for (const [cmdName, binPath] of Object.entries(binEntries)) {
176
+ const targetPath = pathShim.join(destDir, binPath);
177
+ this.layout.createBinStub(this.vfs, this.cwd, cmdName, targetPath);
178
+ }
179
+
180
+ if (pkg.dependencies) {
181
+ for (const depName of Object.keys(pkg.dependencies)) {
182
+ try {
183
+ this.installSync(depName);
184
+ } catch {
185
+ /* skip optional failures */
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ private fetchManifestSync(name: string): Manifest {
192
+ if (this.manifestCache.has(name)) return this.manifestCache.get(name)!;
193
+
194
+ const encodedName = name.startsWith("@")
195
+ ? `@${encodeURIComponent(name.slice(1))}`
196
+ : encodeURIComponent(name);
197
+ const url = `${this.registryUrl}/${encodedName}`;
198
+
199
+ if (typeof XMLHttpRequest === "undefined") {
200
+ throw new Error(
201
+ "Synchronous auto-install requires XMLHttpRequest (browser environment)",
202
+ );
203
+ }
204
+ const xhr = new XMLHttpRequest();
205
+ xhr.open("GET", url, false);
206
+ xhr.setRequestHeader("Accept", "application/json");
207
+ xhr.send();
208
+
209
+ if (xhr.status < 200 || xhr.status >= 300) {
210
+ throw new Error(`Failed to fetch manifest for ${name}: ${xhr.status}`);
211
+ }
212
+
213
+ const manifest = JSON.parse(xhr.responseText) as Manifest;
214
+ this.manifestCache.set(name, manifest);
215
+ return manifest;
216
+ }
217
+ }
@@ -0,0 +1,136 @@
1
+ import pako from "pako";
2
+ import { MemFS } from "../memfs";
3
+ import * as pathShim from "../polyfills/path";
4
+ import type { PackageCache } from "./cache";
5
+
6
+ const BLOCK = 512;
7
+ const decoder = new TextDecoder();
8
+
9
+ function decodeField(buf: Uint8Array, start: number, len: number): string {
10
+ let i = start;
11
+ const limit = start + len;
12
+ while (i < limit && buf[i] !== 0) i++;
13
+ return decoder.decode(buf.subarray(start, i));
14
+ }
15
+
16
+ function readSize(buf: Uint8Array, offset: number, len: number): number {
17
+ return parseInt(decodeField(buf, offset, len), 8) || 0;
18
+ }
19
+
20
+ function isZeroBlock(buf: Uint8Array): boolean {
21
+ for (let i = 0; i < buf.length; i++) {
22
+ if (buf[i] !== 0) return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ function decompress(raw: Uint8Array): Uint8Array {
28
+ try {
29
+ return pako.ungzip(raw);
30
+ } catch {
31
+ return raw;
32
+ }
33
+ }
34
+
35
+ async function fetchArchive(url: string): Promise<Uint8Array> {
36
+ const res = await fetch(url);
37
+ if (!res.ok) throw new Error(`Failed to download ${url}: ${res.status}`);
38
+ return new Uint8Array(await res.arrayBuffer());
39
+ }
40
+
41
+ function* iterateEntries(tar: Uint8Array): Generator<{
42
+ name: string;
43
+ size: number;
44
+ type: number;
45
+ linkTarget: string;
46
+ data: Uint8Array | null;
47
+ }> {
48
+ let pos = 0;
49
+ while (pos + BLOCK <= tar.length) {
50
+ const hdr = tar.subarray(pos, pos + BLOCK);
51
+ pos += BLOCK;
52
+
53
+ if (isZeroBlock(hdr)) break;
54
+
55
+ const rawName = decodeField(hdr, 0, 100);
56
+ if (!rawName) break;
57
+
58
+ const size = readSize(hdr, 124, 12);
59
+ const type = hdr[156];
60
+ const linkTarget = decodeField(hdr, 157, 100);
61
+ const prefix = decodeField(hdr, 345, 155);
62
+ const name = prefix ? `${prefix}/${rawName}` : rawName;
63
+
64
+ let data: Uint8Array | null = null;
65
+ if (size > 0 && pos + size <= tar.length) {
66
+ data = tar.subarray(pos, pos + size);
67
+ }
68
+
69
+ yield { name, size, type, linkTarget, data };
70
+ pos += Math.ceil(size / BLOCK) * BLOCK;
71
+ }
72
+ }
73
+
74
+ function stripPrefix(name: string, depth: number): string {
75
+ if (depth <= 0) return name;
76
+ return name.split("/").slice(depth).join("/");
77
+ }
78
+
79
+ function unpackEntries(
80
+ tar: Uint8Array,
81
+ vfs: MemFS,
82
+ dest: string,
83
+ stripDepth: number,
84
+ ): void {
85
+ vfs.mkdirSync(dest, { recursive: true });
86
+
87
+ for (const entry of iterateEntries(tar)) {
88
+ const relative = stripPrefix(entry.name, stripDepth);
89
+ if (!relative || relative === ".") continue;
90
+
91
+ const target = pathShim.join(dest, relative);
92
+ const parentDir = pathShim.dirname(target);
93
+
94
+ if (entry.type === 53 || entry.name.endsWith("/")) {
95
+ vfs.mkdirSync(target, { recursive: true });
96
+ } else if (entry.type === 0 || entry.type === 48) {
97
+ vfs.mkdirSync(parentDir, { recursive: true });
98
+ vfs.writeFileSync(
99
+ target,
100
+ entry.data ? new Uint8Array(entry.data) : new Uint8Array(0),
101
+ );
102
+ } else if (entry.type === 50 && entry.linkTarget) {
103
+ try {
104
+ vfs.mkdirSync(parentDir, { recursive: true });
105
+ vfs.symlinkSync(entry.linkTarget, target);
106
+ } catch {}
107
+ }
108
+ }
109
+ }
110
+
111
+ export async function downloadAndExtract(
112
+ url: string,
113
+ vfs: MemFS,
114
+ destDir: string,
115
+ stripComponents = 1,
116
+ cache?: PackageCache,
117
+ ): Promise<void> {
118
+ let compressed: Uint8Array;
119
+ if (cache) {
120
+ compressed = await cache.getTarball(url, () => fetchArchive(url));
121
+ } else {
122
+ compressed = await fetchArchive(url);
123
+ }
124
+ const tar = decompress(compressed);
125
+ unpackEntries(tar, vfs, destDir, stripComponents);
126
+ }
127
+
128
+ export async function extractTarball(
129
+ data: Uint8Array,
130
+ vfs: MemFS,
131
+ destDir: string,
132
+ stripComponents = 1,
133
+ ): Promise<void> {
134
+ const tar = decompress(data);
135
+ unpackEntries(tar, vfs, destDir, stripComponents);
136
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Monorepo workspace support.
3
+ *
4
+ * Parses `workspaces` field from package.json or `pnpm-workspace.yaml`
5
+ * and resolves `workspace:*` version specifiers to local packages.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const workspaces = discoverWorkspaces(vfs, '/');
10
+ * // [{ name: '@myapp/utils', path: '/packages/utils', version: '1.0.0' }]
11
+ * ```
12
+ */
13
+
14
+ import type { MemFS } from "../memfs";
15
+ import * as pathShim from "../polyfills/path";
16
+
17
+ export interface WorkspacePackage {
18
+ /** Package name from its package.json. */
19
+ name: string;
20
+ /** Absolute path to the workspace package directory. */
21
+ path: string;
22
+ /** Version from its package.json. */
23
+ version: string;
24
+ }
25
+
26
+ /**
27
+ * Discover all workspace packages in a monorepo.
28
+ *
29
+ * Checks for:
30
+ * 1. `workspaces` field in root package.json (npm/yarn format)
31
+ * 2. `pnpm-workspace.yaml` (pnpm format)
32
+ *
33
+ * Returns an array of discovered workspace packages with their names,
34
+ * paths, and versions.
35
+ */
36
+ export function discoverWorkspaces(
37
+ vfs: MemFS,
38
+ cwd: string,
39
+ ): WorkspacePackage[] {
40
+ const patterns = getWorkspacePatterns(vfs, cwd);
41
+ if (patterns.length === 0) return [];
42
+
43
+ const packages: WorkspacePackage[] = [];
44
+
45
+ for (const pattern of patterns) {
46
+ // Expand glob-like patterns (e.g. "packages/*")
47
+ const dirs = expandWorkspacePattern(vfs, cwd, pattern);
48
+ for (const dir of dirs) {
49
+ const pkgJsonPath = pathShim.join(dir, "package.json");
50
+ try {
51
+ if (!vfs.existsSync(pkgJsonPath)) continue;
52
+ const pkg = JSON.parse(vfs.readFileSync(pkgJsonPath, "utf8"));
53
+ if (pkg.name) {
54
+ packages.push({
55
+ name: pkg.name,
56
+ path: dir,
57
+ version: pkg.version || "0.0.0",
58
+ });
59
+ }
60
+ } catch {
61
+ // Skip invalid package.json
62
+ }
63
+ }
64
+ }
65
+
66
+ return packages;
67
+ }
68
+
69
+ /**
70
+ * Read workspace patterns from package.json or pnpm-workspace.yaml.
71
+ */
72
+ function getWorkspacePatterns(vfs: MemFS, cwd: string): string[] {
73
+ // Check package.json workspaces field
74
+ const pkgJsonPath = pathShim.join(cwd, "package.json");
75
+ try {
76
+ if (vfs.existsSync(pkgJsonPath)) {
77
+ const pkg = JSON.parse(vfs.readFileSync(pkgJsonPath, "utf8"));
78
+ if (Array.isArray(pkg.workspaces)) {
79
+ return pkg.workspaces;
80
+ }
81
+ if (pkg.workspaces?.packages && Array.isArray(pkg.workspaces.packages)) {
82
+ return pkg.workspaces.packages;
83
+ }
84
+ }
85
+ } catch {
86
+ /* skip */
87
+ }
88
+
89
+ // Check pnpm-workspace.yaml
90
+ const pnpmWsPath = pathShim.join(cwd, "pnpm-workspace.yaml");
91
+ try {
92
+ if (vfs.existsSync(pnpmWsPath)) {
93
+ const content = vfs.readFileSync(pnpmWsPath, "utf8");
94
+ return parsePnpmWorkspaceYaml(content);
95
+ }
96
+ } catch {
97
+ /* skip */
98
+ }
99
+
100
+ return [];
101
+ }
102
+
103
+ /**
104
+ * Parse a simple pnpm-workspace.yaml to extract package patterns.
105
+ * Handles the common format:
106
+ * ```yaml
107
+ * packages:
108
+ * - 'packages/*'
109
+ * - 'apps/*'
110
+ * ```
111
+ */
112
+ function parsePnpmWorkspaceYaml(content: string): string[] {
113
+ const patterns: string[] = [];
114
+ let inPackages = false;
115
+
116
+ for (const line of content.split("\n")) {
117
+ const trimmed = line.trim();
118
+ if (trimmed === "packages:" || trimmed === "packages :") {
119
+ inPackages = true;
120
+ continue;
121
+ }
122
+ if (inPackages) {
123
+ if (trimmed.startsWith("- ")) {
124
+ // Strip quotes and the leading "- "
125
+ const pattern = trimmed
126
+ .slice(2)
127
+ .replace(/^['"]|['"]$/g, "")
128
+ .trim();
129
+ if (pattern) patterns.push(pattern);
130
+ } else if (trimmed && !trimmed.startsWith("#")) {
131
+ // Non-list item means we've left the packages section
132
+ break;
133
+ }
134
+ }
135
+ }
136
+
137
+ return patterns;
138
+ }
139
+
140
+ /**
141
+ * Expand a workspace pattern like "packages/*" into actual directory paths.
142
+ */
143
+ function expandWorkspacePattern(
144
+ vfs: MemFS,
145
+ cwd: string,
146
+ pattern: string,
147
+ ): string[] {
148
+ // Remove trailing /
149
+ pattern = pattern.replace(/\/+$/, "");
150
+
151
+ if (pattern.endsWith("/*")) {
152
+ // Glob: list children of the directory
153
+ const parentDir = pathShim.join(cwd, pattern.slice(0, -2));
154
+ try {
155
+ if (!vfs.existsSync(parentDir)) return [];
156
+ return vfs
157
+ .readdirSync(parentDir)
158
+ .filter(name => {
159
+ const full = pathShim.join(parentDir, name);
160
+ try {
161
+ return vfs.statSync(full).isDirectory();
162
+ } catch {
163
+ return false;
164
+ }
165
+ })
166
+ .map(name => pathShim.join(parentDir, name));
167
+ } catch {
168
+ return [];
169
+ }
170
+ }
171
+
172
+ if (pattern.endsWith("/**")) {
173
+ // Recursive glob: find all directories containing package.json
174
+ const parentDir = pathShim.join(cwd, pattern.slice(0, -3));
175
+ const results: string[] = [];
176
+ findPackageDirs(vfs, parentDir, results);
177
+ return results;
178
+ }
179
+
180
+ // Exact path
181
+ const dir = pathShim.join(cwd, pattern);
182
+ return vfs.existsSync(dir) ? [dir] : [];
183
+ }
184
+
185
+ function findPackageDirs(vfs: MemFS, dir: string, results: string[]): void {
186
+ try {
187
+ const pkgJson = pathShim.join(dir, "package.json");
188
+ if (vfs.existsSync(pkgJson)) results.push(dir);
189
+
190
+ for (const name of vfs.readdirSync(dir)) {
191
+ if (name === "node_modules" || name.startsWith(".")) continue;
192
+ const full = pathShim.join(dir, name);
193
+ try {
194
+ if (vfs.statSync(full).isDirectory()) {
195
+ findPackageDirs(vfs, full, results);
196
+ }
197
+ } catch {
198
+ /* skip */
199
+ }
200
+ }
201
+ } catch {
202
+ /* skip */
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Resolve a `workspace:*` version specifier to the local package path.
208
+ * Returns the workspace package if found, or `null` if not a workspace dep.
209
+ */
210
+ export function resolveWorkspaceDep(
211
+ specifier: string,
212
+ workspaces: WorkspacePackage[],
213
+ ): WorkspacePackage | null {
214
+ for (const ws of workspaces) {
215
+ if (ws.name === specifier) return ws;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ /**
221
+ * Check if a version string is a workspace protocol specifier.
222
+ */
223
+ export function isWorkspaceProtocol(version: string): boolean {
224
+ return version.startsWith("workspace:");
225
+ }
226
+
227
+ /**
228
+ * Create symlinks in node_modules for workspace packages, similar to
229
+ * how pnpm/yarn link local packages.
230
+ */
231
+ export function linkWorkspaces(
232
+ vfs: MemFS,
233
+ cwd: string,
234
+ workspaces: WorkspacePackage[],
235
+ ): void {
236
+ const nmDir = pathShim.join(cwd, "node_modules");
237
+ vfs.mkdirSync(nmDir, { recursive: true });
238
+
239
+ for (const ws of workspaces) {
240
+ const linkPath = pathShim.join(nmDir, ws.name);
241
+ // Create parent for scoped packages
242
+ if (ws.name.includes("/")) {
243
+ const scope = pathShim.dirname(linkPath);
244
+ vfs.mkdirSync(scope, { recursive: true });
245
+ }
246
+ // Only create if not already present
247
+ if (!vfs.existsSync(linkPath)) {
248
+ try {
249
+ vfs.symlinkSync(ws.path, linkPath);
250
+ } catch {
251
+ // Symlink may already exist
252
+ }
253
+ }
254
+ }
255
+ }