@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.
- package/dist/browser-bundle.d.ts +40 -0
- package/dist/builtins.d.ts +22 -0
- package/dist/code-transform.d.ts +7 -0
- package/dist/config/cdn.d.ts +13 -0
- package/dist/container.d.ts +101 -0
- package/dist/dev-server.d.ts +69 -0
- package/dist/errors.d.ts +19 -0
- package/dist/frameworks/code-transforms.d.ts +32 -0
- package/dist/frameworks/next-api-handler.d.ts +72 -0
- package/dist/frameworks/next-dev-server.d.ts +141 -0
- package/dist/frameworks/next-html-generator.d.ts +36 -0
- package/dist/frameworks/next-route-resolver.d.ts +19 -0
- package/dist/frameworks/next-shims.d.ts +78 -0
- package/dist/frameworks/remix-dev-server.d.ts +47 -0
- package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
- package/dist/frameworks/vite-dev-server.d.ts +50 -0
- package/dist/fs-errors.d.ts +36 -0
- package/dist/index.cjs +14916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.mjs +14898 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kernel.d.ts +48 -0
- package/dist/memfs.d.ts +144 -0
- package/dist/metrics.d.ts +78 -0
- package/dist/module-resolver.d.ts +60 -0
- package/dist/network-interceptor.d.ts +71 -0
- package/dist/npm/cache.d.ts +76 -0
- package/dist/npm/index.d.ts +60 -0
- package/dist/npm/lockfile-reader.d.ts +32 -0
- package/dist/npm/pnpm.d.ts +18 -0
- package/dist/npm/registry.d.ts +45 -0
- package/dist/npm/resolver.d.ts +39 -0
- package/dist/npm/sync-installer.d.ts +18 -0
- package/dist/npm/tarball.d.ts +4 -0
- package/dist/npm/workspaces.d.ts +46 -0
- package/dist/persistence.d.ts +94 -0
- package/dist/plugin.d.ts +156 -0
- package/dist/polyfills/assert.d.ts +30 -0
- package/dist/polyfills/child_process.d.ts +116 -0
- package/dist/polyfills/chokidar.d.ts +18 -0
- package/dist/polyfills/crypto.d.ts +49 -0
- package/dist/polyfills/events.d.ts +28 -0
- package/dist/polyfills/fs.d.ts +82 -0
- package/dist/polyfills/http.d.ts +147 -0
- package/dist/polyfills/module.d.ts +29 -0
- package/dist/polyfills/net.d.ts +53 -0
- package/dist/polyfills/os.d.ts +91 -0
- package/dist/polyfills/path.d.ts +96 -0
- package/dist/polyfills/perf_hooks.d.ts +21 -0
- package/dist/polyfills/process.d.ts +99 -0
- package/dist/polyfills/querystring.d.ts +15 -0
- package/dist/polyfills/readdirp.d.ts +18 -0
- package/dist/polyfills/readline.d.ts +32 -0
- package/dist/polyfills/stream.d.ts +106 -0
- package/dist/polyfills/stubs.d.ts +737 -0
- package/dist/polyfills/tty.d.ts +25 -0
- package/dist/polyfills/url.d.ts +41 -0
- package/dist/polyfills/util.d.ts +61 -0
- package/dist/polyfills/v8.d.ts +43 -0
- package/dist/polyfills/vm.d.ts +76 -0
- package/dist/polyfills/worker-threads.d.ts +77 -0
- package/dist/polyfills/ws.d.ts +32 -0
- package/dist/polyfills/zlib.d.ts +87 -0
- package/dist/runtime-helpers.d.ts +4 -0
- package/dist/runtime-interface.d.ts +39 -0
- package/dist/sandbox.d.ts +69 -0
- package/dist/server-bridge.d.ts +55 -0
- package/dist/shell-commands.d.ts +2 -0
- package/dist/shell.d.ts +101 -0
- package/dist/transpiler.d.ts +47 -0
- package/dist/type-checker.d.ts +57 -0
- package/dist/types/package-json.d.ts +17 -0
- package/dist/utils/binary-encoding.d.ts +4 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/safe-path.d.ts +6 -0
- package/dist/worker-runtime.d.ts +34 -0
- package/package.json +59 -0
- package/src/browser-bundle.ts +498 -0
- package/src/builtins.ts +222 -0
- package/src/code-transform.ts +183 -0
- package/src/config/cdn.ts +17 -0
- package/src/container.ts +343 -0
- package/src/dev-server.ts +322 -0
- package/src/errors.ts +604 -0
- package/src/frameworks/code-transforms.ts +667 -0
- package/src/frameworks/next-api-handler.ts +366 -0
- package/src/frameworks/next-dev-server.ts +1252 -0
- package/src/frameworks/next-html-generator.ts +585 -0
- package/src/frameworks/next-route-resolver.ts +521 -0
- package/src/frameworks/next-shims.ts +1084 -0
- package/src/frameworks/remix-dev-server.ts +163 -0
- package/src/frameworks/sveltekit-dev-server.ts +197 -0
- package/src/frameworks/vite-dev-server.ts +370 -0
- package/src/fs-errors.ts +118 -0
- package/src/index.ts +188 -0
- package/src/kernel.ts +381 -0
- package/src/memfs.ts +1006 -0
- package/src/metrics.ts +140 -0
- package/src/module-resolver.ts +511 -0
- package/src/network-interceptor.ts +143 -0
- package/src/npm/cache.ts +172 -0
- package/src/npm/index.ts +377 -0
- package/src/npm/lockfile-reader.ts +105 -0
- package/src/npm/pnpm.ts +108 -0
- package/src/npm/registry.ts +120 -0
- package/src/npm/resolver.ts +339 -0
- package/src/npm/sync-installer.ts +217 -0
- package/src/npm/tarball.ts +136 -0
- package/src/npm/workspaces.ts +255 -0
- package/src/persistence.ts +235 -0
- package/src/plugin.ts +293 -0
- package/src/polyfills/assert.ts +164 -0
- package/src/polyfills/child_process.ts +535 -0
- package/src/polyfills/chokidar.ts +52 -0
- package/src/polyfills/crypto.ts +433 -0
- package/src/polyfills/events.ts +178 -0
- package/src/polyfills/fs.ts +297 -0
- package/src/polyfills/http.ts +478 -0
- package/src/polyfills/module.ts +97 -0
- package/src/polyfills/net.ts +123 -0
- package/src/polyfills/os.ts +108 -0
- package/src/polyfills/path.ts +169 -0
- package/src/polyfills/perf_hooks.ts +30 -0
- package/src/polyfills/process.ts +349 -0
- package/src/polyfills/querystring.ts +66 -0
- package/src/polyfills/readdirp.ts +72 -0
- package/src/polyfills/readline.ts +80 -0
- package/src/polyfills/stream.ts +610 -0
- package/src/polyfills/stubs.ts +600 -0
- package/src/polyfills/tty.ts +43 -0
- package/src/polyfills/url.ts +97 -0
- package/src/polyfills/util.ts +173 -0
- package/src/polyfills/v8.ts +62 -0
- package/src/polyfills/vm.ts +111 -0
- package/src/polyfills/worker-threads.ts +189 -0
- package/src/polyfills/ws.ts +73 -0
- package/src/polyfills/zlib.ts +244 -0
- package/src/runtime-helpers.ts +83 -0
- package/src/runtime-interface.ts +46 -0
- package/src/sandbox.ts +178 -0
- package/src/server-bridge.ts +473 -0
- package/src/service-worker.ts +153 -0
- package/src/shell-commands.ts +708 -0
- package/src/shell.ts +795 -0
- package/src/transpiler.ts +282 -0
- package/src/type-checker.ts +241 -0
- package/src/types/package-json.ts +17 -0
- package/src/utils/binary-encoding.ts +38 -0
- package/src/utils/hash.ts +24 -0
- package/src/utils/safe-path.ts +38 -0
- 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
|
+
}
|
package/src/npm/pnpm.ts
ADDED
|
@@ -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 };
|