@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,105 @@
1
+ /**
2
+ * Lockfile reader for package-lock.json (v3 format).
3
+ *
4
+ * When a lockfile exists, the package manager can skip dependency resolution
5
+ * entirely and use the exact versions and tarball URLs from the lockfile.
6
+ * Combined with the package cache, this enables zero-network deterministic
7
+ * installs.
8
+ */
9
+
10
+ import type { MemFS } from "../memfs";
11
+ import * as pathShim from "../polyfills/path";
12
+ import type { ResolvedPackage } from "./resolver";
13
+
14
+ /** A single package entry parsed from a lockfile. */
15
+ export interface LockfileEntry {
16
+ name: string;
17
+ version: string;
18
+ resolved: string; // tarball URL
19
+ dependencies?: Record<string, string>;
20
+ }
21
+
22
+ /** Parsed lockfile data. */
23
+ export interface LockfileData {
24
+ lockfileVersion: number;
25
+ packages: Map<string, LockfileEntry>;
26
+ }
27
+
28
+ /**
29
+ * Read and parse a package-lock.json (v3 format) from the VFS.
30
+ * Returns `null` if no lockfile exists or it can't be parsed.
31
+ */
32
+ export function readLockfile(vfs: MemFS, cwd: string): LockfileData | null {
33
+ const lockfilePath = pathShim.join(cwd, "package-lock.json");
34
+ try {
35
+ if (!vfs.existsSync(lockfilePath)) return null;
36
+ const content = JSON.parse(vfs.readFileSync(lockfilePath, "utf8"));
37
+ return parseLockfileV3(content);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Parse lockfile JSON (v3 format) into structured data.
45
+ */
46
+ function parseLockfileV3(json: Record<string, unknown>): LockfileData | null {
47
+ const lockfileVersion = json.lockfileVersion as number;
48
+ if (!lockfileVersion || lockfileVersion < 3) return null;
49
+
50
+ const rawPackages = json.packages as
51
+ | Record<
52
+ string,
53
+ {
54
+ version?: string;
55
+ resolved?: string;
56
+ dependencies?: Record<string, string>;
57
+ }
58
+ >
59
+ | undefined;
60
+
61
+ if (!rawPackages) return null;
62
+
63
+ const packages = new Map<string, LockfileEntry>();
64
+
65
+ for (const [key, entry] of Object.entries(rawPackages)) {
66
+ // Keys are like "node_modules/react" or "node_modules/@scope/pkg"
67
+ if (!key.startsWith("node_modules/")) continue;
68
+ const name = key.slice("node_modules/".length);
69
+
70
+ if (!entry.version || !entry.resolved) continue;
71
+
72
+ packages.set(name, {
73
+ name,
74
+ version: entry.version,
75
+ resolved: entry.resolved,
76
+ dependencies: entry.dependencies,
77
+ });
78
+ }
79
+
80
+ return { lockfileVersion, packages };
81
+ }
82
+
83
+ /**
84
+ * Convert lockfile entries to ResolvedPackage map for use by the installer.
85
+ * This replaces the full dependency resolution step when a lockfile is available.
86
+ */
87
+ export function lockfileToResolved(
88
+ lockfile: LockfileData,
89
+ ): Map<string, ResolvedPackage> {
90
+ const resolved = new Map<string, ResolvedPackage>();
91
+
92
+ for (const [name, entry] of lockfile.packages) {
93
+ resolved.set(name, {
94
+ name,
95
+ version: entry.version,
96
+ dependencies: entry.dependencies || {},
97
+ dist: {
98
+ tarball: entry.resolved,
99
+ shasum: "",
100
+ },
101
+ });
102
+ }
103
+
104
+ return resolved;
105
+ }
@@ -0,0 +1,108 @@
1
+ import { MemFS } from "../memfs";
2
+ import { LayoutStrategy } from "./index";
3
+ import { ResolvedPackage } from "./resolver";
4
+ import * as path from "../polyfills/path";
5
+
6
+ /**
7
+ * pnpm-style content-addressable store layout.
8
+ *
9
+ * Structure:
10
+ * node_modules/.pnpm/<name>@<version>/node_modules/<name>/ ← actual files
11
+ * node_modules/<name> → symlink to store path above
12
+ * node_modules/.pnpm/<name>@<version>/node_modules/<dep> → symlink to dep store
13
+ */
14
+ export class PnpmLayout implements LayoutStrategy {
15
+ private storePath(cwd: string, pkgName: string, pkgVersion: string): string {
16
+ const storeKey = `${sanitiseName(pkgName)}@${pkgVersion}`;
17
+ return path.join(
18
+ cwd,
19
+ "node_modules",
20
+ ".pnpm",
21
+ storeKey,
22
+ "node_modules",
23
+ pkgName,
24
+ );
25
+ }
26
+
27
+ getPackageDir(cwd: string, pkgName: string, pkgVersion: string): string {
28
+ return this.storePath(cwd, pkgName, pkgVersion);
29
+ }
30
+
31
+ createTopLevelLink(
32
+ vfs: MemFS,
33
+ cwd: string,
34
+ pkgName: string,
35
+ pkgVersion: string,
36
+ ): void {
37
+ const linkPath = path.join(cwd, "node_modules", pkgName);
38
+
39
+ if (pkgName.startsWith("@")) {
40
+ const scopeDir = path.join(cwd, "node_modules", pkgName.split("/")[0]);
41
+ if (!vfs.existsSync(scopeDir)) {
42
+ vfs.mkdirSync(scopeDir, { recursive: true });
43
+ }
44
+ }
45
+
46
+ if (vfs.existsSync(linkPath)) return;
47
+
48
+ const target = this.storePath(cwd, pkgName, pkgVersion);
49
+ vfs.symlinkSync(target, linkPath);
50
+ }
51
+
52
+ createDependencyLinks(
53
+ vfs: MemFS,
54
+ cwd: string,
55
+ pkg: ResolvedPackage,
56
+ allResolved: Map<string, ResolvedPackage>,
57
+ ): void {
58
+ const declared = pkg.dependencies;
59
+ if (!declared || Object.keys(declared).length === 0) return;
60
+
61
+ const storeKey = `${sanitiseName(pkg.name)}@${pkg.version}`;
62
+ const parentNm = path.join(
63
+ cwd,
64
+ "node_modules",
65
+ ".pnpm",
66
+ storeKey,
67
+ "node_modules",
68
+ );
69
+
70
+ for (const depName of Object.keys(declared)) {
71
+ const resolved = allResolved.get(depName);
72
+ if (!resolved) continue;
73
+
74
+ const depLink = path.join(parentNm, depName);
75
+ if (vfs.existsSync(depLink)) continue;
76
+
77
+ if (depName.startsWith("@")) {
78
+ const scopeDir = path.join(parentNm, depName.split("/")[0]);
79
+ if (!vfs.existsSync(scopeDir)) {
80
+ vfs.mkdirSync(scopeDir, { recursive: true });
81
+ }
82
+ }
83
+
84
+ const depStore = this.storePath(cwd, resolved.name, resolved.version);
85
+ vfs.symlinkSync(depStore, depLink);
86
+ }
87
+ }
88
+
89
+ createBinStub(
90
+ vfs: MemFS,
91
+ cwd: string,
92
+ cmdName: string,
93
+ targetPath: string,
94
+ ): void {
95
+ const binDir = path.join(cwd, "node_modules", ".bin");
96
+ vfs.mkdirSync(binDir, { recursive: true });
97
+ const stubPath = path.join(binDir, cmdName);
98
+ vfs.writeFileSync(
99
+ stubPath,
100
+ `#!/usr/bin/env node\nrequire("${targetPath}");\n`,
101
+ );
102
+ }
103
+ }
104
+
105
+ /** Replace `/` in scoped names with `+` for flat store keys (matches real pnpm). */
106
+ function sanitiseName(name: string): string {
107
+ return name.replace(/\//g, "+");
108
+ }
@@ -0,0 +1,120 @@
1
+ import type { PackageCache } from "./cache";
2
+
3
+ export interface RegistryOptions {
4
+ registry?: string;
5
+ token?: string;
6
+ /** Optional package cache for manifest and tarball caching. */
7
+ cache?: PackageCache;
8
+ }
9
+
10
+ export interface PackageManifest {
11
+ name: string;
12
+ "dist-tags": Record<string, string>;
13
+ versions: Record<string, PackageVersion>;
14
+ }
15
+
16
+ export interface PackageVersion {
17
+ name: string;
18
+ version: string;
19
+ dependencies?: Record<string, string>;
20
+ devDependencies?: Record<string, string>;
21
+ peerDependencies?: Record<string, string>;
22
+ optionalDependencies?: Record<string, string>;
23
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
24
+ bin?: string | Record<string, string>;
25
+ main?: string;
26
+ module?: string;
27
+ exports?: unknown;
28
+ dist: {
29
+ tarball: string;
30
+ shasum: string;
31
+ integrity?: string;
32
+ };
33
+ }
34
+
35
+ export class Registry {
36
+ private baseUrl: string;
37
+ private token?: string;
38
+ private inMemoryCache = new Map<string, PackageManifest>();
39
+ private packageCache?: PackageCache;
40
+
41
+ constructor(options: RegistryOptions = {}) {
42
+ this.baseUrl = (options.registry || "https://registry.npmjs.org").replace(
43
+ /\/$/,
44
+ "",
45
+ );
46
+ this.token = options.token;
47
+ this.packageCache = options.cache;
48
+ }
49
+
50
+ async getManifest(name: string): Promise<PackageManifest> {
51
+ // Check in-memory cache first (legacy behaviour)
52
+ if (this.inMemoryCache.has(name)) return this.inMemoryCache.get(name)!;
53
+
54
+ // Check package cache (with TTL)
55
+ if (this.packageCache) {
56
+ return this.packageCache.getManifest(name, () =>
57
+ this.fetchManifest(name),
58
+ );
59
+ }
60
+
61
+ return this.fetchManifest(name);
62
+ }
63
+
64
+ private async fetchManifest(name: string): Promise<PackageManifest> {
65
+ const encodedName = name.startsWith("@")
66
+ ? `@${encodeURIComponent(name.slice(1))}`
67
+ : encodeURIComponent(name);
68
+ const headers: Record<string, string> = { Accept: "application/json" };
69
+ if (this.token) {
70
+ headers["Authorization"] = `Bearer ${this.token}`;
71
+ }
72
+ const response = await fetch(`${this.baseUrl}/${encodedName}`, {
73
+ headers,
74
+ });
75
+
76
+ if (!response.ok)
77
+ throw new Error(`Failed to fetch package ${name}: ${response.status}`);
78
+
79
+ const manifest = (await response.json()) as PackageManifest;
80
+ this.inMemoryCache.set(name, manifest);
81
+ return manifest;
82
+ }
83
+
84
+ async getVersion(name: string, version: string): Promise<PackageVersion> {
85
+ const manifest = await this.getManifest(name);
86
+ if (manifest["dist-tags"][version])
87
+ version = manifest["dist-tags"][version];
88
+ const pkgVersion = manifest.versions[version];
89
+ if (!pkgVersion)
90
+ throw new Error(`Version ${version} not found for ${name}`);
91
+ return pkgVersion;
92
+ }
93
+
94
+ async downloadTarball(url: string): Promise<ArrayBuffer> {
95
+ // Check tarball cache
96
+ if (this.packageCache) {
97
+ const data = await this.packageCache.getTarball(url, async () => {
98
+ const raw = await this.fetchTarball(url);
99
+ return new Uint8Array(raw);
100
+ });
101
+ return data.buffer as ArrayBuffer;
102
+ }
103
+ return this.fetchTarball(url);
104
+ }
105
+
106
+ private async fetchTarball(url: string): Promise<ArrayBuffer> {
107
+ const headers: Record<string, string> = {};
108
+ if (this.token) {
109
+ headers["Authorization"] = `Bearer ${this.token}`;
110
+ }
111
+ const response = await fetch(url, { headers });
112
+ if (!response.ok)
113
+ throw new Error(`Failed to download tarball: ${response.status}`);
114
+ return response.arrayBuffer();
115
+ }
116
+
117
+ clearCache(): void {
118
+ this.inMemoryCache.clear();
119
+ }
120
+ }
@@ -0,0 +1,339 @@
1
+ import type { Registry, PackageVersion } from "./registry";
2
+
3
+ export interface ResolvedPackage {
4
+ name: string;
5
+ version: string;
6
+ dependencies: Record<string, string>;
7
+ dist: { tarball: string; shasum: string };
8
+ bin?: string | Record<string, string>;
9
+ }
10
+
11
+ export interface ResolveOptions {
12
+ registry: Registry;
13
+ includeDev?: boolean;
14
+ includeOptional?: boolean;
15
+ }
16
+
17
+ export class SemVer {
18
+ readonly major: number;
19
+ readonly minor: number;
20
+ readonly patch: number;
21
+ readonly pre: string;
22
+
23
+ constructor(raw: string) {
24
+ const cleaned = raw.replace(/^[=v]+/, "");
25
+ const [mainPart, prePart = ""] = cleaned.split("-");
26
+ const nums = mainPart.split(".").map(Number);
27
+ this.major = nums[0] || 0;
28
+ this.minor = nums[1] || 0;
29
+ this.patch = nums[2] || 0;
30
+ this.pre = prePart;
31
+ }
32
+
33
+ compareTo(other: SemVer): number {
34
+ if (this.major !== other.major) return this.major - other.major;
35
+ if (this.minor !== other.minor) return this.minor - other.minor;
36
+ if (this.patch !== other.patch) return this.patch - other.patch;
37
+ if (!this.pre && other.pre) return 1;
38
+ if (this.pre && !other.pre) return -1;
39
+ if (this.pre && other.pre) {
40
+ // Compare prerelease segments per semver spec:
41
+ // split on ".", compare each segment numerically if both numeric, otherwise lexically
42
+ const aParts = this.pre.split(".");
43
+ const bParts = other.pre.split(".");
44
+ const len = Math.max(aParts.length, bParts.length);
45
+ for (let i = 0; i < len; i++) {
46
+ if (i >= aParts.length) return -1; // fewer segments = lower precedence
47
+ if (i >= bParts.length) return 1;
48
+ const aNum = /^\d+$/.test(aParts[i]);
49
+ const bNum = /^\d+$/.test(bParts[i]);
50
+ if (aNum && bNum) {
51
+ const diff = Number(aParts[i]) - Number(bParts[i]);
52
+ if (diff !== 0) return diff;
53
+ } else if (aNum !== bNum) {
54
+ // Numeric identifiers have lower precedence than string identifiers
55
+ return aNum ? -1 : 1;
56
+ } else {
57
+ if (aParts[i] < bParts[i]) return -1;
58
+ if (aParts[i] > bParts[i]) return 1;
59
+ }
60
+ }
61
+ }
62
+ return 0;
63
+ }
64
+
65
+ matches(range: string): boolean {
66
+ return satisfies(this.toString(), range);
67
+ }
68
+
69
+ toString(): string {
70
+ const base = `${this.major}.${this.minor}.${this.patch}`;
71
+ return this.pre ? `${base}-${this.pre}` : base;
72
+ }
73
+ }
74
+
75
+ interface RangeConstraint {
76
+ op:
77
+ | "="
78
+ | ">="
79
+ | "<="
80
+ | ">"
81
+ | "<"
82
+ | "^"
83
+ | "~"
84
+ | "partial-major"
85
+ | "partial-minor"
86
+ | "any";
87
+ target: SemVer;
88
+ raw?: string;
89
+ }
90
+
91
+ function parseConstraint(token: string): RangeConstraint | null {
92
+ const t = token.trim();
93
+ if (!t || t === "*" || t === "latest")
94
+ return { op: "any", target: new SemVer("0.0.0") };
95
+ if (t.startsWith("npm:")) return { op: "any", target: new SemVer("0.0.0") };
96
+
97
+ if (t.startsWith(">=")) return { op: ">=", target: new SemVer(t.slice(2)) };
98
+ if (t.startsWith("<=")) return { op: "<=", target: new SemVer(t.slice(2)) };
99
+ if (t.startsWith(">") && !t.startsWith(">="))
100
+ return { op: ">", target: new SemVer(t.slice(1)) };
101
+ if (t.startsWith("<") && !t.startsWith("<="))
102
+ return { op: "<", target: new SemVer(t.slice(1)) };
103
+ if (t.startsWith("^")) return { op: "^", target: new SemVer(t.slice(1)) };
104
+ if (t.startsWith("~")) return { op: "~", target: new SemVer(t.slice(1)) };
105
+ if (t.startsWith("=")) return { op: "=", target: new SemVer(t.slice(1)) };
106
+
107
+ const dots = (t.match(/\./g) || []).length;
108
+ if (dots === 0 && /^\d+$/.test(t))
109
+ return { op: "partial-major", target: new SemVer(t + ".0.0") };
110
+ if (dots === 1 && /^\d+\.\d+$/.test(t))
111
+ return { op: "partial-minor", target: new SemVer(t + ".0") };
112
+
113
+ return { op: "=", target: new SemVer(t) };
114
+ }
115
+
116
+ function testConstraint(ver: SemVer, c: RangeConstraint): boolean {
117
+ switch (c.op) {
118
+ case "any":
119
+ return true;
120
+ case "=":
121
+ return ver.compareTo(c.target) === 0;
122
+ case ">=":
123
+ return ver.compareTo(c.target) >= 0;
124
+ case "<=":
125
+ return ver.compareTo(c.target) <= 0;
126
+ case ">":
127
+ return ver.compareTo(c.target) > 0;
128
+ case "<":
129
+ return ver.compareTo(c.target) < 0;
130
+ case "~":
131
+ return (
132
+ ver.major === c.target.major &&
133
+ ver.minor === c.target.minor &&
134
+ ver.patch >= c.target.patch
135
+ );
136
+ case "^": {
137
+ const r = c.target;
138
+ if (ver.compareTo(r) < 0) return false; // below minimum
139
+ if (r.major > 0) return ver.major === r.major;
140
+ if (r.minor > 0) return ver.major === 0 && ver.minor === r.minor;
141
+ return ver.major === 0 && ver.minor === 0 && ver.patch === r.patch;
142
+ }
143
+ case "partial-major":
144
+ return ver.major === c.target.major;
145
+ case "partial-minor":
146
+ return ver.major === c.target.major && ver.minor === c.target.minor;
147
+ }
148
+ }
149
+
150
+ function satisfies(version: string, range: string): boolean {
151
+ range = range.trim();
152
+ if (range === "*" || range === "" || range === "latest") return true;
153
+ if (range.startsWith("npm:")) return true;
154
+
155
+ if (range.includes("||")) {
156
+ return range.split("||").some(alt => satisfies(version, alt.trim()));
157
+ }
158
+
159
+ range = range.replace(/(>=?|<=?|[~^=])\s+/g, "$1");
160
+
161
+ const tokens = range.split(/\s+/).filter(Boolean);
162
+ if (tokens.length > 1 && tokens[1] === "-" && tokens.length === 3) {
163
+ return (
164
+ satisfies(version, `>=${tokens[0]}`) &&
165
+ satisfies(version, `<=${tokens[2]}`)
166
+ );
167
+ }
168
+
169
+ const ver = new SemVer(version);
170
+ for (const tok of tokens) {
171
+ const c = parseConstraint(tok);
172
+ if (!c || !testConstraint(ver, c)) return false;
173
+ }
174
+ return true;
175
+ }
176
+
177
+ function compareVersions(a: string, b: string): number {
178
+ return new SemVer(a).compareTo(new SemVer(b));
179
+ }
180
+
181
+ function findBestVersion(versions: string[], range: string): string | null {
182
+ const valid = versions.filter(v => satisfies(v, range));
183
+ if (valid.length === 0) return null;
184
+ return valid.sort(compareVersions).pop()!;
185
+ }
186
+
187
+ interface QueueEntry {
188
+ name: string;
189
+ range: string;
190
+ optional?: boolean;
191
+ }
192
+
193
+ export async function resolveDependencies(
194
+ name: string,
195
+ versionRange: string,
196
+ options: ResolveOptions,
197
+ ): Promise<Map<string, ResolvedPackage>> {
198
+ const resolved = new Map<string, ResolvedPackage>();
199
+ const visited = new Set<string>();
200
+ const queue: QueueEntry[] = [{ name, range: versionRange }];
201
+
202
+ while (queue.length > 0) {
203
+ const batch = queue.splice(0, queue.length);
204
+ const pending = batch.filter(entry => {
205
+ const key = `${entry.name}@${entry.range}`;
206
+ if (visited.has(key)) return false;
207
+ visited.add(key);
208
+ return true;
209
+ });
210
+
211
+ const results = await Promise.all(
212
+ pending.map(async entry => {
213
+ try {
214
+ const manifest = await options.registry.getManifest(entry.name);
215
+ const versions = Object.keys(manifest.versions);
216
+ let target = entry.range;
217
+
218
+ if (manifest["dist-tags"][entry.range]) {
219
+ target = manifest["dist-tags"][entry.range];
220
+ } else {
221
+ const best = findBestVersion(versions, entry.range);
222
+ if (!best) {
223
+ if (entry.optional) return null;
224
+ throw new Error(
225
+ `No matching version for ${entry.name}@${entry.range}`,
226
+ );
227
+ }
228
+ target = best;
229
+ }
230
+
231
+ if (resolved.has(entry.name)) return null;
232
+
233
+ const pkgVersion = manifest.versions[target];
234
+ if (!pkgVersion) {
235
+ if (entry.optional) return null;
236
+ throw new Error(`Version ${target} not found for ${entry.name}`);
237
+ }
238
+
239
+ const deps = mergePeerDeps(
240
+ pkgVersion.dependencies || {},
241
+ pkgVersion.peerDependencies || {},
242
+ pkgVersion.peerDependenciesMeta,
243
+ );
244
+
245
+ // Include optional dependencies when includeOptional is set
246
+ if (options.includeOptional && pkgVersion.optionalDependencies) {
247
+ for (const [optName, optRange] of Object.entries(
248
+ pkgVersion.optionalDependencies,
249
+ )) {
250
+ if (!deps[optName]) {
251
+ deps[optName] = optRange;
252
+ }
253
+ }
254
+ }
255
+
256
+ // Track which dependency names came from optionalDependencies
257
+ const optionalDepNames = new Set<string>(
258
+ options.includeOptional && pkgVersion.optionalDependencies
259
+ ? Object.keys(pkgVersion.optionalDependencies)
260
+ : [],
261
+ );
262
+
263
+ const pkg: ResolvedPackage = {
264
+ name: entry.name,
265
+ version: target,
266
+ dependencies: deps,
267
+ dist: pkgVersion.dist,
268
+ bin: pkgVersion.bin,
269
+ };
270
+ return { pkg, optionalDepNames };
271
+ } catch (err) {
272
+ // Optional dependencies should fail gracefully
273
+ if (entry.optional) return null;
274
+ throw err;
275
+ }
276
+ }),
277
+ );
278
+
279
+ for (const result of results) {
280
+ if (!result || resolved.has(result.pkg.name)) continue;
281
+ const { pkg, optionalDepNames } = result;
282
+ resolved.set(pkg.name, pkg);
283
+ for (const [depName, depRange] of Object.entries(pkg.dependencies)) {
284
+ queue.push({
285
+ name: depName,
286
+ range: depRange,
287
+ optional: optionalDepNames.has(depName),
288
+ });
289
+ }
290
+ }
291
+ }
292
+
293
+ return resolved;
294
+ }
295
+
296
+ export async function resolveFromPackageJson(
297
+ packageJson: {
298
+ dependencies?: Record<string, string>;
299
+ devDependencies?: Record<string, string>;
300
+ },
301
+ options: ResolveOptions,
302
+ ): Promise<Map<string, ResolvedPackage>> {
303
+ const all = new Map<string, ResolvedPackage>();
304
+ const deps = { ...packageJson.dependencies };
305
+ if (options.includeDev) Object.assign(deps, packageJson.devDependencies);
306
+
307
+ const results = await Promise.all(
308
+ Object.entries(deps).map(([n, r]) =>
309
+ resolveDependencies(n, r, options).catch(
310
+ () => new Map<string, ResolvedPackage>(),
311
+ ),
312
+ ),
313
+ );
314
+
315
+ for (const result of results) {
316
+ for (const [n, pkg] of result) {
317
+ if (!all.has(n)) all.set(n, pkg);
318
+ }
319
+ }
320
+
321
+ return all;
322
+ }
323
+
324
+ /** Merge non-optional peerDependencies into deps. Existing deps take precedence. */
325
+ export function mergePeerDeps(
326
+ deps: Record<string, string>,
327
+ peerDeps: Record<string, string>,
328
+ peerMeta?: Record<string, { optional?: boolean }>,
329
+ ): Record<string, string> {
330
+ const merged = { ...deps };
331
+ for (const [name, range] of Object.entries(peerDeps)) {
332
+ if (!peerMeta?.[name]?.optional && !merged[name]) {
333
+ merged[name] = range;
334
+ }
335
+ }
336
+ return merged;
337
+ }
338
+
339
+ export { satisfies, compareVersions, findBestVersion };