@mintlify/package-builder 0.0.0-stage → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +66 -2
  3. package/dist/build.d.ts +4 -0
  4. package/dist/build.js +338 -0
  5. package/dist/bundle.d.ts +44 -0
  6. package/dist/bundle.js +681 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.js +112 -0
  9. package/dist/diagnostics.d.ts +11 -0
  10. package/dist/diagnostics.js +32 -0
  11. package/dist/hashing.d.ts +28 -0
  12. package/dist/hashing.js +98 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.js +11 -0
  15. package/dist/install.d.ts +34 -0
  16. package/dist/install.js +325 -0
  17. package/dist/local-files.d.ts +11 -0
  18. package/dist/local-files.js +120 -0
  19. package/dist/lockfile.d.ts +40 -0
  20. package/dist/lockfile.js +498 -0
  21. package/dist/package-json.d.ts +14 -0
  22. package/dist/package-json.js +170 -0
  23. package/dist/package-manager.d.ts +17 -0
  24. package/dist/package-manager.js +87 -0
  25. package/dist/react.d.ts +37 -0
  26. package/dist/react.js +264 -0
  27. package/dist/render.d.ts +37 -0
  28. package/dist/render.js +292 -0
  29. package/dist/runtime/client.d.ts +11 -0
  30. package/dist/runtime/client.js +54 -0
  31. package/dist/runtime/server.d.ts +6 -0
  32. package/dist/runtime/server.js +11 -0
  33. package/dist/runtime/shell.d.ts +21 -0
  34. package/dist/runtime/shell.js +68 -0
  35. package/dist/runtime/theme.d.ts +1 -0
  36. package/dist/runtime/theme.js +1 -0
  37. package/dist/sanitize.d.ts +9 -0
  38. package/dist/sanitize.js +650 -0
  39. package/dist/semver.d.ts +2 -0
  40. package/dist/semver.js +10 -0
  41. package/dist/tsconfig.build.tsbuildinfo +1 -0
  42. package/dist/types.d.ts +98 -0
  43. package/dist/types.js +1 -0
  44. package/dist/version.d.ts +1 -0
  45. package/dist/version.js +11 -0
  46. package/package.json +74 -4
