@mintlify/package-builder 0.0.0-stage → 0.0.2

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 (45) hide show
  1. package/README.md +66 -2
  2. package/dist/build.d.ts +4 -0
  3. package/dist/build.js +338 -0
  4. package/dist/bundle.d.ts +44 -0
  5. package/dist/bundle.js +681 -0
  6. package/dist/cli.d.ts +13 -0
  7. package/dist/cli.js +112 -0
  8. package/dist/diagnostics.d.ts +11 -0
  9. package/dist/diagnostics.js +32 -0
  10. package/dist/hashing.d.ts +28 -0
  11. package/dist/hashing.js +98 -0
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.js +11 -0
  14. package/dist/install.d.ts +34 -0
  15. package/dist/install.js +325 -0
  16. package/dist/local-files.d.ts +11 -0
  17. package/dist/local-files.js +120 -0
  18. package/dist/lockfile.d.ts +40 -0
  19. package/dist/lockfile.js +498 -0
  20. package/dist/package-json.d.ts +14 -0
  21. package/dist/package-json.js +170 -0
  22. package/dist/package-manager.d.ts +17 -0
  23. package/dist/package-manager.js +87 -0
  24. package/dist/react.d.ts +37 -0
  25. package/dist/react.js +264 -0
  26. package/dist/render.d.ts +37 -0
  27. package/dist/render.js +292 -0
  28. package/dist/runtime/client.d.ts +11 -0
  29. package/dist/runtime/client.js +54 -0
  30. package/dist/runtime/server.d.ts +6 -0
  31. package/dist/runtime/server.js +11 -0
  32. package/dist/runtime/shell.d.ts +21 -0
  33. package/dist/runtime/shell.js +68 -0
  34. package/dist/runtime/theme.d.ts +1 -0
  35. package/dist/runtime/theme.js +1 -0
  36. package/dist/sanitize.d.ts +9 -0
  37. package/dist/sanitize.js +650 -0
  38. package/dist/semver.d.ts +2 -0
  39. package/dist/semver.js +10 -0
  40. package/dist/tsconfig.build.tsbuildinfo +1 -0
  41. package/dist/types.d.ts +104 -0
  42. package/dist/types.js +1 -0
  43. package/dist/version.d.ts +1 -0
  44. package/dist/version.js +11 -0
  45. package/package.json +71 -4
