@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,143 @@
1
+ /**
2
+ * Network request interception and mocking for jiki containers.
3
+ *
4
+ * Allows intercepting and mocking outgoing `fetch()` calls from executed
5
+ * code. Useful for testing, offline scenarios, and CORS avoidance.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const container = boot();
10
+ * container.mockFetch('/api/users', { json: [{ name: 'Alice' }] });
11
+ * container.execute('fetch("/api/users").then(r => r.json()).then(console.log)');
12
+ * ```
13
+ */
14
+
15
+ export interface MockResponse {
16
+ /** HTTP status code. Default: 200. */
17
+ status?: number;
18
+ /** Response headers. */
19
+ headers?: Record<string, string>;
20
+ /** Response body as string. */
21
+ body?: string;
22
+ /** Response body as JSON (auto-serialized). Overrides `body`. */
23
+ json?: unknown;
24
+ }
25
+
26
+ export type FetchHandler = (
27
+ url: string,
28
+ init?: RequestInit,
29
+ ) => MockResponse | null | undefined | Promise<MockResponse | null | undefined>;
30
+
31
+ interface MockRule {
32
+ pattern: string | RegExp;
33
+ response?: MockResponse;
34
+ handler?: FetchHandler;
35
+ }
36
+
37
+ /**
38
+ * Intercepts fetch calls and returns mock responses when a URL matches
39
+ * a registered pattern.
40
+ */
41
+ export class NetworkInterceptor {
42
+ private rules: MockRule[] = [];
43
+ private handlers: FetchHandler[] = [];
44
+
45
+ /**
46
+ * Register a static mock response for a URL pattern.
47
+ *
48
+ * @param pattern - String (exact match or prefix) or RegExp
49
+ * @param response - Mock response to return
50
+ */
51
+ mock(pattern: string | RegExp, response: MockResponse): void {
52
+ this.rules.push({ pattern, response });
53
+ }
54
+
55
+ /**
56
+ * Register a dynamic handler that can inspect the request and return
57
+ * a response. Return `null` to pass through to the next handler or
58
+ * the real network.
59
+ */
60
+ onFetch(handler: FetchHandler): void {
61
+ this.handlers.push(handler);
62
+ }
63
+
64
+ /**
65
+ * Remove all mock rules and handlers.
66
+ */
67
+ clear(): void {
68
+ this.rules.length = 0;
69
+ this.handlers.length = 0;
70
+ }
71
+
72
+ /** Number of registered rules. */
73
+ get ruleCount(): number {
74
+ return this.rules.length;
75
+ }
76
+
77
+ /** Number of registered handlers. */
78
+ get handlerCount(): number {
79
+ return this.handlers.length;
80
+ }
81
+
82
+ /**
83
+ * Try to match a URL against registered rules and handlers.
84
+ * Returns a Response-like object if matched, or `null` to pass through.
85
+ */
86
+ async intercept(
87
+ url: string,
88
+ init?: RequestInit,
89
+ ): Promise<MockResponse | null> {
90
+ // Check static rules first
91
+ for (const rule of this.rules) {
92
+ if (this.matches(url, rule.pattern)) {
93
+ return rule.response || { status: 200 };
94
+ }
95
+ }
96
+
97
+ // Check dynamic handlers
98
+ for (const handler of this.handlers) {
99
+ const result = await handler(url, init);
100
+ if (result) return result;
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ private matches(url: string, pattern: string | RegExp): boolean {
107
+ if (pattern instanceof RegExp) return pattern.test(url);
108
+ // String: exact match or prefix match
109
+ return url === pattern || url.startsWith(pattern);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Convert a MockResponse to a format suitable for the `fetch` polyfill.
115
+ */
116
+ export function mockResponseToFetchResponse(mock: MockResponse): {
117
+ ok: boolean;
118
+ status: number;
119
+ statusText: string;
120
+ headers: Record<string, string>;
121
+ text: () => Promise<string>;
122
+ json: () => Promise<unknown>;
123
+ arrayBuffer: () => Promise<ArrayBuffer>;
124
+ } {
125
+ const status = mock.status ?? 200;
126
+ const body =
127
+ mock.json !== undefined ? JSON.stringify(mock.json) : (mock.body ?? "");
128
+ const headers = {
129
+ "Content-Type": mock.json !== undefined ? "application/json" : "text/plain",
130
+ ...mock.headers,
131
+ };
132
+
133
+ return {
134
+ ok: status >= 200 && status < 300,
135
+ status,
136
+ statusText: status === 200 ? "OK" : String(status),
137
+ headers,
138
+ text: async () => body,
139
+ json: async () => (mock.json !== undefined ? mock.json : JSON.parse(body)),
140
+ arrayBuffer: async () =>
141
+ new TextEncoder().encode(body).buffer as ArrayBuffer,
142
+ };
143
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Package cache for npm manifests and tarballs.
3
+ *
4
+ * Provides an in-memory cache with an optional persistent backing store
5
+ * (IndexedDB or any `PersistenceAdapter`-like backend). Manifests have a
6
+ * configurable TTL; tarballs are immutable by URL and cached indefinitely.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const cache = new PackageCache();
11
+ * const registry = new Registry();
12
+ * // Wrap getManifest with cache
13
+ * const manifest = await cache.getManifest('react', () => registry.getManifest('react'));
14
+ * ```
15
+ */
16
+
17
+ import type { PackageManifest } from "./registry";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface PackageCacheOptions {
24
+ /** Manifest TTL in milliseconds. Default: 1 hour. */
25
+ manifestTtlMs?: number;
26
+ /** Maximum number of manifests to keep in memory. Default: 500. */
27
+ maxManifests?: number;
28
+ /** Maximum number of tarballs to keep in memory. Default: 200. */
29
+ maxTarballs?: number;
30
+ }
31
+
32
+ interface CachedManifest {
33
+ manifest: PackageManifest;
34
+ timestamp: number;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // PackageCache
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * In-memory cache for npm package manifests and tarball data.
43
+ *
44
+ * - **Manifests** are keyed by package name with a configurable TTL (default
45
+ * 1 hour). After TTL expiry the manifest is re-fetched on next access.
46
+ * - **Tarballs** are keyed by URL. Since npm tarball URLs are immutable
47
+ * (content-addressed), they are cached indefinitely.
48
+ */
49
+ export class PackageCache {
50
+ private manifests = new Map<string, CachedManifest>();
51
+ private tarballs = new Map<string, Uint8Array>();
52
+ private manifestTtlMs: number;
53
+ private maxManifests: number;
54
+ private maxTarballs: number;
55
+
56
+ constructor(options: PackageCacheOptions = {}) {
57
+ this.manifestTtlMs = options.manifestTtlMs ?? 60 * 60 * 1000; // 1 hour
58
+ this.maxManifests = options.maxManifests ?? 500;
59
+ this.maxTarballs = options.maxTarballs ?? 200;
60
+ }
61
+
62
+ // -- Manifests ------------------------------------------------------------
63
+
64
+ /**
65
+ * Get a cached manifest, or fetch and cache it.
66
+ *
67
+ * @param name - Package name
68
+ * @param fetcher - Async function that fetches the manifest from the registry
69
+ * @returns The manifest (from cache or freshly fetched)
70
+ */
71
+ async getManifest(
72
+ name: string,
73
+ fetcher: () => Promise<PackageManifest>,
74
+ ): Promise<PackageManifest> {
75
+ const cached = this.manifests.get(name);
76
+ if (cached && Date.now() - cached.timestamp < this.manifestTtlMs) {
77
+ return cached.manifest;
78
+ }
79
+
80
+ const manifest = await fetcher();
81
+ this.setManifest(name, manifest);
82
+ return manifest;
83
+ }
84
+
85
+ /** Check if a manifest is cached and not expired. */
86
+ hasManifest(name: string): boolean {
87
+ const cached = this.manifests.get(name);
88
+ return !!cached && Date.now() - cached.timestamp < this.manifestTtlMs;
89
+ }
90
+
91
+ /** Store a manifest in the cache. */
92
+ setManifest(name: string, manifest: PackageManifest): void {
93
+ if (this.manifests.size >= this.maxManifests) {
94
+ // Evict oldest entry
95
+ const oldest = this.manifests.keys().next().value;
96
+ if (oldest !== undefined) this.manifests.delete(oldest);
97
+ }
98
+ this.manifests.set(name, { manifest, timestamp: Date.now() });
99
+ }
100
+
101
+ // -- Tarballs -------------------------------------------------------------
102
+
103
+ /**
104
+ * Get a cached tarball, or fetch and cache it.
105
+ *
106
+ * @param url - Tarball URL (immutable by npm convention)
107
+ * @param fetcher - Async function that downloads the tarball
108
+ * @returns The tarball data
109
+ */
110
+ async getTarball(
111
+ url: string,
112
+ fetcher: () => Promise<Uint8Array>,
113
+ ): Promise<Uint8Array> {
114
+ const cached = this.tarballs.get(url);
115
+ if (cached) return cached;
116
+
117
+ const data = await fetcher();
118
+ this.setTarball(url, data);
119
+ return data;
120
+ }
121
+
122
+ /** Check if a tarball is cached. */
123
+ hasTarball(url: string): boolean {
124
+ return this.tarballs.has(url);
125
+ }
126
+
127
+ /** Store a tarball in the cache. */
128
+ setTarball(url: string, data: Uint8Array): void {
129
+ if (this.tarballs.size >= this.maxTarballs) {
130
+ const oldest = this.tarballs.keys().next().value;
131
+ if (oldest !== undefined) this.tarballs.delete(oldest);
132
+ }
133
+ this.tarballs.set(url, data);
134
+ }
135
+
136
+ // -- Diagnostics ----------------------------------------------------------
137
+
138
+ /** Number of cached manifests. */
139
+ get manifestCount(): number {
140
+ return this.manifests.size;
141
+ }
142
+
143
+ /** Number of cached tarballs. */
144
+ get tarballCount(): number {
145
+ return this.tarballs.size;
146
+ }
147
+
148
+ /** Total bytes of cached tarballs. */
149
+ get tarballBytes(): number {
150
+ let total = 0;
151
+ for (const data of this.tarballs.values()) total += data.length;
152
+ return total;
153
+ }
154
+
155
+ // -- Cache control --------------------------------------------------------
156
+
157
+ /** Clear all cached manifests. */
158
+ clearManifests(): void {
159
+ this.manifests.clear();
160
+ }
161
+
162
+ /** Clear all cached tarballs. */
163
+ clearTarballs(): void {
164
+ this.tarballs.clear();
165
+ }
166
+
167
+ /** Clear all cached data. */
168
+ clear(): void {
169
+ this.manifests.clear();
170
+ this.tarballs.clear();
171
+ }
172
+ }
@@ -0,0 +1,377 @@
1
+ import { MemFS } from "../memfs";
2
+ import { Registry, RegistryOptions } from "./registry";
3
+ import {
4
+ resolveDependencies,
5
+ resolveFromPackageJson,
6
+ satisfies as semverSatisfies,
7
+ compareVersions,
8
+ ResolvedPackage,
9
+ } from "./resolver";
10
+ import { downloadAndExtract } from "./tarball";
11
+ import * as path from "../polyfills/path";
12
+ import { PackageCache, type PackageCacheOptions } from "./cache";
13
+ import { readLockfile, lockfileToResolved } from "./lockfile-reader";
14
+
15
+ export interface LayoutStrategy {
16
+ getPackageDir(cwd: string, pkgName: string, pkgVersion: string): string;
17
+ createTopLevelLink(
18
+ vfs: MemFS,
19
+ cwd: string,
20
+ pkgName: string,
21
+ pkgVersion: string,
22
+ ): void;
23
+ createDependencyLinks(
24
+ vfs: MemFS,
25
+ cwd: string,
26
+ pkg: ResolvedPackage,
27
+ allResolved: Map<string, ResolvedPackage>,
28
+ ): void;
29
+ createBinStub(
30
+ vfs: MemFS,
31
+ cwd: string,
32
+ cmdName: string,
33
+ targetPath: string,
34
+ ): void;
35
+ }
36
+
37
+ export class NpmLayout implements LayoutStrategy {
38
+ getPackageDir(cwd: string, pkgName: string, _pkgVersion: string): string {
39
+ return path.join(cwd, "node_modules", pkgName);
40
+ }
41
+
42
+ createTopLevelLink(): void {}
43
+ createDependencyLinks(): void {}
44
+
45
+ createBinStub(
46
+ vfs: MemFS,
47
+ cwd: string,
48
+ cmdName: string,
49
+ targetPath: string,
50
+ ): void {
51
+ const binDir = path.join(cwd, "node_modules", ".bin");
52
+ vfs.mkdirSync(binDir, { recursive: true });
53
+ const stubPath = path.join(binDir, cmdName);
54
+ vfs.writeFileSync(
55
+ stubPath,
56
+ `#!/usr/bin/env node\nrequire("${targetPath}");\n`,
57
+ );
58
+ }
59
+ }
60
+
61
+ function normalizeBin(
62
+ pkgName: string,
63
+ bin?: Record<string, string> | string,
64
+ ): Record<string, string> {
65
+ if (!bin) return {};
66
+ if (typeof bin === "string") {
67
+ const cmdName = pkgName.includes("/") ? pkgName.split("/").pop()! : pkgName;
68
+ return { [cmdName]: bin };
69
+ }
70
+ return bin;
71
+ }
72
+
73
+ export function parsePackageSpec(spec: string): {
74
+ name: string;
75
+ version: string;
76
+ } {
77
+ if (spec.startsWith("@")) {
78
+ const atIdx = spec.indexOf("@", 1);
79
+ if (atIdx > 0)
80
+ return { name: spec.slice(0, atIdx), version: spec.slice(atIdx + 1) };
81
+ return { name: spec, version: "latest" };
82
+ }
83
+ const atIdx = spec.lastIndexOf("@");
84
+ if (atIdx > 0)
85
+ return { name: spec.slice(0, atIdx), version: spec.slice(atIdx + 1) };
86
+ return { name: spec, version: "latest" };
87
+ }
88
+
89
+ export interface InstallOptions {
90
+ registry?: string;
91
+ save?: boolean;
92
+ saveDev?: boolean;
93
+ includeDev?: boolean;
94
+ includeOptional?: boolean;
95
+ onProgress?: (message: string) => void;
96
+ transform?: boolean;
97
+ concurrency?: number;
98
+ }
99
+
100
+ export interface InstallResult {
101
+ installed: Map<string, ResolvedPackage>;
102
+ added: string[];
103
+ }
104
+
105
+ const DEFAULT_CONCURRENCY =
106
+ typeof navigator !== "undefined" && navigator.hardwareConcurrency
107
+ ? navigator.hardwareConcurrency
108
+ : 6;
109
+
110
+ export class PackageManager {
111
+ private vfs: MemFS;
112
+ private registry: Registry;
113
+ private cwd: string;
114
+ readonly layout: LayoutStrategy;
115
+ /** Package cache for manifests and tarballs. */
116
+ readonly cache: PackageCache;
117
+
118
+ constructor(
119
+ vfs: MemFS,
120
+ options: {
121
+ cwd?: string;
122
+ layout?: LayoutStrategy;
123
+ cache?: PackageCache;
124
+ } & RegistryOptions = {},
125
+ ) {
126
+ this.vfs = vfs;
127
+ this.cache = options.cache || new PackageCache();
128
+ this.registry = new Registry({ ...options, cache: this.cache });
129
+ this.cwd = options.cwd || "/";
130
+ this.layout = options.layout || new NpmLayout();
131
+ }
132
+
133
+ /** Clear the package cache (manifests and tarballs). */
134
+ clearCache(): void {
135
+ this.cache.clear();
136
+ }
137
+
138
+ async install(
139
+ packageSpec: string | string[],
140
+ options: InstallOptions = {},
141
+ ): Promise<InstallResult> {
142
+ const specs = Array.isArray(packageSpec) ? packageSpec : [packageSpec];
143
+ const { onProgress } = options;
144
+ const allResolved = new Map<string, ResolvedPackage>();
145
+ const added: string[] = [];
146
+
147
+ const results = await Promise.all(
148
+ specs.map(async spec => {
149
+ const { name, version } = parsePackageSpec(spec);
150
+ onProgress?.(`Resolving ${name}@${version || "latest"}...`);
151
+ return resolveDependencies(name, version || "latest", {
152
+ registry: this.registry,
153
+ includeDev: options.includeDev,
154
+ includeOptional: options.includeOptional,
155
+ });
156
+ }),
157
+ );
158
+
159
+ for (const result of results) {
160
+ for (const [name, pkg] of result) {
161
+ if (!allResolved.has(name)) {
162
+ allResolved.set(name, pkg);
163
+ } else {
164
+ // Deduplicate: keep the higher version when two specs resolve the same package
165
+ const existing = allResolved.get(name)!;
166
+ if (
167
+ pkg.version !== existing.version &&
168
+ compareVersions(pkg.version, existing.version) > 0
169
+ ) {
170
+ allResolved.set(name, pkg);
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ const packages = Array.from(allResolved.values());
177
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
178
+ onProgress?.(`Downloading ${packages.length} packages...`);
179
+
180
+ for (let i = 0; i < packages.length; i += concurrency) {
181
+ const batch = packages.slice(i, i + concurrency);
182
+ await Promise.all(
183
+ batch.map(async pkg => {
184
+ const destDir = this.layout.getPackageDir(
185
+ this.cwd,
186
+ pkg.name,
187
+ pkg.version,
188
+ );
189
+ if (this.vfs.existsSync(destDir)) return;
190
+ onProgress?.(`Installing ${pkg.name}@${pkg.version}`);
191
+ await downloadAndExtract(
192
+ pkg.dist.tarball,
193
+ this.vfs,
194
+ destDir,
195
+ 1,
196
+ this.cache,
197
+ );
198
+ added.push(`${pkg.name}@${pkg.version}`);
199
+
200
+ this.layout.createTopLevelLink(
201
+ this.vfs,
202
+ this.cwd,
203
+ pkg.name,
204
+ pkg.version,
205
+ );
206
+
207
+ const binEntries = normalizeBin(pkg.name, pkg.bin);
208
+ for (const [cmdName, binPath] of Object.entries(binEntries)) {
209
+ const targetPath = path.join(destDir, binPath);
210
+ this.layout.createBinStub(this.vfs, this.cwd, cmdName, targetPath);
211
+ }
212
+ }),
213
+ );
214
+ }
215
+
216
+ for (const pkg of packages) {
217
+ this.layout.createDependencyLinks(this.vfs, this.cwd, pkg, allResolved);
218
+ }
219
+
220
+ if (options.save || options.saveDev) {
221
+ this.updatePackageJson(specs, allResolved, options.saveDev);
222
+ }
223
+
224
+ buildLockfile(this.vfs, allResolved, this.cwd);
225
+
226
+ onProgress?.(`Installed ${added.length} packages.`);
227
+ return { installed: allResolved, added };
228
+ }
229
+
230
+ async installFromPackageJson(
231
+ options: InstallOptions = {},
232
+ ): Promise<InstallResult> {
233
+ const pkgJsonPath = path.join(this.cwd, "package.json");
234
+ if (!this.vfs.existsSync(pkgJsonPath))
235
+ throw new Error("No package.json found");
236
+
237
+ // Try lockfile-first installation (deterministic, no network for resolution)
238
+ const lockfile = readLockfile(this.vfs, this.cwd);
239
+ let resolved: Map<string, ResolvedPackage>;
240
+ if (lockfile && lockfile.packages.size > 0) {
241
+ options.onProgress?.("Using lockfile for deterministic install...");
242
+ resolved = lockfileToResolved(lockfile);
243
+ } else {
244
+ const pkgJson = JSON.parse(this.vfs.readFileSync(pkgJsonPath, "utf8"));
245
+ resolved = await resolveFromPackageJson(pkgJson, {
246
+ registry: this.registry,
247
+ includeDev: options.includeDev,
248
+ includeOptional: options.includeOptional,
249
+ });
250
+ }
251
+
252
+ const added: string[] = [];
253
+ const packages = Array.from(resolved.values());
254
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
255
+ options.onProgress?.(`Downloading ${packages.length} packages...`);
256
+
257
+ for (let i = 0; i < packages.length; i += concurrency) {
258
+ const batch = packages.slice(i, i + concurrency);
259
+ await Promise.all(
260
+ batch.map(async pkg => {
261
+ const destDir = this.layout.getPackageDir(
262
+ this.cwd,
263
+ pkg.name,
264
+ pkg.version,
265
+ );
266
+ if (this.vfs.existsSync(destDir)) return;
267
+ options.onProgress?.(`Installing ${pkg.name}@${pkg.version}`);
268
+ await downloadAndExtract(
269
+ pkg.dist.tarball,
270
+ this.vfs,
271
+ destDir,
272
+ 1,
273
+ this.cache,
274
+ );
275
+ added.push(`${pkg.name}@${pkg.version}`);
276
+
277
+ this.layout.createTopLevelLink(
278
+ this.vfs,
279
+ this.cwd,
280
+ pkg.name,
281
+ pkg.version,
282
+ );
283
+
284
+ const binEntries = normalizeBin(pkg.name, pkg.bin);
285
+ for (const [cmdName, binPath] of Object.entries(binEntries)) {
286
+ const targetPath = path.join(destDir, binPath);
287
+ this.layout.createBinStub(this.vfs, this.cwd, cmdName, targetPath);
288
+ }
289
+ }),
290
+ );
291
+ }
292
+
293
+ for (const pkg of packages) {
294
+ this.layout.createDependencyLinks(this.vfs, this.cwd, pkg, resolved);
295
+ }
296
+
297
+ buildLockfile(this.vfs, resolved, this.cwd);
298
+
299
+ return { installed: resolved, added };
300
+ }
301
+
302
+ list(): string[] {
303
+ const nmDir = path.join(this.cwd, "node_modules");
304
+ if (!this.vfs.existsSync(nmDir)) return [];
305
+ const entries = this.vfs.readdirSync(nmDir);
306
+ const packages: string[] = [];
307
+ for (const entry of entries) {
308
+ if (entry.startsWith(".")) continue;
309
+ if (entry.startsWith("@")) {
310
+ const scopeDir = path.join(nmDir, entry);
311
+ const scoped = this.vfs.readdirSync(scopeDir);
312
+ for (const s of scoped) packages.push(`${entry}/${s}`);
313
+ } else {
314
+ packages.push(entry);
315
+ }
316
+ }
317
+ return packages;
318
+ }
319
+
320
+ private updatePackageJson(
321
+ specs: string[],
322
+ resolved: Map<string, ResolvedPackage>,
323
+ isDev?: boolean,
324
+ ): void {
325
+ const pkgJsonPath = path.join(this.cwd, "package.json");
326
+ let pkgJson: Record<string, unknown> = {};
327
+ if (this.vfs.existsSync(pkgJsonPath)) {
328
+ pkgJson = JSON.parse(this.vfs.readFileSync(pkgJsonPath, "utf8"));
329
+ }
330
+ const field = isDev ? "devDependencies" : "dependencies";
331
+ if (!pkgJson[field]) pkgJson[field] = {};
332
+ const deps = pkgJson[field] as Record<string, string>;
333
+ for (const spec of specs) {
334
+ const { name } = parsePackageSpec(spec);
335
+ const pkg = resolved.get(name);
336
+ if (pkg) deps[name] = `^${pkg.version}`;
337
+ }
338
+ this.vfs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
339
+ }
340
+ }
341
+
342
+ export function buildLockfile(
343
+ vfs: MemFS,
344
+ resolved: Map<string, ResolvedPackage>,
345
+ cwd: string = "/",
346
+ ): void {
347
+ const lockfile: Record<string, unknown> = {
348
+ name: "jiki-project",
349
+ lockfileVersion: 3,
350
+ packages: {} as Record<
351
+ string,
352
+ {
353
+ version: string;
354
+ resolved: string;
355
+ dependencies?: Record<string, string>;
356
+ }
357
+ >,
358
+ };
359
+ const packages = lockfile.packages as Record<string, any>;
360
+ for (const [name, pkg] of resolved) {
361
+ packages[`node_modules/${name}`] = {
362
+ version: pkg.version,
363
+ resolved: pkg.dist.tarball,
364
+ dependencies:
365
+ Object.keys(pkg.dependencies).length > 0 ? pkg.dependencies : undefined,
366
+ };
367
+ }
368
+ const lockfilePath = path.join(cwd, "package-lock.json");
369
+ vfs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2));
370
+ }
371
+
372
+ export { Registry } from "./registry";
373
+ export type { RegistryOptions } from "./registry";
374
+ export type { ResolvedPackage } from "./resolver";
375
+ export { satisfies, compareVersions } from "./resolver";
376
+ export { readLockfile, lockfileToResolved } from "./lockfile-reader";
377
+ export type { LockfileData, LockfileEntry } from "./lockfile-reader";