package/dist/cli.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { build } from './build.js';
6
+ const USAGE = `Usage: mint-package-builder build --input <input.json> --out <dir> [--installer local|npm-ci|prepared] [--json]
7
+
8
+ input.json holds a BuildInput (docsRoot, packages, islands, snippetFiles, installer, registryTokens,
9
+ expected). With expected { depsHash, islandsHash } the manifest carries those hashes and the build
10
+ fails if the builder computes different ones.
11
+ --out overrides outDir; --installer overrides installer. Exits 1 when the build has errors.`;
12
+ export function parseArgs(argv) {
13
+ const args = { json: false };
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const arg = argv[i];
16
+ if (arg === '--input')
17
+ args.input = argv[++i];
18
+ else if (arg === '--out')
19
+ args.out = argv[++i];
20
+ else if (arg === '--installer')
21
+ args.installer = argv[++i];
22
+ else if (arg === '--json')
23
+ args.json = true;
24
+ else if (!args.command && arg && !arg.startsWith('-'))
25
+ args.command = arg;
26
+ else
27
+ throw new Error(`Unknown argument: ${arg}`);
28
+ }
29
+ return args;
30
+ }
31
+ function readExpected(value) {
32
+ const record = typeof value === 'object' && value !== null ? value : {};
33
+ const text = (field) => (typeof field === 'string' ? field : '');
34
+ return { depsHash: text(record.depsHash), islandsHash: text(record.islandsHash) };
35
+ }
36
+ function kb(bytes) {
37
+ return `${(bytes / 1024).toFixed(1)} KB`;
38
+ }
39
+ export function formatResult(result) {
40
+ const { manifest, stats } = result;
41
+ const lines = [
42
+ `Build ${manifest.buildId} ${result.ok ? 'succeeded' : 'failed'}`,
43
+ `react ${manifest.react.version || '?'} / react-dom ${manifest.react.domVersion || '?'}`,
44
+ `install ${Math.round(stats.installMs)} ms, bundle ${Math.round(stats.bundleMs)} ms, render ${Math.round(stats.renderMs)} ms`,
45
+ `client ${kb(stats.clientBytes)} (${kb(stats.clientGzipBytes)} gzip), server ${kb(stats.serverBytes)}`,
46
+ ];
47
+ for (const [name, { version }] of Object.entries(manifest.packages))
48
+ lines.push(` ${name}@${version}`);
49
+ for (const [id, island] of Object.entries(manifest.islands)) {
50
+ const gz = stats.islandClientGzipBytes[id];
51
+ lines.push(` island ${id}: ${island.html !== undefined ? `ssr ${island.ssrMs ?? 0} ms` : 'client-only'}${gz !== undefined ? `, ${kb(gz)} gzip JS` : ''}`);
52
+ }
53
+ for (const d of manifest.diagnostics)
54
+ lines.push(`${d.level === 'error' ? 'error' : 'warn '} [${d.code}]${d.pkg ? ` ${d.pkg}:` : ''} ${d.message}`);
55
+ return lines.join('\n');
56
+ }
57
+ export async function main(argv) {
58
+ let args;
59
+ try {
60
+ args = parseArgs(argv);
61
+ }
62
+ catch (err) {
63
+ process.stderr.write(`${err.message}\n${USAGE}\n`);
64
+ return 2;
65
+ }
66
+ if (args.command !== 'build' || !args.input) {
67
+ process.stderr.write(`${USAGE}\n`);
68
+ return 2;
69
+ }
70
+ const raw = JSON.parse(readFileSync(resolve(args.input), 'utf8'));
71
+ const outDir = args.out ?? raw.outDir;
72
+ if (!outDir || !raw.docsRoot) {
73
+ process.stderr.write(`input.json needs docsRoot, and --out or outDir is required.\n${USAGE}\n`);
74
+ return 2;
75
+ }
76
+ const installer = args.installer ?? raw.installer ?? 'npm-ci';
77
+ if (installer !== 'local' && installer !== 'npm-ci' && installer !== 'prepared') {
78
+ process.stderr.write(`--installer must be local, npm-ci or prepared\n`);
79
+ return 2;
80
+ }
81
+ const result = await build({
82
+ docsRoot: resolve(raw.docsRoot),
83
+ packages: raw.packages ?? {},
84
+ islands: raw.islands ?? [],
85
+ snippetFiles: raw.snippetFiles ?? {},
86
+ outDir: resolve(outDir),
87
+ installer,
88
+ registryTokens: raw.registryTokens,
89
+ packageManagers: raw.packageManagers,
90
+ renderMemoryLimitBytes: raw.renderMemoryLimitBytes,
91
+ ...(raw.expected !== undefined ? { expected: readExpected(raw.expected) } : {}),
92
+ });
93
+ process.stderr.write(`${formatResult(result)}\n`);
94
+ if (args.json)
95
+ process.stdout.write(`${JSON.stringify(result)}\n`);
96
+ return result.ok ? 0 : 1;
97
+ }
98
+ function invokedDirectly() {
99
+ try {
100
+ return (process.argv[1] !== undefined &&
101
+ realpathSync(process.argv[1]) === fileURLToPath(import.meta.url));
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ if (invokedDirectly()) {
108
+ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
109
+ process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
110
+ process.exit(1);
111
+ });
112
+ }
@@ -0,0 +1,11 @@
1
+ import type { BuildDiagnostic } from './types.js';
2
+ export declare class Diagnostics {
3
+ readonly list: BuildDiagnostic[];
4
+ private readonly seen;
5
+ add(diagnostic: BuildDiagnostic): void;
6
+ error(code: string, message: string, pkg?: string): void;
7
+ warn(code: string, message: string, pkg?: string): void;
8
+ addAll(diagnostics: BuildDiagnostic[]): void;
9
+ hasErrors(): boolean;
10
+ }
11
+ export declare function hasErrors(diagnostics: BuildDiagnostic[]): boolean;
@@ -0,0 +1,32 @@
1
+ export class Diagnostics {
2
+ constructor() {
3
+ this.list = [];
4
+ this.seen = new Set();
5
+ }
6
+ add(diagnostic) {
7
+ const key = `${diagnostic.level}|${diagnostic.code}|${diagnostic.pkg ?? ''}|${diagnostic.message}`;
8
+ if (this.seen.has(key))
9
+ return;
10
+ this.seen.add(key);
11
+ this.list.push(diagnostic);
12
+ }
13
+ error(code, message, pkg) {
14
+ this.add(withPkg({ level: 'error', code, message }, pkg));
15
+ }
16
+ warn(code, message, pkg) {
17
+ this.add(withPkg({ level: 'warning', code, message }, pkg));
18
+ }
19
+ addAll(diagnostics) {
20
+ for (const d of diagnostics)
21
+ this.add(d);
22
+ }
23
+ hasErrors() {
24
+ return this.list.some((d) => d.level === 'error');
25
+ }
26
+ }
27
+ function withPkg(d, pkg) {
28
+ return pkg === undefined ? d : { ...d, pkg };
29
+ }
30
+ export function hasErrors(diagnostics) {
31
+ return diagnostics.some((d) => d.level === 'error');
32
+ }
@@ -0,0 +1,28 @@
1
+ import type { IslandSpec } from './types.js';
2
+ export declare const DEPS_HASH_ALGORITHM = "mint-pkg-deps/1";
3
+ export declare const ISLANDS_HASH_ALGORITHM = "mint-pkg-islands/1";
4
+ export type HashLockfileKind = 'npm' | 'pnpm';
5
+ export type DepsHashInput = {
6
+ lockfileBytes: Uint8Array | string;
7
+ lockfileKind: HashLockfileKind;
8
+ dependencies: Record<string, string>;
9
+ registries?: Record<string, string>;
10
+ builderVersion: string;
11
+ };
12
+ export type IslandsHashInput = {
13
+ islands: Pick<IslandSpec, 'id' | 'kind' | 'module'>[];
14
+ snippetFiles: Record<string, string | Uint8Array>;
15
+ provider?: string;
16
+ css?: string[];
17
+ ssr?: boolean;
18
+ isolation?: 'page' | 'frame';
19
+ };
20
+ export declare function sha256Hex(data: string | Uint8Array): string;
21
+ export declare function sha384Integrity(data: Uint8Array | string): string;
22
+ export declare function gzipSize(data: Uint8Array | string): number;
23
+ export declare function stableStringify(value: unknown): string;
24
+ export declare function computeDepsHash(input: DepsHashInput): string;
25
+ export declare function computeIslandsHash(input: IslandsHashInput): string;
26
+ export declare function computeBuildId(depsHash: string, islandsHash: string): string;
27
+ export declare function islandIdFor(moduleSource: string): string;
28
+ export declare function islandPrefix(id: string): string;
@@ -0,0 +1,98 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { gzipSync } from 'node:zlib';
3
+ /*
4
+ * Build hashes: the one canonical definition.
5
+ *
6
+ * The server (S1, api/utils/packages/packageBuildHashes.ts) and the package-builder worker (S2,
7
+ * src/hashes.ts) carry ports of these functions. All three must produce the same output for the
8
+ * vectors in __test__/fixtures/hash-vectors.json; that file is copied byte for byte into both
9
+ * repos. Change the algorithm only by bumping the `algorithm` tag and every copy together.
10
+ *
11
+ * Definitions:
12
+ * H(bytes) lowercase hex SHA-256. A string is hashed as its UTF-8 bytes.
13
+ * canonical(v) JSON with no whitespace. Object keys are sorted by UTF-16 code unit order
14
+ * (JavaScript `<` on strings) and keys whose value is `undefined` are dropped.
15
+ * Arrays keep their order. Strings and numbers use JSON.stringify.
16
+ *
17
+ * depsHash = H(canonical({
18
+ * algorithm: 'mint-pkg-deps/1',
19
+ * builderVersion, // @mintlify/package-builder version
20
+ * lockfileKind, // 'npm' | 'pnpm'
21
+ * lockfile: H(lockfileBytes), // the lockfile exactly as installed
22
+ * dependencies, // package.json "dependencies", {} when absent
23
+ * registries, // docs.json packages.registries as written, {} when absent
24
+ * }))
25
+ *
26
+ * islandsHash = H(canonical({
27
+ * algorithm: 'mint-pkg-islands/1',
28
+ * islands: [{ id, kind, module: H(module) }] sorted by id,
29
+ * snippets: { [path]: H(source) },
30
+ * provider: provider ?? null,
31
+ * css: css ?? [],
32
+ * ssr: ssr ?? true,
33
+ * isolation: isolation ?? 'page',
34
+ * }))
35
+ *
36
+ * buildId = H(depsHash + islandsHash).slice(0, 32)
37
+ *
38
+ * Island `pagePath` and `imports` are left out on purpose: neither changes the build output, and
39
+ * leaving them out lets a build be reused when an unchanged island moves to another page.
40
+ */
41
+ export const DEPS_HASH_ALGORITHM = 'mint-pkg-deps/1';
42
+ export const ISLANDS_HASH_ALGORITHM = 'mint-pkg-islands/1';
43
+ export function sha256Hex(data) {
44
+ return createHash('sha256').update(data).digest('hex');
45
+ }
46
+ export function sha384Integrity(data) {
47
+ return `sha384-${createHash('sha384').update(data).digest('base64')}`;
48
+ }
49
+ export function gzipSize(data) {
50
+ return gzipSync(data, { level: 9 }).length;
51
+ }
52
+ const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
53
+ export function stableStringify(value) {
54
+ if (value === undefined)
55
+ return 'null';
56
+ if (value === null || typeof value !== 'object')
57
+ return JSON.stringify(value);
58
+ if (Array.isArray(value))
59
+ return `[${value.map(stableStringify).join(',')}]`;
60
+ const entries = Object.entries(value)
61
+ .filter(([, v]) => v !== undefined)
62
+ .sort(([a], [b]) => byCodeUnit(a, b));
63
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}`;
64
+ }
65
+ export function computeDepsHash(input) {
66
+ return sha256Hex(stableStringify({
67
+ algorithm: DEPS_HASH_ALGORITHM,
68
+ builderVersion: input.builderVersion,
69
+ lockfileKind: input.lockfileKind,
70
+ lockfile: sha256Hex(input.lockfileBytes),
71
+ dependencies: input.dependencies,
72
+ registries: input.registries ?? {},
73
+ }));
74
+ }
75
+ export function computeIslandsHash(input) {
76
+ const islands = input.islands
77
+ .map((island) => ({ id: island.id, kind: island.kind, module: sha256Hex(island.module) }))
78
+ .sort((a, b) => byCodeUnit(a.id, b.id));
79
+ const snippets = Object.fromEntries(Object.entries(input.snippetFiles).map(([path, source]) => [path, sha256Hex(source)]));
80
+ return sha256Hex(stableStringify({
81
+ algorithm: ISLANDS_HASH_ALGORITHM,
82
+ islands,
83
+ snippets,
84
+ provider: input.provider ?? null,
85
+ css: input.css ?? [],
86
+ ssr: input.ssr ?? true,
87
+ isolation: input.isolation ?? 'page',
88
+ }));
89
+ }
90
+ export function computeBuildId(depsHash, islandsHash) {
91
+ return sha256Hex(depsHash + islandsHash).slice(0, 32);
92
+ }
93
+ export function islandIdFor(moduleSource) {
94
+ return `i${sha256Hex(moduleSource).slice(0, 16)}`;
95
+ }
96
+ export function islandPrefix(id) {
97
+ return `mi-${id}-`;
98
+ }
@@ -0,0 +1,12 @@
1
+ export { build, MAX_CLIENT_GZIP_BYTES, MAX_SERVER_BUNDLE_BYTES } from './build.js';
2
+ export { bundle, ISLAND_ID, ISLANDS_HOOKS_SPECIFIER, type BundleOptions, type BundleResult, } from './bundle.js';
3
+ export { computeBuildId, computeDepsHash, computeIslandsHash, DEPS_HASH_ALGORITHM, ISLANDS_HASH_ALGORITHM, islandIdFor, islandPrefix, sha256Hex, sha384Integrity, stableStringify, type DepsHashInput, type HashLockfileKind, type IslandsHashInput, } from './hashing.js';
4
+ export { generateNpmrc, install, type InstallOptions, type InstallResult } from './install.js';
5
+ export { collectLocalFiles, type LocalFiles, type LocalFilesInput } from './local-files.js';
6
+ export { findLockfile, LOCKFILE_NAMES, registryFor, validateLockfile, type FoundLockfile, type LockfileName, type LockfileValidation, } from './lockfile.js';
7
+ export { checkDuplicateReact, checkPeerRanges, checkReactVersions, packageOfPath, readReactInfo, scanRemovedApis, singleReactPlugin, type ReactInfo, } from './react.js';
8
+ export { ISLAND_RENDER_TIMEOUT_MS, renderIslands, type RenderOutcome } from './render.js';
9
+ export { MAX_ISLAND_HTML_BYTES, sanitizeHtml, sanitizeIslandHtml } from './sanitize.js';
10
+ export { satisfies } from './semver.js';
11
+ export { BUILDER_VERSION } from './version.js';
12
+ export type * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { build, MAX_CLIENT_GZIP_BYTES, MAX_SERVER_BUNDLE_BYTES } from './build.js';
2
+ export { bundle, ISLAND_ID, ISLANDS_HOOKS_SPECIFIER, } from './bundle.js';
3
+ export { computeBuildId, computeDepsHash, computeIslandsHash, DEPS_HASH_ALGORITHM, ISLANDS_HASH_ALGORITHM, islandIdFor, islandPrefix, sha256Hex, sha384Integrity, stableStringify, } from './hashing.js';
4
+ export { generateNpmrc, install } from './install.js';
5
+ export { collectLocalFiles } from './local-files.js';
6
+ export { findLockfile, LOCKFILE_NAMES, registryFor, validateLockfile, } from './lockfile.js';
7
+ export { checkDuplicateReact, checkPeerRanges, checkReactVersions, packageOfPath, readReactInfo, scanRemovedApis, singleReactPlugin, } from './react.js';
8
+ export { ISLAND_RENDER_TIMEOUT_MS, renderIslands } from './render.js';
9
+ export { MAX_ISLAND_HTML_BYTES, sanitizeHtml, sanitizeIslandHtml } from './sanitize.js';
10
+ export { satisfies } from './semver.js';
11
+ export { BUILDER_VERSION } from './version.js';
@@ -0,0 +1,34 @@
1
+ import { type FoundLockfile } from './lockfile.js';
2
+ import { type PackageJson } from './package-json.js';
3
+ import { type PackageManagerCommand, type SpawnHost } from './package-manager.js';
4
+ import type { BuildDiagnostic, Installer, PackageManagerBinaries } from './types.js';
5
+ export declare const INSTALL_TIMEOUT_MS: number;
6
+ export type InstallResult = {
7
+ ok: boolean;
8
+ installRoot: string;
9
+ durationMs: number;
10
+ usedLegacyPeerDeps: boolean;
11
+ diagnostics: BuildDiagnostic[];
12
+ cleanup(): Promise<void>;
13
+ };
14
+ export type InstallOptions = {
15
+ docsRoot: string;
16
+ installer: Installer;
17
+ lockfile?: FoundLockfile;
18
+ packageJson?: PackageJson;
19
+ registries?: Record<string, string>;
20
+ registryTokens?: Record<string, string>;
21
+ packageManagers?: PackageManagerBinaries;
22
+ expectedVersions?: Record<string, string>;
23
+ timeoutMs?: number;
24
+ host?: SpawnHost;
25
+ };
26
+ export declare function pnpmCommand(packageJson: PackageJson, binaries: Record<string, string> | undefined, host: SpawnHost | undefined): {
27
+ command: PackageManagerCommand;
28
+ diagnostic?: BuildDiagnostic;
29
+ };
30
+ export declare function generateNpmrc(registries: Record<string, string> | undefined, tokens: Record<string, string> | undefined): {
31
+ npmrc: string;
32
+ diagnostics: BuildDiagnostic[];
33
+ };
34
+ export declare function install(opts: InstallOptions): Promise<InstallResult>;
@@ -0,0 +1,325 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { basename, join } from 'node:path';
6
+ import { performance } from 'node:perf_hooks';
7
+ import { lockfileKind, NPMRC_COMMENT_OR_ESCAPE, NPMRC_COMMENT_OR_ESCAPE_PROBLEM, registryProblem, } from './lockfile.js';
8
+ import { cleanPackageJson } from './package-json.js';
9
+ import { resolvePackageManager, } from './package-manager.js';
10
+ export const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
11
+ const PASSTHROUGH_ENV = [
12
+ 'PATH',
13
+ 'HOME',
14
+ 'USERPROFILE',
15
+ 'APPDATA',
16
+ 'LOCALAPPDATA',
17
+ 'TMPDIR',
18
+ 'TMP',
19
+ 'TEMP',
20
+ 'LANG',
21
+ 'SystemRoot',
22
+ 'HTTPS_PROXY',
23
+ 'HTTP_PROXY',
24
+ 'NO_PROXY',
25
+ 'https_proxy',
26
+ 'http_proxy',
27
+ 'no_proxy',
28
+ 'NODE_EXTRA_CA_CERTS',
29
+ 'SSL_CERT_FILE',
30
+ 'npm_config_cache',
31
+ 'NPM_CONFIG_CACHE',
32
+ ];
33
+ function runCommand(cmd, args, cwd, env, timeoutMs) {
34
+ return new Promise((resolvePromise) => {
35
+ const child = spawn(cmd, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
36
+ let output = '';
37
+ const append = (chunk) => {
38
+ output = (output + chunk.toString()).slice(-64_000);
39
+ };
40
+ child.stdout.on('data', append);
41
+ child.stderr.on('data', append);
42
+ let timedOut = false;
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ child.kill('SIGKILL');
46
+ }, timeoutMs);
47
+ child.on('error', (err) => {
48
+ clearTimeout(timer);
49
+ resolvePromise({ code: -1, output: `${output}\n${err.message}`, timedOut });
50
+ });
51
+ child.on('close', (code) => {
52
+ clearTimeout(timer);
53
+ resolvePromise({ code, output, timedOut });
54
+ });
55
+ });
56
+ }
57
+ function registryHostAndPath(url) {
58
+ const u = new URL(url);
59
+ const path = u.pathname.endsWith('/') ? u.pathname : `${u.pathname}/`;
60
+ return `${u.host}${path}`;
61
+ }
62
+ function tokenRegistry(url) {
63
+ try {
64
+ if (new URL(url).protocol !== 'https:')
65
+ return undefined;
66
+ return registryHostAndPath(url);
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ const PNPM_VERSION = /^pnpm@(\d+)\./;
73
+ export function pnpmCommand(packageJson, binaries, host) {
74
+ const fallback = resolvePackageManager('pnpm', host);
75
+ if (!binaries || Object.keys(binaries).length === 0)
76
+ return { command: fallback };
77
+ const requested = typeof packageJson.packageManager === 'string' ? packageJson.packageManager : undefined;
78
+ const major = requested ? PNPM_VERSION.exec(requested)?.[1] : undefined;
79
+ const path = major ? binaries[major] : undefined;
80
+ if (path) {
81
+ return {
82
+ command: /\.[cm]?js$/.test(path)
83
+ ? { ok: true, command: process.execPath, args: [path] }
84
+ : { ok: true, command: path, args: [] },
85
+ };
86
+ }
87
+ if (!requested)
88
+ return { command: fallback };
89
+ const supported = Object.keys(binaries)
90
+ .sort((a, b) => Number(a) - Number(b))
91
+ .join(', ');
92
+ return {
93
+ command: fallback,
94
+ diagnostic: {
95
+ level: 'warning',
96
+ code: 'package-manager-version-unsupported',
97
+ message: `package.json asks for ${requested}; builds use pnpm ${supported}, so the default pnpm was used.`,
98
+ },
99
+ };
100
+ }
101
+ const INVALID_HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
102
+ const NPMRC_STRIPPED_EDGE = /^[\s"']|[\s"']$/;
103
+ function registryTokenProblem(token) {
104
+ if (/[\r\n]/.test(token))
105
+ return 'a newline';
106
+ if (INVALID_HEADER_VALUE.test(token))
107
+ return 'a character HTTP headers cannot carry';
108
+ if (token.includes('${'))
109
+ return '${, which npm expands from environment variables';
110
+ if (NPMRC_STRIPPED_EDGE.test(token)) {
111
+ return 'quotes or whitespace at either end, which npm strips and unescapes in .npmrc';
112
+ }
113
+ if (NPMRC_COMMENT_OR_ESCAPE.test(token))
114
+ return NPMRC_COMMENT_OR_ESCAPE_PROBLEM;
115
+ return undefined;
116
+ }
117
+ export function generateNpmrc(registries, tokens) {
118
+ const diagnostics = [];
119
+ const lines = [
120
+ 'registry=https://registry.npmjs.org/',
121
+ 'ignore-scripts=true',
122
+ 'audit=false',
123
+ 'fund=false',
124
+ 'update-notifier=false',
125
+ ];
126
+ const valid = {};
127
+ for (const [scope, url] of Object.entries(registries ?? {}).sort()) {
128
+ const problem = registryProblem(scope, url);
129
+ if (problem) {
130
+ diagnostics.push({ level: 'error', code: 'registry-invalid', message: problem });
131
+ continue;
132
+ }
133
+ valid[scope] = registryHostAndPath(url);
134
+ lines.push(`${scope}:registry=https://${valid[scope]}`);
135
+ }
136
+ const scopesByRegistry = new Map();
137
+ for (const [scope, hostAndPath] of Object.entries(valid)) {
138
+ scopesByRegistry.set(hostAndPath, [...(scopesByRegistry.get(hostAndPath) ?? []), scope]);
139
+ }
140
+ for (const [registry, token] of Object.entries(tokens ?? {}).sort()) {
141
+ const hostAndPath = tokenRegistry(registry);
142
+ const scopes = hostAndPath === undefined ? undefined : scopesByRegistry.get(hostAndPath);
143
+ if (!scopes) {
144
+ if (Object.values(registries ?? {}).includes(registry))
145
+ continue;
146
+ diagnostics.push({
147
+ level: 'warning',
148
+ code: 'registry-token-unused',
149
+ message: `A registry token was provided for ${registry}, but no scope in packages.registries uses that registry, so it was not sent.`,
150
+ });
151
+ continue;
152
+ }
153
+ const problem = registryTokenProblem(token);
154
+ if (problem) {
155
+ diagnostics.push({
156
+ level: 'error',
157
+ code: 'registry-token-invalid',
158
+ message: `The token for ${scopes.join(', ')} contains ${problem}.`,
159
+ });
160
+ continue;
161
+ }
162
+ lines.push(`//${hostAndPath}:_authToken=${token}`);
163
+ }
164
+ return { npmrc: `${lines.join('\n')}\n`, diagnostics };
165
+ }
166
+ function localDrift(installRoot, expected, level) {
167
+ const out = [];
168
+ for (const [name, version] of Object.entries(expected)) {
169
+ const pkgPath = join(installRoot, 'node_modules', name, 'package.json');
170
+ let installed;
171
+ try {
172
+ installed = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
173
+ }
174
+ catch {
175
+ installed = undefined;
176
+ }
177
+ if (installed !== version) {
178
+ out.push({
179
+ level,
180
+ code: 'local-node-modules-drift',
181
+ message: `node_modules has ${name} ${installed ?? '(missing)'} but the lockfile has ${version}. Run npm install.`,
182
+ pkg: name,
183
+ });
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ async function installExisting(opts) {
189
+ const start = performance.now();
190
+ const installRoot = await realpath(opts.docsRoot);
191
+ const diagnostics = [];
192
+ const prepared = opts.installer === 'prepared';
193
+ if (!existsSync(join(installRoot, 'node_modules'))) {
194
+ diagnostics.push({
195
+ level: 'error',
196
+ code: 'install-missing-node-modules',
197
+ message: prepared
198
+ ? `No node_modules in ${installRoot}. Install packages before building.`
199
+ : `No node_modules in ${installRoot}. Run npm install there first.`,
200
+ });
201
+ }
202
+ else if (opts.expectedVersions) {
203
+ diagnostics.push(...localDrift(installRoot, opts.expectedVersions, prepared ? 'error' : 'warning'));
204
+ }
205
+ return {
206
+ ok: diagnostics.every((d) => d.level !== 'error'),
207
+ installRoot,
208
+ durationMs: performance.now() - start,
209
+ usedLegacyPeerDeps: false,
210
+ diagnostics,
211
+ cleanup: async () => { },
212
+ };
213
+ }
214
+ function failureTail(output) {
215
+ const lines = output
216
+ .split('\n')
217
+ .map((l) => l.trim())
218
+ .filter((l) => l && !/^npm (warn|notice)/i.test(l));
219
+ return lines.slice(-12).join('\n');
220
+ }
221
+ async function installClean(opts) {
222
+ const start = performance.now();
223
+ const diagnostics = [];
224
+ const dir = await realpath(await mkdtemp(join(tmpdir(), 'mint-pkg-install-')));
225
+ const cleanup = () => rm(dir, { recursive: true, force: true });
226
+ const fail = (code, message) => {
227
+ diagnostics.push({ level: 'error', code, message });
228
+ return {
229
+ ok: false,
230
+ installRoot: dir,
231
+ durationMs: performance.now() - start,
232
+ usedLegacyPeerDeps: false,
233
+ diagnostics,
234
+ cleanup,
235
+ };
236
+ };
237
+ if (!opts.lockfile)
238
+ return fail('lockfile-missing', 'A lockfile is required for a clean install.');
239
+ if (!opts.packageJson)
240
+ return fail('package-json-missing', 'A package.json is required.');
241
+ await writeFile(join(dir, 'package.json'), cleanPackageJson(opts.packageJson));
242
+ await writeFile(join(dir, basename(opts.lockfile.path)), opts.lockfile.content);
243
+ const { npmrc, diagnostics: npmrcDiagnostics } = generateNpmrc(opts.registries, opts.registryTokens);
244
+ diagnostics.push(...npmrcDiagnostics);
245
+ if (npmrcDiagnostics.some((d) => d.level === 'error')) {
246
+ return {
247
+ ok: false,
248
+ installRoot: dir,
249
+ durationMs: performance.now() - start,
250
+ usedLegacyPeerDeps: false,
251
+ diagnostics,
252
+ cleanup,
253
+ };
254
+ }
255
+ const kind = lockfileKind(opts.lockfile.fileName);
256
+ const pm = kind === 'npm' ? 'npm' : 'pnpm';
257
+ let resolved;
258
+ if (kind === 'npm') {
259
+ resolved = resolvePackageManager('npm', opts.host);
260
+ }
261
+ else {
262
+ const chosen = pnpmCommand(opts.packageJson, opts.packageManagers?.pnpm, opts.host);
263
+ resolved = chosen.command;
264
+ if (chosen.diagnostic)
265
+ diagnostics.push(chosen.diagnostic);
266
+ }
267
+ if (!resolved.ok)
268
+ return fail('package-manager-missing', resolved.message);
269
+ const npmrcPath = join(dir, '.npmrc');
270
+ await writeFile(npmrcPath, npmrc, { mode: 0o600 });
271
+ const env = {};
272
+ for (const key of PASSTHROUGH_ENV)
273
+ if (process.env[key] !== undefined)
274
+ env[key] = process.env[key];
275
+ env.npm_config_userconfig = join(dir, '.npmrc-user-empty');
276
+ env.npm_config_globalconfig = join(dir, '.npmrc-global-empty');
277
+ env.NODE_ENV = 'production';
278
+ env.COREPACK_ENABLE_STRICT = '0';
279
+ env.COREPACK_ENABLE_AUTO_PIN = '0';
280
+ env.npm_config_manage_package_manager_versions = 'false';
281
+ const timeoutMs = opts.timeoutMs ?? INSTALL_TIMEOUT_MS;
282
+ const baseArgs = kind === 'npm'
283
+ ? ['ci', '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund']
284
+ : [
285
+ 'install',
286
+ '--frozen-lockfile',
287
+ '--ignore-scripts',
288
+ '--ignore-pnpmfile',
289
+ '--prod',
290
+ '--config.package-import-method=copy',
291
+ ];
292
+ const run = (args) => runCommand(resolved.command, [...resolved.args, ...args], dir, env, timeoutMs);
293
+ let usedLegacyPeerDeps = false;
294
+ let result = await run(baseArgs);
295
+ if (result.code !== 0 && kind === 'npm' && /ERESOLVE/.test(result.output)) {
296
+ usedLegacyPeerDeps = true;
297
+ diagnostics.push({
298
+ level: 'warning',
299
+ code: 'npm-legacy-peer-deps',
300
+ message: 'npm ci failed with ERESOLVE (a peer range excludes an installed version); retried with --legacy-peer-deps.',
301
+ });
302
+ result = await run([...baseArgs, '--legacy-peer-deps']);
303
+ }
304
+ await rm(npmrcPath, { force: true });
305
+ if (result.code !== 0) {
306
+ const reason = result.timedOut
307
+ ? `timed out after ${Math.round(timeoutMs / 1000)} s`
308
+ : `exited with code ${String(result.code)}`;
309
+ return {
310
+ ...fail('install-failed', `${pm} ${baseArgs[0]} ${reason}:\n${failureTail(result.output)}`),
311
+ usedLegacyPeerDeps,
312
+ };
313
+ }
314
+ return {
315
+ ok: true,
316
+ installRoot: dir,
317
+ durationMs: performance.now() - start,
318
+ usedLegacyPeerDeps,
319
+ diagnostics,
320
+ cleanup,
321
+ };
322
+ }
323
+ export function install(opts) {
324
+ return opts.installer === 'npm-ci' ? installClean(opts) : installExisting(opts);
325
+ }