@@ -0,0 +1,11 @@
1
+ import type { IslandSpec } from './types.js';
2
+ export type LocalFiles = Record<string, string | Uint8Array>;
3
+ export type LocalFilesInput = {
4
+ docsRoot: string;
5
+ islands: Pick<IslandSpec, 'module'>[];
6
+ snippetFiles: Record<string, string>;
7
+ provider?: string;
8
+ rejectHardLinks?: boolean;
9
+ };
10
+ export declare function runtimeImports(files: LocalFiles, islands: Pick<IslandSpec, 'id' | 'pagePath' | 'module'>[]): Promise<string[]>;
11
+ export declare function collectLocalFiles(input: LocalFilesInput): LocalFiles;
@@ -0,0 +1,120 @@
1
+ import { transform } from 'esbuild';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
4
+ import { join, posix } from 'node:path';
5
+ import { docsPathToRelative, inside, URL_LIKE } from './bundle.js';
6
+ const SCRIPT = /\.[cm]?[jt]sx?$/;
7
+ const SCRIPT_SPECIFIER = /\b(?:from|import)\s*\(?\s*(['"])([^'"\n]+)\1|\brequire\s*\(\s*(['"])([^'"\n]+)\3/g;
8
+ const CSS_SPECIFIER = /@import\s+(?:url\(\s*)?(['"]?)([^'")\s;]+)\1|\burl\(\s*(['"]?)([^'")\s]+)\3\s*\)/g;
9
+ const LOCAL_SCRIPT_SPECIFIER = /^\.{0,2}\/(?!\/)/;
10
+ const RUNTIME_PATH_CALL = /_(import|require)\(\s*(?:\/\*[\s\S]*?\*\/\s*)*(`\.{0,2}\/(?!\/)[^`]*?\$\{[^`]*`|(?<q>[`'"])\.{0,2}\/(?!\/)[^`'"\n]*\k<q>\s*\+[^)\n]*)/g;
11
+ const EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.css', '.json'];
12
+ const SCRIPT_CANDIDATES = ['', ...EXTENSIONS, ...EXTENSIONS.map((ext) => `/index${ext}`)];
13
+ function specifiers(source, pattern) {
14
+ return [...source.matchAll(pattern)].flatMap((m) => m[2] ?? m[4] ?? []);
15
+ }
16
+ function text(content) {
17
+ return typeof content === 'string' ? content : new TextDecoder().decode(content);
18
+ }
19
+ function loaderFor(path) {
20
+ return path.endsWith('tsx') ? 'tsx' : path.endsWith('ts') ? 'ts' : 'jsx';
21
+ }
22
+ export async function runtimeImports(files, islands) {
23
+ const sources = [
24
+ ...Object.entries(files)
25
+ .filter(([path]) => SCRIPT.test(path))
26
+ .map(([path, content]) => ({ where: path, loader: loaderFor(path), source: text(content) })),
27
+ ...islands.map((i) => ({
28
+ where: `island ${i.id} (${i.pagePath})`,
29
+ loader: 'jsx',
30
+ source: i.module,
31
+ })),
32
+ ];
33
+ const mark = `__mint${randomBytes(8).toString('hex')}`;
34
+ const calls = new RegExp(mark + RUNTIME_PATH_CALL.source, 'g');
35
+ const found = await Promise.all(sources.map(async ({ where, loader, source }) => {
36
+ const code = await transform(source, {
37
+ loader,
38
+ supported: { 'dynamic-import': false },
39
+ define: { require: `${mark}_require` },
40
+ })
41
+ .then((r) => transform(r.code, { loader: 'js', define: { require: `${mark}_import` } }))
42
+ .then((r) => r.code, () => '');
43
+ return [...code.matchAll(calls)].map((m) => `${where}: ${m[1]}(${m[2].replace(/\s+/g, ' ')}) builds its path at runtime; import each file statically.`);
44
+ }));
45
+ return found.flat();
46
+ }
47
+ export function collectLocalFiles(input) {
48
+ const files = { ...input.snippetFiles };
49
+ let root;
50
+ try {
51
+ root = realpathSync(input.docsRoot);
52
+ }
53
+ catch {
54
+ root = undefined;
55
+ }
56
+ const read = (rel) => {
57
+ if (!root || rel.split('/').includes('node_modules'))
58
+ return undefined;
59
+ try {
60
+ const real = realpathSync(join(root, rel));
61
+ const stat = statSync(real);
62
+ const linked = input.rejectHardLinks === true && stat.nlink > 1;
63
+ return inside(root, real) && stat.isFile() && !linked
64
+ ? new Uint8Array(readFileSync(real))
65
+ : undefined;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ };
71
+ const locate = (specifier, importer) => {
72
+ if (specifier.startsWith('/'))
73
+ return docsPathToRelative(specifier.replace(/\/+$/, ''));
74
+ const rel = posix.normalize(posix.join(posix.dirname(importer), specifier)).replace(/\/+$/, '');
75
+ return rel === '.' || rel === '..' || rel.startsWith('../') ? undefined : rel;
76
+ };
77
+ const follow = (specifier, importer, candidates) => {
78
+ for (const spec of new Set([specifier, specifier.replace(/[?#].*$/, '')])) {
79
+ const rel = locate(spec, importer);
80
+ if (rel === undefined)
81
+ continue;
82
+ for (const suffix of candidates) {
83
+ const path = `/${rel}${suffix}`;
84
+ if (files[path] !== undefined)
85
+ return path;
86
+ const data = read(`${rel}${suffix}`);
87
+ if (data) {
88
+ files[path] = data;
89
+ return path;
90
+ }
91
+ }
92
+ }
93
+ return undefined;
94
+ };
95
+ const roots = [
96
+ ...(input.provider ? [input.provider] : []),
97
+ ...input.islands.flatMap((i) => specifiers(i.module, SCRIPT_SPECIFIER).filter((s) => /^\/(?!\/)/.test(s))),
98
+ ].flatMap((specifier) => follow(specifier, '', SCRIPT_CANDIDATES) ?? []);
99
+ const stack = [...Object.keys(input.snippetFiles), ...roots];
100
+ const seen = new Set();
101
+ while (stack.length > 0) {
102
+ const path = stack.pop();
103
+ const rel = docsPathToRelative(path);
104
+ const content = files[path];
105
+ if (seen.has(path) || rel === undefined || content === undefined)
106
+ continue;
107
+ seen.add(path);
108
+ const css = rel.endsWith('.css');
109
+ if (!css && !SCRIPT.test(rel))
110
+ continue;
111
+ for (const specifier of specifiers(text(content), css ? CSS_SPECIFIER : SCRIPT_SPECIFIER)) {
112
+ if (css ? URL_LIKE.test(specifier) : !LOCAL_SCRIPT_SPECIFIER.test(specifier))
113
+ continue;
114
+ const found = follow(specifier, rel, css ? [''] : SCRIPT_CANDIDATES);
115
+ if (found)
116
+ stack.push(found);
117
+ }
118
+ }
119
+ return files;
120
+ }
@@ -0,0 +1,40 @@
1
+ import { type PackageJson } from './package-json.js';
2
+ import type { BuildDiagnostic } from './types.js';
3
+ export declare const NPM_REGISTRY = "https://registry.npmjs.org/";
4
+ export declare const LOCKFILE_NAMES: readonly ["package-lock.json", "pnpm-lock.yaml"];
5
+ export type LockfileName = (typeof LOCKFILE_NAMES)[number];
6
+ export type LockfileKind = 'npm' | 'pnpm';
7
+ export type LockfileValidation = {
8
+ ok: boolean;
9
+ kind: LockfileKind;
10
+ diagnostics: BuildDiagnostic[];
11
+ topLevel: Record<string, string>;
12
+ entryCount: number;
13
+ };
14
+ export type FoundLockfile = {
15
+ fileName: LockfileName;
16
+ path: string;
17
+ content: string;
18
+ };
19
+ export declare function lockfileKind(fileName: LockfileName): LockfileKind;
20
+ export declare function findLockfile(dir: string, { allowSymlink }?: {
21
+ allowSymlink: boolean;
22
+ }): {
23
+ lockfile?: FoundLockfile;
24
+ diagnostics: BuildDiagnostic[];
25
+ };
26
+ export declare const NPMRC_COMMENT_OR_ESCAPE: RegExp;
27
+ export declare const NPMRC_COMMENT_OR_ESCAPE_PROBLEM = "a ;, # or \\, which npm reads as a comment or escape in .npmrc";
28
+ export declare function registryProblem(scope: string, url: string): string | undefined;
29
+ export declare function validateRegistries(registries: Record<string, string> | undefined): BuildDiagnostic[];
30
+ export declare function registryFor(name: string, registries: Record<string, string> | undefined): string;
31
+ export declare function parsePnpmKey(key: string): {
32
+ name: string;
33
+ version: string;
34
+ };
35
+ export declare function validateLockfile(input: {
36
+ fileName: LockfileName;
37
+ content: string;
38
+ registries?: Record<string, string>;
39
+ packageJson?: PackageJson;
40
+ }): LockfileValidation;
@@ -0,0 +1,498 @@
1
+ import yaml from 'js-yaml';
2
+ import { existsSync, lstatSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { hasErrors } from './diagnostics.js';
5
+ import { DEPENDENCY_FIELDS, dependencySpecs, jsonErrorPosition, } from './package-json.js';
6
+ export const NPM_REGISTRY = 'https://registry.npmjs.org/';
7
+ export const LOCKFILE_NAMES = ['package-lock.json', 'pnpm-lock.yaml'];
8
+ export function lockfileKind(fileName) {
9
+ return fileName === 'pnpm-lock.yaml' ? 'pnpm' : 'npm';
10
+ }
11
+ export function findLockfile(dir, { allowSymlink } = { allowSymlink: false }) {
12
+ const diagnostics = [];
13
+ const present = LOCKFILE_NAMES.filter((name) => existsSync(join(dir, name)));
14
+ const [first] = present;
15
+ if (first === undefined) {
16
+ const message = existsSync(join(dir, 'yarn.lock'))
17
+ ? 'yarn.lock is not supported. Add a package-lock.json (npm install) or pnpm-lock.yaml next to package.json.'
18
+ : 'A package-lock.json or pnpm-lock.yaml is required next to package.json.';
19
+ diagnostics.push({ level: 'error', code: 'lockfile-missing', message });
20
+ return { diagnostics };
21
+ }
22
+ if (present.length > 1) {
23
+ diagnostics.push({
24
+ level: 'warning',
25
+ code: 'lockfile-multiple',
26
+ message: `Found ${present.join(' and ')}; using ${first}. Delete the one you do not use.`,
27
+ });
28
+ }
29
+ const path = join(dir, first);
30
+ if (!allowSymlink && lstatSync(path).isSymbolicLink()) {
31
+ diagnostics.push({
32
+ level: 'error',
33
+ code: 'lockfile-symlink',
34
+ message: `${first} is a symbolic link. Commit the file itself in the docs folder.`,
35
+ });
36
+ return { diagnostics };
37
+ }
38
+ return { lockfile: { fileName: first, path, content: readFileSync(path, 'utf8') }, diagnostics };
39
+ }
40
+ function classifySource(source) {
41
+ const s = source.trim().toLowerCase();
42
+ if (/^(git\+|git:|github:|gitlab:|bitbucket:|gist:)/.test(s)) {
43
+ return { code: 'lockfile-git-source', what: 'a git source' };
44
+ }
45
+ if (s.startsWith('file:'))
46
+ return { code: 'lockfile-file-source', what: 'a file: source' };
47
+ if (s.startsWith('link:') || s.startsWith('workspace:')) {
48
+ return { code: 'lockfile-link-source', what: 'a link: source' };
49
+ }
50
+ if (s.startsWith('http:'))
51
+ return { code: 'lockfile-insecure-source', what: 'an http: URL' };
52
+ if (s.startsWith('https:'))
53
+ return undefined;
54
+ return { code: 'lockfile-unknown-source', what: `an unsupported source (${source})` };
55
+ }
56
+ function scopeOf(name) {
57
+ if (!name.startsWith('@'))
58
+ return undefined;
59
+ const slash = name.indexOf('/');
60
+ return slash > 0 ? name.slice(0, slash) : undefined;
61
+ }
62
+ function withTrailingSlash(url) {
63
+ return url.endsWith('/') ? url : `${url}/`;
64
+ }
65
+ const SCOPE = /^@[a-z0-9-~][a-z0-9-._~]*$/;
66
+ const PNPM_RESOLUTION_KEYS = new Set(['integrity', 'tarball']);
67
+ const PNPM_IMPORTER_FIELDS = [
68
+ 'dependencies',
69
+ 'optionalDependencies',
70
+ 'devDependencies',
71
+ ];
72
+ function yamlErrorPosition(err) {
73
+ if (!(err instanceof yaml.YAMLException))
74
+ return '';
75
+ const reason = err.reason ? `: ${err.reason}` : '';
76
+ return `${reason} at line ${err.mark.line + 1}, column ${err.mark.column + 1}`;
77
+ }
78
+ export const NPMRC_COMMENT_OR_ESCAPE = /[;#\\]/;
79
+ export const NPMRC_COMMENT_OR_ESCAPE_PROBLEM = 'a ;, # or \\, which npm reads as a comment or escape in .npmrc';
80
+ function npmrcRewrites(value) {
81
+ if (value.includes('${'))
82
+ return '${, which npm expands from environment variables';
83
+ if (NPMRC_COMMENT_OR_ESCAPE.test(value))
84
+ return NPMRC_COMMENT_OR_ESCAPE_PROBLEM;
85
+ return undefined;
86
+ }
87
+ const MAX_REGISTRY_URL_LENGTH = 512;
88
+ const NPMRC_KEY_END = 'which npm reads as the end of the auth token key in .npmrc';
89
+ export function registryProblem(scope, url) {
90
+ if (!SCOPE.test(scope))
91
+ return `${scope} is not an npm scope such as @acme.`;
92
+ if (url.length > MAX_REGISTRY_URL_LENGTH) {
93
+ return `The registry for ${scope} is longer than ${MAX_REGISTRY_URL_LENGTH} characters.`;
94
+ }
95
+ if (/[\s\u0000-\u001f\u007f]/.test(url))
96
+ return `The registry for ${scope} contains whitespace.`;
97
+ const rewritten = npmrcRewrites(url);
98
+ if (rewritten)
99
+ return `The registry for ${scope} contains ${rewritten}.`;
100
+ if (!/^https:\/\/[^/]/.test(url)) {
101
+ return `The registry for ${scope} must start with https:// and then the host.`;
102
+ }
103
+ let parsed;
104
+ try {
105
+ parsed = new URL(url);
106
+ }
107
+ catch {
108
+ return `The registry for ${scope} is not a valid URL: ${url}`;
109
+ }
110
+ if (parsed.username || parsed.password || url.includes('?') || parsed.hash) {
111
+ return `The registry for ${scope} must not contain credentials, a query or a fragment.`;
112
+ }
113
+ const parsedRewritten = npmrcRewrites(parsed.href);
114
+ if (parsedRewritten) {
115
+ return `The registry for ${scope} is read as ${parsed.href} and then contains ${parsedRewritten}.`;
116
+ }
117
+ if (parsed.hostname.includes('=')) {
118
+ return `The registry for ${scope} has an = in the host, ${NPMRC_KEY_END}.`;
119
+ }
120
+ if (parsed.pathname.includes('=')) {
121
+ return `The registry for ${scope} has an = in the path, ${NPMRC_KEY_END}.`;
122
+ }
123
+ return undefined;
124
+ }
125
+ export function validateRegistries(registries) {
126
+ const out = [];
127
+ for (const [scope, url] of Object.entries(registries ?? {})) {
128
+ const problem = registryProblem(scope, typeof url === 'string' ? url : String(url));
129
+ if (problem)
130
+ out.push({ level: 'error', code: 'registry-invalid', message: problem });
131
+ }
132
+ return out;
133
+ }
134
+ export function registryFor(name, registries) {
135
+ const scope = scopeOf(name);
136
+ const scoped = scope !== undefined ? registries?.[scope] : undefined;
137
+ return withTrailingSlash(scoped ?? NPM_REGISTRY);
138
+ }
139
+ function checkHttpsUrl(name, url, registries) {
140
+ let parsed;
141
+ try {
142
+ parsed = new URL(url);
143
+ }
144
+ catch {
145
+ return {
146
+ level: 'error',
147
+ code: 'lockfile-unknown-source',
148
+ message: `${name} has an invalid URL in the lockfile: ${url}`,
149
+ pkg: name,
150
+ };
151
+ }
152
+ if (parsed.username || parsed.password) {
153
+ return {
154
+ level: 'error',
155
+ code: 'lockfile-credentials',
156
+ message: `${name} has credentials embedded in its lockfile URL. Remove them and configure the registry token in the dashboard.`,
157
+ pkg: name,
158
+ };
159
+ }
160
+ const expected = new URL(registryFor(name, registries));
161
+ if (parsed.protocol !== 'https:' ||
162
+ parsed.host !== expected.host ||
163
+ !parsed.pathname.startsWith(expected.pathname)) {
164
+ const scope = scopeOf(name);
165
+ const hint = scope !== undefined && registries?.[scope]
166
+ ? `packages.registries maps ${scope} to ${expected.href}`
167
+ : scope !== undefined
168
+ ? `add "${scope}" to packages.registries in docs.json if it comes from a private registry`
169
+ : 'unscoped packages must come from registry.npmjs.org';
170
+ return {
171
+ level: 'error',
172
+ code: 'lockfile-registry-mismatch',
173
+ message: `${name} resolves to ${parsed.origin}${parsed.pathname}, which is not its registry (${hint}).`,
174
+ pkg: name,
175
+ };
176
+ }
177
+ return undefined;
178
+ }
179
+ function sourceError(name, source, problem) {
180
+ return {
181
+ level: 'error',
182
+ code: problem.code,
183
+ message: `${name} comes from ${problem.what} (${source}). Only registry packages with integrity hashes are supported.`,
184
+ pkg: name,
185
+ };
186
+ }
187
+ function missingIntegrity(name) {
188
+ return {
189
+ level: 'error',
190
+ code: 'lockfile-missing-integrity',
191
+ message: `${name} has no integrity hash in the lockfile. Regenerate it with a current npm or pnpm.`,
192
+ pkg: name,
193
+ };
194
+ }
195
+ function isRecord(value) {
196
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
197
+ }
198
+ function nameFromNpmKey(key) {
199
+ const idx = key.lastIndexOf('node_modules/');
200
+ return idx === -1 ? key : key.slice(idx + 'node_modules/'.length);
201
+ }
202
+ function validateNpm(content, registries) {
203
+ const diagnostics = [];
204
+ const topLevel = {};
205
+ let lock;
206
+ try {
207
+ lock = JSON.parse(content);
208
+ }
209
+ catch (err) {
210
+ diagnostics.push({
211
+ level: 'error',
212
+ code: 'lockfile-invalid',
213
+ message: `package-lock.json is not valid JSON${jsonErrorPosition(err, content)}.`,
214
+ });
215
+ return { diagnostics, topLevel, entryCount: 0 };
216
+ }
217
+ if (!isRecord(lock)) {
218
+ diagnostics.push({
219
+ level: 'error',
220
+ code: 'lockfile-invalid',
221
+ message: 'package-lock.json is not an object.',
222
+ });
223
+ return { diagnostics, topLevel, entryCount: 0 };
224
+ }
225
+ const version = lock.lockfileVersion;
226
+ if ((version !== 2 && version !== 3) || !isRecord(lock.packages)) {
227
+ diagnostics.push({
228
+ level: 'error',
229
+ code: 'lockfile-unsupported',
230
+ message: `package-lock.json lockfileVersion ${String(version)} is not supported. Regenerate it with npm 7 or later (lockfileVersion 2 or 3).`,
231
+ });
232
+ return { diagnostics, topLevel, entryCount: 0 };
233
+ }
234
+ let entryCount = 0;
235
+ for (const [key, raw] of Object.entries(lock.packages)) {
236
+ if (key === '' || !isRecord(raw))
237
+ continue;
238
+ entryCount++;
239
+ const entry = raw;
240
+ const name = typeof entry.name === 'string' ? entry.name : nameFromNpmKey(key);
241
+ if (!key.includes('node_modules/')) {
242
+ diagnostics.push({
243
+ level: 'error',
244
+ code: 'lockfile-link-source',
245
+ message: `${key} is a workspace package. Workspaces are not supported; publish the package to a registry.`,
246
+ pkg: name,
247
+ });
248
+ continue;
249
+ }
250
+ if (entry.inBundle === true) {
251
+ if (entry.resolved !== undefined) {
252
+ diagnostics.push({
253
+ level: 'error',
254
+ code: 'lockfile-bundled-source',
255
+ message: `${name} is marked as bundled in another package but has its own source (${String(entry.resolved)}). Regenerate the lockfile with npm.`,
256
+ pkg: name,
257
+ });
258
+ }
259
+ continue;
260
+ }
261
+ if (entry.link === true) {
262
+ const source = typeof entry.resolved === 'string' ? entry.resolved : 'link';
263
+ diagnostics.push(sourceError(name, source, { code: 'lockfile-link-source', what: 'a linked folder' }));
264
+ continue;
265
+ }
266
+ const isTopLevel = key.startsWith('node_modules/') && key.lastIndexOf('node_modules/') === 0;
267
+ if (isTopLevel && entry.dev !== true && typeof entry.version === 'string') {
268
+ topLevel[nameFromNpmKey(key)] = entry.version;
269
+ }
270
+ if (typeof entry.resolved === 'string') {
271
+ const problem = classifySource(entry.resolved);
272
+ if (problem) {
273
+ diagnostics.push(sourceError(name, entry.resolved, problem));
274
+ continue;
275
+ }
276
+ const hostProblem = checkHttpsUrl(name, entry.resolved, registries);
277
+ if (hostProblem)
278
+ diagnostics.push(hostProblem);
279
+ }
280
+ else if (typeof entry.version === 'string') {
281
+ const problem = classifySource(entry.version);
282
+ if (problem && problem.code !== 'lockfile-unknown-source') {
283
+ diagnostics.push(sourceError(name, entry.version, problem));
284
+ continue;
285
+ }
286
+ }
287
+ if (typeof entry.integrity !== 'string' || entry.integrity.length === 0) {
288
+ diagnostics.push(missingIntegrity(name));
289
+ }
290
+ }
291
+ return { diagnostics, topLevel, entryCount };
292
+ }
293
+ export function parsePnpmKey(key) {
294
+ let k = key.startsWith('/') ? key.slice(1) : key;
295
+ const paren = k.indexOf('(');
296
+ if (paren !== -1)
297
+ k = k.slice(0, paren);
298
+ const at = k.indexOf('@', k.startsWith('@') ? 1 : 0);
299
+ if (at === -1)
300
+ return { name: k, version: '' };
301
+ return { name: k.slice(0, at), version: k.slice(at + 1) };
302
+ }
303
+ function stripPeerSuffix(version) {
304
+ const paren = version.indexOf('(');
305
+ return paren === -1 ? version : version.slice(0, paren);
306
+ }
307
+ function validatePnpm(content, registries) {
308
+ const diagnostics = [];
309
+ const topLevel = {};
310
+ let lock;
311
+ try {
312
+ lock = yaml.load(content, { schema: yaml.CORE_SCHEMA });
313
+ }
314
+ catch (err) {
315
+ diagnostics.push({
316
+ level: 'error',
317
+ code: 'lockfile-invalid',
318
+ message: `pnpm-lock.yaml is not valid YAML${yamlErrorPosition(err)}.`,
319
+ });
320
+ return { diagnostics, topLevel, entryCount: 0 };
321
+ }
322
+ if (!isRecord(lock)) {
323
+ diagnostics.push({
324
+ level: 'error',
325
+ code: 'lockfile-invalid',
326
+ message: 'pnpm-lock.yaml is empty.',
327
+ });
328
+ return { diagnostics, topLevel, entryCount: 0 };
329
+ }
330
+ const lockVersion = parseFloat(String(lock.lockfileVersion));
331
+ if (!(lockVersion >= 6)) {
332
+ diagnostics.push({
333
+ level: 'error',
334
+ code: 'lockfile-unsupported',
335
+ message: `pnpm-lock.yaml lockfileVersion ${String(lock.lockfileVersion)} is not supported. Regenerate it with pnpm 8 or later.`,
336
+ });
337
+ return { diagnostics, topLevel, entryCount: 0 };
338
+ }
339
+ const importers = isRecord(lock.importers) ? lock.importers : { '.': lock };
340
+ for (const [importerPath, importer] of Object.entries(importers)) {
341
+ if (!isRecord(importer))
342
+ continue;
343
+ if (importerPath !== '.') {
344
+ diagnostics.push({
345
+ level: 'error',
346
+ code: 'lockfile-link-source',
347
+ message: `pnpm workspace importer ${importerPath} is not supported; only the docs folder's package.json is installed.`,
348
+ });
349
+ continue;
350
+ }
351
+ for (const field of ['dependencies', 'optionalDependencies', 'devDependencies']) {
352
+ const deps = importer[field];
353
+ if (!isRecord(deps))
354
+ continue;
355
+ for (const [name, spec] of Object.entries(deps)) {
356
+ const version = isRecord(spec) ? spec.version : spec;
357
+ if (typeof version !== 'string')
358
+ continue;
359
+ const problem = /^(link:|file:|git|github:|http)/.test(version)
360
+ ? classifySource(version)
361
+ : undefined;
362
+ if (problem) {
363
+ diagnostics.push(sourceError(name, version, problem));
364
+ continue;
365
+ }
366
+ if (field !== 'devDependencies')
367
+ topLevel[name] = stripPeerSuffix(version);
368
+ }
369
+ }
370
+ }
371
+ let entryCount = 0;
372
+ const packages = isRecord(lock.packages) ? lock.packages : {};
373
+ for (const [key, raw] of Object.entries(packages)) {
374
+ entryCount++;
375
+ const { name, version } = parsePnpmKey(key);
376
+ const entry = isRecord(raw) ? raw : {};
377
+ const resolution = isRecord(entry.resolution) ? entry.resolution : {};
378
+ const versionProblem = /^[a-z+]+:/i.test(version) ? classifySource(version) : undefined;
379
+ if (versionProblem) {
380
+ diagnostics.push(sourceError(name, version, versionProblem));
381
+ continue;
382
+ }
383
+ if (resolution.type === 'git' ||
384
+ typeof resolution.repo === 'string' ||
385
+ typeof resolution.commit === 'string') {
386
+ diagnostics.push(sourceError(name, String(resolution.repo ?? version), {
387
+ code: 'lockfile-git-source',
388
+ what: 'a git source',
389
+ }));
390
+ continue;
391
+ }
392
+ if (resolution.type === 'directory' || typeof resolution.directory === 'string') {
393
+ diagnostics.push(sourceError(name, String(resolution.directory ?? version), {
394
+ code: 'lockfile-file-source',
395
+ what: 'a local folder',
396
+ }));
397
+ continue;
398
+ }
399
+ const unexpected = Object.keys(resolution).filter((k) => !PNPM_RESOLUTION_KEYS.has(k));
400
+ if (unexpected.length > 0) {
401
+ diagnostics.push({
402
+ level: 'error',
403
+ code: 'lockfile-resolution-unsupported',
404
+ message: `${name} has an unsupported resolution in pnpm-lock.yaml (${unexpected.join(', ')}). Only registry packages with integrity hashes are supported.`,
405
+ pkg: name,
406
+ });
407
+ continue;
408
+ }
409
+ if (typeof resolution.tarball === 'string') {
410
+ const problem = classifySource(resolution.tarball);
411
+ if (problem) {
412
+ diagnostics.push(sourceError(name, resolution.tarball, problem));
413
+ continue;
414
+ }
415
+ const hostProblem = checkHttpsUrl(name, resolution.tarball, registries);
416
+ if (hostProblem)
417
+ diagnostics.push(hostProblem);
418
+ }
419
+ if (typeof resolution.integrity !== 'string' || resolution.integrity.length === 0) {
420
+ diagnostics.push(missingIntegrity(name));
421
+ }
422
+ }
423
+ return { diagnostics, topLevel, entryCount };
424
+ }
425
+ function npmRootSpecs(content) {
426
+ let lock;
427
+ try {
428
+ lock = JSON.parse(content);
429
+ }
430
+ catch {
431
+ return undefined;
432
+ }
433
+ if (!isRecord(lock) || !isRecord(lock.packages) || !isRecord(lock.packages['']))
434
+ return undefined;
435
+ const root = lock.packages[''];
436
+ return Object.fromEntries(DEPENDENCY_FIELDS.map((f) => [f, dependencySpecs(root, f)]));
437
+ }
438
+ function pnpmRootSpecs(content) {
439
+ let lock;
440
+ try {
441
+ lock = yaml.load(content, { schema: yaml.CORE_SCHEMA });
442
+ }
443
+ catch {
444
+ return undefined;
445
+ }
446
+ if (!isRecord(lock))
447
+ return undefined;
448
+ const importer = isRecord(lock.importers) ? lock.importers['.'] : lock;
449
+ if (!isRecord(importer))
450
+ return {};
451
+ const out = {};
452
+ for (const field of PNPM_IMPORTER_FIELDS) {
453
+ const deps = importer[field];
454
+ out[field] = {};
455
+ if (!isRecord(deps))
456
+ continue;
457
+ for (const [name, spec] of Object.entries(deps)) {
458
+ if (isRecord(spec) && typeof spec.specifier === 'string')
459
+ out[field][name] = spec.specifier;
460
+ }
461
+ }
462
+ return out;
463
+ }
464
+ function rootSyncProblems(kind, fileName, content, packageJson) {
465
+ const lockSpecs = kind === 'npm' ? npmRootSpecs(content) : pnpmRootSpecs(content);
466
+ if (!lockSpecs)
467
+ return [];
468
+ const fields = kind === 'npm' ? DEPENDENCY_FIELDS : PNPM_IMPORTER_FIELDS;
469
+ const differing = [];
470
+ for (const field of fields) {
471
+ const wanted = dependencySpecs(packageJson, field);
472
+ const locked = lockSpecs[field] ?? {};
473
+ for (const name of new Set([...Object.keys(wanted), ...Object.keys(locked)])) {
474
+ if (wanted[name] !== locked[name])
475
+ differing.push(`${field} ${name}`);
476
+ }
477
+ }
478
+ if (differing.length === 0)
479
+ return [];
480
+ const command = kind === 'npm' ? 'npm install' : 'pnpm install';
481
+ return [
482
+ {
483
+ level: 'error',
484
+ code: 'lockfile-out-of-sync',
485
+ message: `${fileName} does not match package.json (${differing.slice(0, 5).join(', ')}${differing.length > 5 ? ', …' : ''}). Run ${command} and commit the lockfile.`,
486
+ },
487
+ ];
488
+ }
489
+ export function validateLockfile(input) {
490
+ const kind = lockfileKind(input.fileName);
491
+ const result = kind === 'npm'
492
+ ? validateNpm(input.content, input.registries)
493
+ : validatePnpm(input.content, input.registries);
494
+ if (input.packageJson && !hasErrors(result.diagnostics)) {
495
+ result.diagnostics.push(...rootSyncProblems(kind, input.fileName, input.content, input.packageJson));
496
+ }
497
+ return { ok: !hasErrors(result.diagnostics), kind, ...result };
498
+ }
@@ -0,0 +1,14 @@
1
+ import type { BuildDiagnostic } from './types.js';
2
+ export type PackageJson = Record<string, unknown>;
3
+ export declare const DEPENDENCY_FIELDS: readonly string[];
4
+ export declare function jsonErrorPosition(err: unknown, content: string): string;
5
+ export declare function readPackageJson(docsRoot: string, { allowSymlink }: {
6
+ allowSymlink: boolean;
7
+ }): {
8
+ packageJson?: PackageJson;
9
+ diagnostics: BuildDiagnostic[];
10
+ };
11
+ export declare function dependencySpecs(packageJson: PackageJson, field: string): Record<string, string>;
12
+ export declare function specProblem(spec: string): string | undefined;
13
+ export declare function validatePackageJson(packageJson: PackageJson): BuildDiagnostic[];
14
+ export declare function cleanPackageJson(packageJson: PackageJson): string;