@01.works/visual-review 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.
@@ -0,0 +1,1398 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { constants } from "node:fs";
4
+ import { lstat, mkdir, open, opendir, readdir, realpath, rename, unlink, writeFile } from "node:fs/promises";
5
+ const MAX_SOURCE_MAP_ARTIFACT_BYTES = 20 * 1024 * 1024;
6
+ const RELEASES_PATH = "/v1/source-maps/releases";
7
+ const MAX_MANIFEST_BYTES = 128 * 1024;
8
+ const REQUEST_TIMEOUT_MS = 3e4;
9
+ const SAFE_RELEASE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
10
+ const SAFE_ARTIFACT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
11
+ const JAVASCRIPT_OUTPUT$1 = /\.(?:c|m)?js$/i;
12
+ const PUBLIC_SOURCE_MAP_REFERENCE_OUTPUT = /\.(?:(?:c|m)?js|css)$/i;
13
+ /**
14
+ * Uploads Vite browser source maps and always removes every map from the deployment
15
+ * output. Callers must fail the build when this promise rejects.
16
+ */
17
+ async function uploadViteSourceMaps(input, dependencies = {}) {
18
+ const projectRoot = resolve(input.projectRoot);
19
+ const outputRoot = await resolveBuildOutputDirectory$1(projectRoot, resolve(input.outDir));
20
+ let uploadError;
21
+ let manifest;
22
+ try {
23
+ if (input.buildId === "unversioned" && input.gitCommit === null) throw new Error("Visual Review source-map upload requires a versioned build identity");
24
+ const artifacts = await collectArtifacts(projectRoot, outputRoot, input.base);
25
+ if (artifacts.length === 0) throw new Error("Visual Review source-map upload found no JavaScript output");
26
+ manifest = {
27
+ schemaVersion: 1,
28
+ release: {
29
+ buildId: input.buildId,
30
+ gitCommit: input.gitCommit,
31
+ framework: "vite",
32
+ ...input.frameworkVersion ? { frameworkVersion: input.frameworkVersion } : {},
33
+ mode: input.mode
34
+ },
35
+ artifacts: artifacts.map(({ manifest: artifact }) => artifact)
36
+ };
37
+ await uploadSourceMapRelease({
38
+ manifest,
39
+ artifacts,
40
+ serviceUrl: input.serviceUrl,
41
+ token: input.token
42
+ }, dependencies);
43
+ } catch (error) {
44
+ uploadError = error;
45
+ }
46
+ let cleanupError;
47
+ try {
48
+ await removeSourceMapFiles$1(outputRoot);
49
+ } catch (error) {
50
+ cleanupError = error;
51
+ }
52
+ if (uploadError && cleanupError) throw new AggregateError([uploadError, cleanupError], "Visual Review source-map upload failed and deployment maps could not be removed");
53
+ if (cleanupError) throw new Error("Visual Review could not remove source maps from the deployment output", { cause: cleanupError });
54
+ if (uploadError) throw new Error("Visual Review source-map upload failed", { cause: uploadError });
55
+ if (!manifest) throw new Error("Visual Review source-map manifest was not created");
56
+ return manifest;
57
+ }
58
+ async function cleanupViteSourceMapOutput(projectRoot, outDir) {
59
+ await removeSourceMapFiles$1(await resolveBuildOutputDirectory$1(resolve(projectRoot), resolve(outDir)));
60
+ }
61
+ async function collectArtifacts(projectRoot, outputRoot, base) {
62
+ const files = await collectOutputFiles$1(outputRoot);
63
+ const publicOutputFiles = files.filter((path) => PUBLIC_SOURCE_MAP_REFERENCE_OUTPUT.test(path)).sort();
64
+ for (const generatedFile of publicOutputFiles) if (hasSourceMapReference((await readRegularFileBounded(generatedFile, outputRoot, 20971520)).toString("utf8"))) throw new Error(`Visual Review requires hidden source maps for ${deploymentRelativePath(outputRoot.path, generatedFile)}`);
65
+ const javascriptFiles = files.filter((path) => path.endsWith(".map") && JAVASCRIPT_OUTPUT$1.test(path.slice(0, -4))).map((path) => path.slice(0, -4)).sort();
66
+ const artifacts = [];
67
+ for (const generatedFile of javascriptFiles) {
68
+ const mapFile = `${generatedFile}.map`;
69
+ if (!files.includes(generatedFile)) throw new Error(`Visual Review generated JavaScript is missing for ${deploymentRelativePath(outputRoot.path, mapFile)}`);
70
+ const [generatedBytes, rawMapBytes] = await Promise.all([readRegularFileBounded(generatedFile, outputRoot, MAX_SOURCE_MAP_ARTIFACT_BYTES), readRegularFile$1(mapFile, outputRoot)]);
71
+ if (hasSourceMapReference(generatedBytes.toString("utf8"))) throw new Error(`Visual Review requires hidden source maps for ${deploymentRelativePath(outputRoot.path, generatedFile)}`);
72
+ const { bytes: mapBytes, debugId } = sanitizeSourceMapBytes(rawMapBytes, mapFile, projectRoot);
73
+ if (mapBytes.byteLength > 20971520) throw new Error(`Visual Review source map exceeds 20 MiB for ${deploymentRelativePath(outputRoot.path, generatedFile)}`);
74
+ artifacts.push({
75
+ manifest: {
76
+ runtime: "browser",
77
+ generatedPath: publicGeneratedPath(base, deploymentRelativePath(outputRoot.path, generatedFile)),
78
+ mapSha256: sha256$1(mapBytes),
79
+ generatedSha256: sha256$1(generatedBytes),
80
+ byteSize: mapBytes.byteLength,
81
+ ...debugId ? { debugId } : {}
82
+ },
83
+ mapBytes
84
+ });
85
+ }
86
+ return artifacts;
87
+ }
88
+ async function uploadSourceMapRelease(input, dependencies = {}) {
89
+ const fetchImplementation = dependencies.fetch ?? globalThis.fetch;
90
+ if (typeof fetchImplementation !== "function") throw new Error("Visual Review source-map upload requires Node fetch");
91
+ const serviceUrl = normalizeServiceUrl(input.serviceUrl ?? "https://visual-review.01.works");
92
+ const token = normalizeToken(input.token);
93
+ const manifestBody = JSON.stringify(input.manifest);
94
+ if (Buffer.byteLength(manifestBody) > MAX_MANIFEST_BYTES) throw new Error("Visual Review source-map manifest exceeds the 128 KiB service limit");
95
+ const release = parseCreateReleaseResponse(await request(fetchImplementation, releaseUrl(serviceUrl), {
96
+ method: "POST",
97
+ headers: authenticatedJsonHeaders(token),
98
+ body: manifestBody
99
+ }, readJson));
100
+ if (release.state === "finalized") {
101
+ if (release.uploads.length !== 0) throw new Error("Visual Review source-map service returned uploads for a finalized release");
102
+ return;
103
+ }
104
+ const slots = correlateUploadSlots(release.uploads, input.artifacts);
105
+ await Promise.all(slots.map(async ({ slot, artifact }) => {
106
+ const headers = normalizeUploadHeaders(slot.headers);
107
+ if (!hasHeader(headers, "content-type")) headers["content-type"] = "application/json";
108
+ await request(fetchImplementation, normalizeUploadUrl(slot.uploadUrl), {
109
+ method: "PUT",
110
+ headers,
111
+ body: artifact.mapBytes.buffer.slice(artifact.mapBytes.byteOffset, artifact.mapBytes.byteOffset + artifact.mapBytes.byteLength)
112
+ }, discardResponseBody);
113
+ }));
114
+ const completeBody = JSON.stringify({
115
+ schemaVersion: 1,
116
+ artifacts: slots.map(({ slot, artifact }) => ({
117
+ artifactId: slot.artifactId,
118
+ mapSha256: artifact.manifest.mapSha256,
119
+ byteSize: artifact.manifest.byteSize
120
+ }))
121
+ });
122
+ await request(fetchImplementation, `${releaseUrl(serviceUrl)}/${encodeURIComponent(release.releaseId)}/complete`, {
123
+ method: "POST",
124
+ headers: authenticatedJsonHeaders(token),
125
+ body: completeBody
126
+ }, discardResponseBody);
127
+ }
128
+ async function request(fetchImplementation, url, init, consume) {
129
+ const controller = new AbortController();
130
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
131
+ try {
132
+ const response = await fetchImplementation(url, {
133
+ ...init,
134
+ redirect: "error",
135
+ signal: controller.signal
136
+ });
137
+ if (!response.ok) {
138
+ await discardResponseBody(response);
139
+ throw new Error(`Visual Review source-map service returned HTTP ${response.status}`);
140
+ }
141
+ return await consume(response);
142
+ } finally {
143
+ clearTimeout(timeout);
144
+ }
145
+ }
146
+ function authenticatedJsonHeaders(token) {
147
+ return {
148
+ authorization: `Bearer ${token}`,
149
+ "content-type": "application/json"
150
+ };
151
+ }
152
+ async function readJson(response) {
153
+ const body = await response.text();
154
+ if (Buffer.byteLength(body) > MAX_MANIFEST_BYTES) throw new Error("Visual Review source-map service response is too large");
155
+ try {
156
+ return JSON.parse(body);
157
+ } catch {
158
+ throw new Error("Visual Review source-map service returned invalid JSON");
159
+ }
160
+ }
161
+ async function discardResponseBody(response) {
162
+ try {
163
+ await response.body?.cancel();
164
+ } catch {}
165
+ }
166
+ function parseCreateReleaseResponse(value) {
167
+ if (!isRecord$2(value) || value.schemaVersion !== 1 || typeof value.releaseId !== "string" || !SAFE_RELEASE_ID.test(value.releaseId) || value.state !== "pending" && value.state !== "finalized") throw new Error("Visual Review source-map service returned an invalid release");
168
+ if (!Array.isArray(value.uploads)) throw new Error("Visual Review source-map service returned invalid upload slots");
169
+ const uploads = value.uploads.map((slot) => {
170
+ if (!isRecord$2(slot) || typeof slot.artifactId !== "string" || !SAFE_ARTIFACT_ID.test(slot.artifactId) || !isRuntime(slot.runtime) || typeof slot.generatedPath !== "string" || typeof slot.uploadUrl !== "string" || slot.headers !== void 0 && !isStringRecord(slot.headers)) throw new Error("Visual Review source-map service returned an invalid upload slot");
171
+ return {
172
+ artifactId: slot.artifactId,
173
+ runtime: slot.runtime,
174
+ generatedPath: slot.generatedPath,
175
+ uploadUrl: slot.uploadUrl,
176
+ ...slot.headers ? { headers: slot.headers } : {}
177
+ };
178
+ });
179
+ return {
180
+ schemaVersion: 1,
181
+ releaseId: value.releaseId,
182
+ state: value.state,
183
+ uploads
184
+ };
185
+ }
186
+ function correlateUploadSlots(slots, artifacts) {
187
+ if (slots.length !== artifacts.length) throw new Error("Visual Review source-map service returned the wrong number of upload slots");
188
+ const artifactIds = /* @__PURE__ */ new Set();
189
+ const remaining = new Map(artifacts.map((artifact) => [artifactKey(artifact.manifest.runtime, artifact.manifest.generatedPath), artifact]));
190
+ const correlated = slots.map((slot) => {
191
+ if (artifactIds.has(slot.artifactId)) throw new Error("Visual Review source-map service returned a duplicate artifact ID");
192
+ artifactIds.add(slot.artifactId);
193
+ const key = artifactKey(slot.runtime, slot.generatedPath);
194
+ const artifact = remaining.get(key);
195
+ if (!artifact) throw new Error("Visual Review source-map service returned an unknown upload target");
196
+ remaining.delete(key);
197
+ return {
198
+ slot,
199
+ artifact
200
+ };
201
+ });
202
+ if (remaining.size > 0) throw new Error("Visual Review source-map service omitted an upload target");
203
+ return correlated;
204
+ }
205
+ function normalizeUploadHeaders(value) {
206
+ const normalized = {};
207
+ for (const [name, headerValue] of Object.entries(value ?? {})) {
208
+ const key = name.trim().toLowerCase();
209
+ if (!/^[a-z0-9!#$%&'*+.^_`|~-]+$/.test(key) || /[\r\n]/.test(headerValue)) throw new Error("Visual Review source-map service returned an invalid upload header");
210
+ if ([
211
+ "authorization",
212
+ "cookie",
213
+ "host"
214
+ ].includes(key)) throw new Error("Visual Review source-map service returned a forbidden upload header");
215
+ normalized[key] = headerValue;
216
+ }
217
+ return normalized;
218
+ }
219
+ function normalizeServiceUrl(value) {
220
+ const url = parseSecureUrl(value, "service");
221
+ if (url.pathname !== "/" || url.search || url.hash) throw new Error("Visual Review source-map service URL must be an origin without a path");
222
+ return url.origin;
223
+ }
224
+ function normalizeUploadUrl(value) {
225
+ const url = parseSecureUrl(value, "upload");
226
+ if (url.hash) throw new Error("Visual Review source-map upload URL must not contain a fragment");
227
+ return url.href;
228
+ }
229
+ function parseSecureUrl(value, label) {
230
+ let url;
231
+ try {
232
+ url = new URL(value);
233
+ } catch {
234
+ throw new Error(`Visual Review source-map ${label} URL is invalid`);
235
+ }
236
+ const loopback = [
237
+ "localhost",
238
+ "127.0.0.1",
239
+ "[::1]"
240
+ ].includes(url.hostname);
241
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password) throw new Error(`Visual Review source-map ${label} URL must use HTTPS`);
242
+ return url;
243
+ }
244
+ function releaseUrl(serviceUrl) {
245
+ return `${serviceUrl}${RELEASES_PATH}`;
246
+ }
247
+ function normalizeToken(value) {
248
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || /\s/.test(value)) throw new Error("Visual Review source-map upload token is missing or invalid");
249
+ return value;
250
+ }
251
+ async function collectOutputFiles$1(outputRoot, directory = outputRoot.path) {
252
+ const entries = await readOutputDirectory$1(directory, outputRoot);
253
+ return (await Promise.all(entries.map(async (entry) => {
254
+ const path = resolve(directory, entry.name);
255
+ if (entry.isSymbolicLink()) throw new Error("Visual Review Vite build output must not contain symbolic links");
256
+ if (entry.isDirectory()) return collectOutputFiles$1(outputRoot, path);
257
+ if (entry.isFile()) return [path];
258
+ return [];
259
+ }))).flat();
260
+ }
261
+ async function removeSourceMapFiles$1(outputRoot, directory = outputRoot.path) {
262
+ const entries = await readOutputDirectory$1(directory, outputRoot);
263
+ const failures = [];
264
+ await Promise.all(entries.map(async (entry) => {
265
+ const path = resolve(directory, entry.name);
266
+ try {
267
+ if (entry.isDirectory()) await removeSourceMapFiles$1(outputRoot, path);
268
+ else if (entry.isSymbolicLink()) throw new Error("Visual Review Vite build output must not contain symbolic links");
269
+ else if (entry.isFile() && /\.map$/iu.test(entry.name)) await unlinkRegularFile$1(path, outputRoot);
270
+ } catch (error) {
271
+ failures.push(error);
272
+ }
273
+ }));
274
+ if (failures.length > 0) throw new AggregateError(failures, "Could not remove all Visual Review source maps");
275
+ }
276
+ async function readRegularFile$1(path, root) {
277
+ const { handle } = await openRegularFile$1(path, root, constants.O_RDONLY);
278
+ try {
279
+ return await handle.readFile();
280
+ } finally {
281
+ await handle.close();
282
+ }
283
+ }
284
+ async function readRegularFileBounded(path, root, maxBytes) {
285
+ const { handle, metadata } = await openRegularFile$1(path, root, constants.O_RDONLY);
286
+ try {
287
+ if (metadata.size > maxBytes) throw new Error(`Visual Review public source-map scan exceeds 20 MiB for ${deploymentRelativePath(root.path, path)}`);
288
+ const chunks = [];
289
+ let position = 0;
290
+ while (position <= maxBytes) {
291
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - position));
292
+ const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, position);
293
+ if (bytesRead === 0) break;
294
+ chunks.push(chunk.subarray(0, bytesRead));
295
+ position += bytesRead;
296
+ }
297
+ if (position > maxBytes) throw new Error(`Visual Review public source-map scan exceeds 20 MiB for ${deploymentRelativePath(root.path, path)}`);
298
+ return Buffer.concat(chunks, position);
299
+ } finally {
300
+ await handle.close();
301
+ }
302
+ }
303
+ async function unlinkRegularFile$1(path, root) {
304
+ const parent = dirname(path);
305
+ const parentMetadata = await validateOutputDirectory$1(parent, root);
306
+ const { handle, metadata } = await openRegularFile$1(path, root, constants.O_WRONLY);
307
+ try {
308
+ await handle.truncate(0);
309
+ } finally {
310
+ await handle.close();
311
+ }
312
+ const [current, currentParent] = await Promise.all([lstat(path), validateOutputDirectory$1(parent, root)]);
313
+ if (current.isSymbolicLink() || !current.isFile() || !sameFile$1(current, metadata) || !sameFile$1(currentParent, parentMetadata)) throw new Error("Visual Review Vite source-map output changed during cleanup");
314
+ await unlink(path);
315
+ }
316
+ async function resolveBuildOutputDirectory$1(projectRoot, outDir) {
317
+ assertBuildOutput$1(projectRoot, outDir);
318
+ const before = await lstat(outDir);
319
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error("Visual Review Vite outDir must be a real directory");
320
+ const [canonicalProjectRoot, canonicalOutDir] = await Promise.all([realpath(projectRoot), realpath(outDir)]);
321
+ assertBuildOutput$1(canonicalProjectRoot, canonicalOutDir);
322
+ const after = await lstat(outDir);
323
+ if (!after.isDirectory() || after.isSymbolicLink() || !sameFile$1(before, after)) throw new Error("Visual Review Vite outDir changed during validation");
324
+ return {
325
+ path: outDir,
326
+ canonicalPath: canonicalOutDir,
327
+ device: after.dev,
328
+ inode: after.ino
329
+ };
330
+ }
331
+ async function assertOutputRootUnchanged$1(root) {
332
+ const metadata = await lstat(root.path);
333
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("Visual Review Vite outDir must be a real directory");
334
+ if (metadata.dev !== root.device || metadata.ino !== root.inode) throw new Error("Visual Review Vite outDir changed during source-map processing");
335
+ if (await realpath(root.path) !== root.canonicalPath) throw new Error("Visual Review Vite outDir changed during source-map processing");
336
+ }
337
+ async function readOutputDirectory$1(directory, root) {
338
+ const before = await validateOutputDirectory$1(directory, root);
339
+ const entries = await readdir(directory, { withFileTypes: true });
340
+ if (!sameFile$1(before, await validateOutputDirectory$1(directory, root))) throw new Error("Visual Review Vite output directory changed while reading");
341
+ return entries;
342
+ }
343
+ async function validateOutputDirectory$1(directory, root) {
344
+ assertAtOrInside$1(root.path, directory);
345
+ await assertOutputRootUnchanged$1(root);
346
+ const before = await lstat(directory);
347
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error("Visual Review Vite build output must not contain symbolic links");
348
+ const canonical = await realpath(directory);
349
+ assertAtOrInside$1(root.canonicalPath, canonical);
350
+ const after = await lstat(directory);
351
+ if (!after.isDirectory() || after.isSymbolicLink() || !sameFile$1(before, after)) throw new Error("Visual Review Vite output directory changed during validation");
352
+ await assertOutputRootUnchanged$1(root);
353
+ return after;
354
+ }
355
+ async function openRegularFile$1(path, root, flags) {
356
+ assertInside$2(root.path, path);
357
+ await assertOutputRootUnchanged$1(root);
358
+ const parent = dirname(path);
359
+ const parentMetadata = await validateOutputDirectory$1(parent, root);
360
+ const before = await lstat(path);
361
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("Visual Review source-map output must be a regular file");
362
+ const canonical = await realpath(path);
363
+ assertInside$2(root.canonicalPath, canonical);
364
+ const handle = await open(path, flags | constants.O_NOFOLLOW);
365
+ try {
366
+ const metadata = await handle.stat();
367
+ const currentParent = await validateOutputDirectory$1(parent, root);
368
+ if (!metadata.isFile() || !sameFile$1(before, metadata) || !sameFile$1(parentMetadata, currentParent)) throw new Error("Visual Review Vite source-map output changed while opening");
369
+ return {
370
+ handle,
371
+ metadata
372
+ };
373
+ } catch (error) {
374
+ await handle.close();
375
+ throw error;
376
+ }
377
+ }
378
+ function assertBuildOutput$1(projectRoot, outDir) {
379
+ const path = relative(projectRoot, outDir);
380
+ if (!path || path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new Error("Visual Review Vite outDir must be below the project root");
381
+ }
382
+ function sameFile$1(left, right) {
383
+ return left.dev === right.dev && left.ino === right.ino;
384
+ }
385
+ function assertAtOrInside$1(root, path) {
386
+ const candidate = relative(resolve(root), resolve(path));
387
+ if (candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) throw new Error("Visual Review source-map path escaped the deployment output");
388
+ }
389
+ function deploymentRelativePath(outDir, path) {
390
+ assertInside$2(outDir, path);
391
+ return relative(outDir, path).split(sep).join("/");
392
+ }
393
+ function assertInside$2(root, path) {
394
+ const candidate = relative(resolve(root), resolve(path));
395
+ if (!candidate || candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) throw new Error("Visual Review source-map path escaped the deployment output");
396
+ }
397
+ function publicGeneratedPath(base, relativePath) {
398
+ const safeRelative = relativePath.replace(/^\/+/, "");
399
+ if (base.includes("://")) {
400
+ const baseUrl = new URL(base.endsWith("/") ? base : `${base}/`);
401
+ return new URL(safeRelative, baseUrl).pathname;
402
+ }
403
+ const normalizedBase = base.trim().replace(/\\/g, "/");
404
+ return `${normalizedBase === "" || normalizedBase === "./" ? "/" : `/${normalizedBase.replace(/^\/+|\/+$/g, "")}/`}${safeRelative}`.replace(/\/{2,}/g, "/");
405
+ }
406
+ function sanitizeSourceMapBytes(bytes, mapFile, projectRoot, framework = "vite") {
407
+ let parsed;
408
+ try {
409
+ parsed = JSON.parse(bytes.toString("utf8"));
410
+ } catch {
411
+ throw new Error("Visual Review received an invalid source map");
412
+ }
413
+ if (!isRecord$2(parsed) || parsed.version !== 3) throw new Error("Visual Review only accepts Source Map v3 output");
414
+ const meaningful = sanitizeSourceMapObject(parsed, dirname(mapFile), projectRoot, framework);
415
+ if (!meaningful && Array.isArray(parsed.sections)) {
416
+ delete parsed.sections;
417
+ parsed.sources = [];
418
+ parsed.names = [];
419
+ parsed.mappings = "";
420
+ }
421
+ const debugId = typeof parsed.debugId === "string" && /^[A-Za-z0-9._:-]{1,255}$/.test(parsed.debugId) ? parsed.debugId : void 0;
422
+ return {
423
+ bytes: Buffer.from(JSON.stringify(parsed)),
424
+ ...debugId ? { debugId } : {},
425
+ ...!meaningful ? { empty: true } : {}
426
+ };
427
+ }
428
+ function sanitizeSourceMapObject(map, mapDirectory, projectRoot, framework) {
429
+ sanitizeSourceMapFile(map);
430
+ delete map.sourcesContent;
431
+ if (Array.isArray(map.sections)) {
432
+ delete map.sourceRoot;
433
+ let meaningful = false;
434
+ for (const section of map.sections) {
435
+ if (!isRecord$2(section) || "url" in section || !isRecord$2(section.map)) throw new Error("Visual Review refuses external or invalid indexed source maps");
436
+ if (section.map.version !== 3) throw new Error("Visual Review only accepts Source Map v3 output");
437
+ if (sanitizeSourceMapObject(section.map, mapDirectory, projectRoot, framework)) meaningful = true;
438
+ else section.map = {
439
+ version: 3,
440
+ sources: [],
441
+ names: [],
442
+ mappings: ""
443
+ };
444
+ }
445
+ return meaningful;
446
+ }
447
+ if (!Array.isArray(map.sources) || !map.sources.every((source) => typeof source === "string")) throw new Error("Visual Review source map is missing its sources");
448
+ if (!Array.isArray(map.names) || !map.names.every((name) => typeof name === "string")) throw new Error("Visual Review source map has invalid names");
449
+ if (typeof map.mappings !== "string") throw new Error("Visual Review source map is missing its mappings");
450
+ const sourceRoot = map.sourceRoot === void 0 ? "" : map.sourceRoot;
451
+ if (typeof sourceRoot !== "string" || framework !== "next" && isUnsafeSourcePath(sourceRoot)) throw new Error("Visual Review source map has an unsafe sourceRoot");
452
+ map.sources = map.sources.map((source) => normalizeSourcePath(source, sourceRoot, mapDirectory, projectRoot, framework));
453
+ delete map.sourceRoot;
454
+ return typeof map.mappings === "string" && /[^;,]/u.test(map.mappings);
455
+ }
456
+ function sanitizeSourceMapFile(map) {
457
+ if (map.file === void 0) return;
458
+ if (typeof map.file !== "string") {
459
+ delete map.file;
460
+ return;
461
+ }
462
+ const normalized = map.file.replaceAll("\\", "/").normalize("NFC");
463
+ if (normalized.length === 0 || normalized.length > 2048 || normalized.trim() !== normalized || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(normalized) || hasUnsafePathCharacters(normalized) || normalized.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
464
+ delete map.file;
465
+ return;
466
+ }
467
+ map.file = normalized;
468
+ }
469
+ function normalizeSourcePath(source, sourceRoot, mapDirectory, projectRoot, framework) {
470
+ if (framework === "next") return normalizeNextSourcePath(source, sourceRoot, mapDirectory, projectRoot);
471
+ if (source.length === 0 || isUnsafeSourcePath(source)) throw new Error("Visual Review source map has an unsafe source path");
472
+ const repositoryRelative = relative(projectRoot, isAbsolute(source) ? resolve(source) : resolve(mapDirectory, sourceRoot, source));
473
+ if (repositoryRelative.length === 0 || repositoryRelative === ".." || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) throw new Error("Visual Review source map source escaped the project root");
474
+ return repositoryRelative.split(sep).join("/");
475
+ }
476
+ function normalizeNextSourcePath(source, sourceRoot, mapDirectory, projectRoot) {
477
+ if (source.length === 0) throw new Error("Visual Review source map has an unsafe source path");
478
+ const combined = combineNextSourceRoot(sourceRoot, source);
479
+ if (combined.length === 0 || combined.length > 4096 || hasUnsafePathCharacters(combined)) throw new Error("Visual Review source map has an unsafe source path");
480
+ const ignoredSource = /^ignored\|(.+)$/u.exec(combined)?.[1];
481
+ if (ignoredSource) return normalizeNextSourcePath(ignoredSource, "", mapDirectory, projectRoot);
482
+ if (/^(?:webpack|turbopack):\/\//iu.test(combined)) return normalizeNextVirtualSource(combined, mapDirectory, projectRoot) ?? opaqueNextSource(combined);
483
+ if (/^file:\/\//iu.test(combined)) {
484
+ let url;
485
+ try {
486
+ url = new URL(combined);
487
+ } catch {
488
+ throw new Error("Visual Review source map has an unsafe source path");
489
+ }
490
+ if (url.hostname && url.hostname !== "localhost") return opaqueNextSource(combined);
491
+ const pathname = decodeNextDecoratedPath(url.pathname);
492
+ return (pathname ? repositoryRelativeSource(pathname, projectRoot) : null) ?? opaqueNextSource(combined);
493
+ }
494
+ if (/^https?:\/\//iu.test(combined)) {
495
+ let url;
496
+ try {
497
+ url = new URL(combined);
498
+ } catch {
499
+ return opaqueNextSource(combined);
500
+ }
501
+ if (!isLocalSourceMapHostname(url.hostname)) return opaqueNextSource(combined);
502
+ const pathname = decodeNextDecoratedPath(url.pathname);
503
+ return (pathname ? repositoryRelativeSource(pathname, projectRoot) : null) ?? opaqueNextSource(combined);
504
+ }
505
+ if (/^[A-Za-z]:[\\/]/u.test(combined)) {
506
+ const undecorated = decodeNextDecoratedPath(combined);
507
+ return (undecorated ? repositoryRelativeWindowsSource(undecorated, projectRoot) : null) ?? opaqueNextSource(combined);
508
+ }
509
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(combined)) return opaqueNextSource(combined);
510
+ const undecorated = decodeNextDecoratedPath(combined);
511
+ if (!undecorated) return opaqueNextSource(combined);
512
+ return repositoryRelativeSource(isAbsolute(undecorated) ? resolve(undecorated) : resolve(mapDirectory, undecorated), projectRoot) ?? normalizeNextContextPath(undecorated, projectRoot) ?? opaqueNextSource(combined);
513
+ }
514
+ function combineNextSourceRoot(sourceRoot, source) {
515
+ if (!sourceRoot) return source;
516
+ if (sourceRoot.endsWith("/") || sourceRoot.endsWith("\\")) return `${sourceRoot}${source}`;
517
+ return `${sourceRoot}/${source}`;
518
+ }
519
+ function normalizeNextVirtualSource(source, mapDirectory, projectRoot) {
520
+ const separator = source.indexOf("://");
521
+ if (separator < 0) return null;
522
+ const rest = source.slice(separator + 3);
523
+ let pathname;
524
+ if (rest.startsWith("/")) pathname = rest.replace(/^\/+/, "");
525
+ else {
526
+ const firstSlash = rest.indexOf("/");
527
+ if (firstSlash < 0) return null;
528
+ pathname = rest.slice(firstSlash + 1);
529
+ }
530
+ pathname = decodeNextDecoratedPath(pathname) ?? "";
531
+ if (!pathname) return null;
532
+ pathname = pathname.replace(/^\[project\]\//u, "").replace(/^\.\//u, "");
533
+ const ignoredSource = /^ignored\|(.+)$/u.exec(pathname)?.[1];
534
+ if (ignoredSource) return normalizeNextSourcePath(ignoredSource, "", mapDirectory, projectRoot);
535
+ if (pathname.startsWith("[") || pathname.startsWith("external ") || pathname.startsWith("webpack/")) return null;
536
+ return normalizeNextContextPath(pathname, projectRoot);
537
+ }
538
+ function isLocalSourceMapHostname(hostname) {
539
+ const normalized = hostname.toLowerCase();
540
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "[::1]";
541
+ }
542
+ function repositoryRelativeWindowsSource(path, projectRoot) {
543
+ const normalizedPath = path.replaceAll("\\", "/").normalize("NFC");
544
+ const normalizedRoot = projectRoot.replaceAll("\\", "/").replace(/\/$/u, "").normalize("NFC");
545
+ if (!/^[A-Za-z]:\//u.test(normalizedPath) || !/^[A-Za-z]:\//u.test(normalizedRoot)) return null;
546
+ const lowerPath = normalizedPath.toLowerCase();
547
+ const lowerRoot = normalizedRoot.toLowerCase();
548
+ if (!lowerPath.startsWith(`${lowerRoot}/`)) return null;
549
+ const repositoryRelative = normalizedPath.slice(normalizedRoot.length + 1);
550
+ return isCoreSafeNextRepositoryPath(repositoryRelative) ? repositoryRelative : null;
551
+ }
552
+ function normalizeNextContextPath(value, projectRoot) {
553
+ const withoutQuery = decodeNextDecoratedPath(value)?.replace(/^\.\//u, "") ?? "";
554
+ if (!withoutQuery || hasUnsafePathCharacters(withoutQuery)) return null;
555
+ let context = resolve(projectRoot);
556
+ while (true) {
557
+ const normalized = repositoryRelativeSource(resolve(context, withoutQuery), projectRoot);
558
+ if (normalized) return normalized;
559
+ const parent = dirname(context);
560
+ if (parent === context) break;
561
+ context = parent;
562
+ }
563
+ return null;
564
+ }
565
+ function decodeNextDecoratedPath(value) {
566
+ let decoded = value;
567
+ const encodedOctet = /%[0-9a-f]{2}/iu;
568
+ for (let iteration = 0; iteration < 4 && encodedOctet.test(decoded); iteration += 1) try {
569
+ decoded = decodeURIComponent(decoded);
570
+ } catch {
571
+ return null;
572
+ }
573
+ if (encodedOctet.test(decoded)) return null;
574
+ const undecorated = decoded.split(/[?#]/u, 1)[0] ?? "";
575
+ if (!undecorated || hasUnsafePathCharacters(undecorated)) return null;
576
+ return undecorated;
577
+ }
578
+ function repositoryRelativeSource(path, projectRoot) {
579
+ const repositoryRelative = relative(resolve(projectRoot), resolve(path));
580
+ if (repositoryRelative.length === 0 || repositoryRelative === ".." || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) return null;
581
+ const normalized = repositoryRelative.split(sep).join("/").normalize("NFC");
582
+ return isCoreSafeNextRepositoryPath(normalized) ? normalized : null;
583
+ }
584
+ function isCoreSafeNextRepositoryPath(value) {
585
+ if (value.length === 0 || value.length > 2048 || value.trim() !== value || value.startsWith("/") || value.startsWith("\\") || value.includes("\\") || value.includes("?") || value.includes("#") || value.includes(":") || /%[0-9a-f]{2}/iu.test(value) || hasUnsafePathCharacters(value)) return false;
586
+ return value.split("/").every((segment) => segment.length > 0 && segment.length <= 255 && segment !== "." && segment !== "..");
587
+ }
588
+ function opaqueNextSource(source) {
589
+ return `node_modules/.visual-review-next/${sha256$1(Buffer.from(source)).slice(0, 24)}.js`;
590
+ }
591
+ function hasUnsafePathCharacters(value) {
592
+ return /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(value);
593
+ }
594
+ function isUnsafeSourcePath(value) {
595
+ const uriScheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value);
596
+ const windowsDrive = /^[A-Za-z]:[\\/]/.test(value);
597
+ return value.length > 4096 || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(value) || uriScheme && !windowsDrive;
598
+ }
599
+ function sha256$1(bytes) {
600
+ return createHash("sha256").update(bytes).digest("hex");
601
+ }
602
+ function hasSourceMapReference(source) {
603
+ return /(?:^|\n)\s*\/\/[#@]\s*sourceMappingURL\s*=\s*\S+\s*(?:\n|$)/m.test(source) || /\/\*[#@]\s*sourceMappingURL\s*=\s*[^*]+\*\//m.test(source);
604
+ }
605
+ function artifactKey(runtime, generatedPath) {
606
+ return `${runtime}\0${generatedPath}`;
607
+ }
608
+ function hasHeader(headers, name) {
609
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
610
+ }
611
+ function isRuntime(value) {
612
+ return value === "browser" || value === "server-node" || value === "server-edge";
613
+ }
614
+ function isRecord$2(value) {
615
+ return typeof value === "object" && value !== null && !Array.isArray(value);
616
+ }
617
+ function isStringRecord(value) {
618
+ return isRecord$2(value) && Object.values(value).every((item) => typeof item === "string");
619
+ }
620
+ //#endregion
621
+ //#region src/next-source-map-upload.ts
622
+ const JAVASCRIPT_OUTPUT = /\.(?:c|m)?js$/iu;
623
+ const SOURCE_MAPPED_OUTPUT = /(?:\.(?:c|m)?js|\.css)$/iu;
624
+ const MAX_EDGE_MANIFEST_BYTES$1 = 5 * 1024 * 1024;
625
+ /**
626
+ * Collects private Next.js browser, Node, and Edge maps after compilation.
627
+ * Every sourceMappingURL and map is removed even when collection or upload fails.
628
+ */
629
+ async function uploadNextSourceMaps(input, dependencies = {}) {
630
+ const projectRoot = resolve(input.projectRoot);
631
+ const outputRoot = await resolveBuildOutputDirectory(projectRoot, resolve(input.distDir));
632
+ let uploadError;
633
+ let manifest;
634
+ try {
635
+ if (input.buildId === "unversioned" && input.gitCommit === null) throw new Error("Visual Review source-map upload requires a versioned build identity");
636
+ const pairs = await collectNextOutputPairs({
637
+ outputRoot,
638
+ files: await collectOutputFiles(outputRoot),
639
+ edgeFiles: await readEdgeRuntimeFiles(outputRoot)
640
+ });
641
+ await stripSourceMapReferences(outputRoot);
642
+ const artifacts = await collectNextArtifacts({
643
+ projectRoot,
644
+ outputRoot,
645
+ pairs,
646
+ basePath: input.basePath,
647
+ assetPrefix: input.assetPrefix
648
+ });
649
+ if (artifacts.length === 0) throw new Error("Visual Review source-map upload found no paired Next.js output");
650
+ manifest = {
651
+ schemaVersion: 1,
652
+ release: {
653
+ buildId: input.buildId,
654
+ gitCommit: input.gitCommit,
655
+ framework: "next",
656
+ ...input.frameworkVersion ? { frameworkVersion: input.frameworkVersion } : {},
657
+ mode: input.mode
658
+ },
659
+ artifacts: artifacts.map(({ manifest: artifact }) => artifact)
660
+ };
661
+ await uploadSourceMapRelease({
662
+ manifest,
663
+ artifacts,
664
+ token: input.token,
665
+ serviceUrl: input.serviceUrl
666
+ }, dependencies);
667
+ } catch (error) {
668
+ uploadError = error;
669
+ }
670
+ let cleanupError;
671
+ try {
672
+ await cleanupNextSourceMapOutputRoot(outputRoot);
673
+ } catch (error) {
674
+ cleanupError = error;
675
+ }
676
+ if (uploadError && cleanupError) throw new AggregateError([uploadError, cleanupError], "Visual Review Next.js source-map upload failed and private build output could not be cleaned");
677
+ if (cleanupError) throw new Error("Visual Review could not clean private Next.js source-map output", { cause: cleanupError });
678
+ if (uploadError) throw new Error("Visual Review Next.js source-map upload failed", { cause: uploadError });
679
+ if (!manifest) throw new Error("Visual Review Next.js source-map manifest was not created");
680
+ return manifest;
681
+ }
682
+ async function cleanupNextSourceMapOutput(projectRootValue, distDirValue) {
683
+ await cleanupNextSourceMapOutputRoot(await resolveBuildOutputDirectory(resolve(projectRootValue), resolve(distDirValue)));
684
+ }
685
+ async function cleanupNextSourceMapOutputRoot(outputRoot) {
686
+ const failures = (await Promise.allSettled([stripSourceMapReferences(outputRoot), removeSourceMapFiles(outputRoot)])).filter((result) => result.status === "rejected").map((result) => result.reason);
687
+ if (failures.length > 0) throw new AggregateError(failures, "Could not clean all Visual Review Next.js source maps");
688
+ }
689
+ async function collectNextArtifacts(input) {
690
+ const artifacts = [];
691
+ for (const { generatedFile, mapFile, outputPath, runtime } of input.pairs) {
692
+ const [generatedBytes, rawMapBytes] = await Promise.all([readRegularFile(generatedFile, input.outputRoot), readRegularFile(mapFile, input.outputRoot)]);
693
+ if (containsSourceMapReference(generatedBytes.toString("utf8"))) throw new Error(`Visual Review could not remove sourceMappingURL from ${outputPath}`);
694
+ const { bytes: mapBytes, debugId, empty } = sanitizeSourceMapBytes(rawMapBytes, mapFile, input.projectRoot, "next");
695
+ if (empty) continue;
696
+ if (mapBytes.byteLength > 20971520) throw new Error(`Visual Review source map exceeds 20 MiB for ${outputPath}`);
697
+ artifacts.push({
698
+ manifest: {
699
+ runtime,
700
+ generatedPath: runtime === "browser" ? browserGeneratedPath(outputPath, input.basePath, input.assetPrefix) : serverGeneratedPath(input.projectRoot, generatedFile),
701
+ mapSha256: sha256(mapBytes),
702
+ generatedSha256: sha256(generatedBytes),
703
+ byteSize: mapBytes.byteLength,
704
+ ...debugId ? { debugId } : {}
705
+ },
706
+ mapBytes
707
+ });
708
+ }
709
+ return artifacts.sort((left, right) => `${left.manifest.runtime}\0${left.manifest.generatedPath}`.localeCompare(`${right.manifest.runtime}\0${right.manifest.generatedPath}`));
710
+ }
711
+ async function collectNextOutputPairs(input) {
712
+ const fileSet = new Set(input.files);
713
+ const generatedFiles = input.files.filter((path) => JAVASCRIPT_OUTPUT.test(path)).sort();
714
+ const pairs = [];
715
+ for (const generatedFile of generatedFiles) {
716
+ const outputPath = outputRelativePath(input.outputRoot.path, generatedFile);
717
+ const runtime = classifyRuntime(outputPath, input.edgeFiles);
718
+ if (!runtime) continue;
719
+ const reference = trailingSourceMapReference((await readRegularFile(generatedFile, input.outputRoot)).toString("utf8"));
720
+ const mapFile = reference === null ? `${generatedFile}.map` : resolveSourceMapReference(generatedFile, reference, input.outputRoot.path);
721
+ if (!fileSet.has(mapFile)) continue;
722
+ pairs.push({
723
+ generatedFile,
724
+ mapFile,
725
+ outputPath,
726
+ runtime
727
+ });
728
+ }
729
+ return pairs;
730
+ }
731
+ function trailingSourceMapReference(source) {
732
+ const line = source.match(/(?:^|\r?\n)[ \t]*\/\/[#@][ \t]*sourceMappingURL[ \t]*=[ \t]*([^\r\n]+?)[ \t]*(?:\r?\n)?$/u);
733
+ if (line) return line[1] ?? null;
734
+ return source.match(/(?:^|\r?\n)?[ \t]*\/\*[#@][ \t]*sourceMappingURL[ \t]*=[ \t]*([^*]+?)[ \t]*\*\/[ \t]*(?:\r?\n)?$/u)?.[1] ?? null;
735
+ }
736
+ function resolveSourceMapReference(generatedFile, reference, distDir) {
737
+ let decoded = reference;
738
+ let stable = false;
739
+ for (let iteration = 0; iteration < 8; iteration += 1) {
740
+ let next;
741
+ try {
742
+ next = decodeURIComponent(decoded);
743
+ } catch {
744
+ throw new Error("Visual Review received an invalid Next.js source-map reference");
745
+ }
746
+ if (next === decoded) {
747
+ stable = true;
748
+ break;
749
+ }
750
+ decoded = next;
751
+ }
752
+ if (!reference || reference.length > 4096 || reference.trim() !== reference || !stable || decoded.startsWith("/") || decoded.includes("\\") || decoded.includes("?") || decoded.includes("#") || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(decoded) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(decoded) || decoded.split("/").some((segment) => !segment || segment === "." || segment === "..") || !decoded.endsWith(".map")) throw new Error("Visual Review received an unsafe Next.js source-map reference");
753
+ const mapFile = resolve(dirname(generatedFile), decoded);
754
+ assertInside$1(distDir, mapFile);
755
+ return mapFile;
756
+ }
757
+ function classifyRuntime(outputPath, edgeFiles) {
758
+ if (outputPath.startsWith("static/")) return "browser";
759
+ if (!outputPath.startsWith("server/")) return null;
760
+ if (outputPath.startsWith("server/edge/") || outputPath === "server/edge-runtime-webpack.js" || edgeFiles.has(outputPath)) return "server-edge";
761
+ return "server-node";
762
+ }
763
+ async function readEdgeRuntimeFiles(outputRoot) {
764
+ const manifestPath = resolve(outputRoot.path, "server/middleware-manifest.json");
765
+ let bytes;
766
+ try {
767
+ bytes = await readRegularFile(manifestPath, outputRoot);
768
+ } catch (error) {
769
+ if (isNodeError$1(error) && error.code === "ENOENT") return /* @__PURE__ */ new Set();
770
+ throw error;
771
+ }
772
+ if (bytes.byteLength > MAX_EDGE_MANIFEST_BYTES$1) throw new Error("Visual Review Next.js middleware manifest exceeds 5 MiB");
773
+ let parsed;
774
+ try {
775
+ parsed = JSON.parse(bytes.toString("utf8"));
776
+ } catch {
777
+ throw new Error("Visual Review received an invalid Next.js middleware manifest");
778
+ }
779
+ if (!isRecord$1(parsed)) throw new Error("Visual Review received an invalid Next.js middleware manifest");
780
+ const files = /* @__PURE__ */ new Set();
781
+ for (const groupName of ["middleware", "functions"]) {
782
+ const group = parsed[groupName];
783
+ if (group === void 0) continue;
784
+ if (!isRecord$1(group)) throw new Error("Visual Review received an invalid Next.js middleware manifest");
785
+ for (const entry of Object.values(group)) {
786
+ if (!isRecord$1(entry) || !Array.isArray(entry.files)) throw new Error("Visual Review received an invalid Next.js middleware manifest");
787
+ for (const file of entry.files) {
788
+ if (typeof file !== "string") throw new Error("Visual Review received an invalid Next.js middleware manifest");
789
+ const normalized = normalizeManifestOutputPath(file);
790
+ if (normalized.endsWith(".js")) files.add(normalized);
791
+ }
792
+ }
793
+ }
794
+ return files;
795
+ }
796
+ function normalizeManifestOutputPath(value) {
797
+ const normalized = value.replaceAll("\\", "/").replace(/^\/+/, "");
798
+ if (!normalized || normalized.length > 4096 || normalized.split("/").some((segment) => segment === "..") || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(normalized)) throw new Error("Visual Review received an unsafe Next.js middleware output path");
799
+ return normalized;
800
+ }
801
+ async function stripSourceMapReferences(outputRoot) {
802
+ const files = await collectOutputFiles(outputRoot);
803
+ const failures = [];
804
+ await Promise.all(files.filter((path) => SOURCE_MAPPED_OUTPUT.test(path)).map(async (path) => {
805
+ try {
806
+ const source = (await readRegularFile(path, outputRoot)).toString("utf8");
807
+ const stripped = stripTrailingSourceMapReferences(source);
808
+ if (stripped !== source) await writeRegularFile(path, stripped, outputRoot);
809
+ } catch (error) {
810
+ failures.push(error);
811
+ }
812
+ }));
813
+ if (failures.length > 0) throw new AggregateError(failures, "Could not remove all Next.js sourceMappingURL comments");
814
+ }
815
+ function stripTrailingSourceMapReferences(source) {
816
+ let result = source;
817
+ while (true) {
818
+ const stripped = result.replace(/(?:(?:^|\r?\n)[ \t]*\/\/[#@][ \t]*sourceMappingURL[ \t]*=[^\r\n]*|(?:\r?\n)?[ \t]*\/\*[#@][ \t]*sourceMappingURL[ \t]*=[\s\S]*?\*\/)[ \t]*(?:\r?\n)?$/u, "");
819
+ if (stripped === result) return result;
820
+ result = stripped;
821
+ }
822
+ }
823
+ function containsSourceMapReference(source) {
824
+ return /(?:^|\n)\s*\/\/[#@]\s*sourceMappingURL\s*=\s*\S+\s*(?:\n|$)/mu.test(source) || /\/\*[#@]\s*sourceMappingURL\s*=\s*[^*]+\*\//mu.test(source);
825
+ }
826
+ async function collectOutputFiles(outputRoot, directory = outputRoot.path) {
827
+ const entries = await readOutputDirectory(directory, outputRoot);
828
+ return (await Promise.all(entries.map(async (entry) => {
829
+ const path = resolve(directory, entry.name);
830
+ if (entry.isSymbolicLink()) throw new Error("Visual Review Next.js build output must not contain symbolic links");
831
+ if (entry.isDirectory()) return collectOutputFiles(outputRoot, path);
832
+ if (entry.isFile()) return [path];
833
+ return [];
834
+ }))).flat();
835
+ }
836
+ async function removeSourceMapFiles(outputRoot, directory = outputRoot.path) {
837
+ const entries = await readOutputDirectory(directory, outputRoot);
838
+ const failures = [];
839
+ await Promise.all(entries.map(async (entry) => {
840
+ const path = resolve(directory, entry.name);
841
+ try {
842
+ if (entry.isDirectory()) await removeSourceMapFiles(outputRoot, path);
843
+ else if (entry.isSymbolicLink()) throw new Error("Visual Review Next.js build output must not contain symbolic links");
844
+ else if (entry.isFile() && entry.name.endsWith(".map")) await unlinkRegularFile(path, outputRoot);
845
+ } catch (error) {
846
+ failures.push(error);
847
+ }
848
+ }));
849
+ if (failures.length > 0) throw new AggregateError(failures, "Could not remove all Next.js source maps");
850
+ }
851
+ async function readRegularFile(path, root) {
852
+ const { handle } = await openRegularFile(path, root, constants.O_RDONLY);
853
+ try {
854
+ return await handle.readFile();
855
+ } finally {
856
+ await handle.close();
857
+ }
858
+ }
859
+ async function writeRegularFile(path, value, root) {
860
+ const { handle } = await openRegularFile(path, root, constants.O_WRONLY);
861
+ try {
862
+ await handle.truncate(0);
863
+ await handle.writeFile(value, "utf8");
864
+ } finally {
865
+ await handle.close();
866
+ }
867
+ }
868
+ async function unlinkRegularFile(path, root) {
869
+ const parent = dirname(path);
870
+ const parentMetadata = await validateOutputDirectory(parent, root);
871
+ const { handle, metadata } = await openRegularFile(path, root, constants.O_WRONLY);
872
+ try {
873
+ await handle.truncate(0);
874
+ } finally {
875
+ await handle.close();
876
+ }
877
+ const [current, currentParent] = await Promise.all([lstat(path), validateOutputDirectory(parent, root)]);
878
+ if (current.isSymbolicLink() || !current.isFile() || !sameFile(current, metadata) || !sameFile(currentParent, parentMetadata)) throw new Error("Visual Review Next.js source-map output changed during cleanup");
879
+ await unlink(path);
880
+ }
881
+ function browserGeneratedPath(outputPath, basePath, assetPrefix) {
882
+ const staticPath = outputPath.slice(7);
883
+ return `${normalizeNextPublicPrefix(assetPrefix || basePath || "")}/_next/static/${staticPath}`.replace(/\/{2,}/gu, "/");
884
+ }
885
+ function normalizeNextPublicPrefix(value) {
886
+ if (value.length > 2048 || value.trim() !== value || value.includes("\\") || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(value)) throw new Error("Visual Review Next.js asset prefix is unsafe");
887
+ if (!value) return "";
888
+ let url;
889
+ let rawPath;
890
+ if (value.startsWith("//")) {
891
+ if (value.startsWith("///")) throw new Error("Visual Review Next.js asset prefix URL is invalid");
892
+ url = parseNextPublicUrl(`https:${value}`);
893
+ rawPath = rawUrlPath(value, 2);
894
+ } else if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)) {
895
+ if (!/^https?:\/\//iu.test(value)) throw new Error("Visual Review Next.js asset prefix must use HTTP or HTTPS");
896
+ url = parseNextPublicUrl(value);
897
+ rawPath = rawUrlPath(value, value.indexOf("://") + 3);
898
+ } else {
899
+ if (!value.startsWith("/")) throw new Error("Visual Review Next.js asset prefix path must be absolute");
900
+ if (value.includes("?") || value.includes("#")) throw new Error("Visual Review Next.js asset prefix must not contain query or fragment");
901
+ rawPath = value;
902
+ url = new URL(value, "https://visual-review.invalid");
903
+ }
904
+ if (url.username || url.password || url.search || url.hash) throw new Error("Visual Review Next.js asset prefix must not contain credentials, query, or fragment");
905
+ assertSafePublicPath(rawPath);
906
+ const path = url.pathname;
907
+ if (!path || path === "/") return "";
908
+ return `/${path.replace(/^\/+|\/+$/gu, "")}`;
909
+ }
910
+ function parseNextPublicUrl(value) {
911
+ let url;
912
+ try {
913
+ url = new URL(value);
914
+ } catch {
915
+ throw new Error("Visual Review Next.js asset prefix URL is invalid");
916
+ }
917
+ if (url.protocol !== "http:" && url.protocol !== "https:" || !url.hostname) throw new Error("Visual Review Next.js asset prefix must use HTTP or HTTPS");
918
+ return url;
919
+ }
920
+ function rawUrlPath(value, authorityStart) {
921
+ const pathStart = value.indexOf("/", authorityStart);
922
+ if (pathStart < 0) return "/";
923
+ const queryStart = value.search(/[?#]/u);
924
+ return value.slice(pathStart, queryStart >= pathStart ? queryStart : void 0);
925
+ }
926
+ function assertSafePublicPath(value) {
927
+ let decoded = value;
928
+ let stable = false;
929
+ for (let iteration = 0; iteration < 8; iteration += 1) {
930
+ let next;
931
+ try {
932
+ next = decodeURIComponent(decoded);
933
+ } catch {
934
+ throw new Error("Visual Review Next.js asset prefix contains invalid encoding");
935
+ }
936
+ if (next === decoded) {
937
+ stable = true;
938
+ break;
939
+ }
940
+ decoded = next;
941
+ }
942
+ if (!stable || !decoded.startsWith("/") || decoded.includes("\\") || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(decoded) || decoded.split("/").some((segment) => segment === "." || segment === "..")) throw new Error("Visual Review Next.js asset prefix path is unsafe");
943
+ }
944
+ function serverGeneratedPath(projectRoot, generatedFile) {
945
+ const path = relative(resolve(projectRoot), resolve(generatedFile));
946
+ if (!path || path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new Error("Visual Review Next.js server output escaped the project root");
947
+ return `/${path.split(sep).join("/")}`;
948
+ }
949
+ function outputRelativePath(distDir, path) {
950
+ assertInside$1(distDir, path);
951
+ return relative(distDir, path).split(sep).join("/");
952
+ }
953
+ function assertBuildOutput(projectRoot, distDir) {
954
+ const path = relative(projectRoot, distDir);
955
+ if (!path || path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new Error("Visual Review Next.js distDir must be below the project root");
956
+ }
957
+ async function resolveBuildOutputDirectory(projectRoot, distDir) {
958
+ assertBuildOutput(projectRoot, distDir);
959
+ const before = await lstat(distDir);
960
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error("Visual Review Next.js distDir must be a real directory");
961
+ const [canonicalProjectRoot, canonicalDistDir] = await Promise.all([realpath(projectRoot), realpath(distDir)]);
962
+ assertBuildOutput(canonicalProjectRoot, canonicalDistDir);
963
+ const after = await lstat(distDir);
964
+ if (!after.isDirectory() || after.isSymbolicLink() || !sameFile(before, after)) throw new Error("Visual Review Next.js distDir changed during validation");
965
+ return {
966
+ path: distDir,
967
+ canonicalPath: canonicalDistDir,
968
+ device: after.dev,
969
+ inode: after.ino
970
+ };
971
+ }
972
+ async function assertOutputRootUnchanged(root) {
973
+ const metadata = await lstat(root.path);
974
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("Visual Review Next.js distDir must be a real directory");
975
+ if (metadata.dev !== root.device || metadata.ino !== root.inode) throw new Error("Visual Review Next.js distDir changed during source-map processing");
976
+ if (await realpath(root.path) !== root.canonicalPath) throw new Error("Visual Review Next.js distDir changed during source-map processing");
977
+ }
978
+ async function readOutputDirectory(directory, root) {
979
+ const before = await validateOutputDirectory(directory, root);
980
+ const entries = await readdir(directory, { withFileTypes: true });
981
+ if (!sameFile(before, await validateOutputDirectory(directory, root))) throw new Error("Visual Review Next.js output directory changed while reading");
982
+ return entries;
983
+ }
984
+ async function validateOutputDirectory(directory, root) {
985
+ assertAtOrInside(root.path, directory);
986
+ await assertOutputRootUnchanged(root);
987
+ const before = await lstat(directory);
988
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error("Visual Review Next.js build output must not contain symbolic links");
989
+ const canonical = await realpath(directory);
990
+ assertAtOrInside(root.canonicalPath, canonical);
991
+ const after = await lstat(directory);
992
+ if (!after.isDirectory() || after.isSymbolicLink() || !sameFile(before, after)) throw new Error("Visual Review Next.js output directory changed during validation");
993
+ await assertOutputRootUnchanged(root);
994
+ return after;
995
+ }
996
+ async function openRegularFile(path, root, flags) {
997
+ assertInside$1(root.path, path);
998
+ await assertOutputRootUnchanged(root);
999
+ const parent = dirname(path);
1000
+ const parentMetadata = await validateOutputDirectory(parent, root);
1001
+ const before = await lstat(path);
1002
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("Visual Review Next.js source-map output must be a regular file");
1003
+ const canonical = await realpath(path);
1004
+ assertInside$1(root.canonicalPath, canonical);
1005
+ const handle = await open(path, flags | constants.O_NOFOLLOW);
1006
+ try {
1007
+ const metadata = await handle.stat();
1008
+ const currentParent = await validateOutputDirectory(parent, root);
1009
+ if (!metadata.isFile() || !sameFile(before, metadata) || !sameFile(parentMetadata, currentParent)) throw new Error("Visual Review Next.js source-map output changed while opening");
1010
+ return {
1011
+ handle,
1012
+ metadata
1013
+ };
1014
+ } catch (error) {
1015
+ await handle.close();
1016
+ throw error;
1017
+ }
1018
+ }
1019
+ function sameFile(left, right) {
1020
+ return left.dev === right.dev && left.ino === right.ino;
1021
+ }
1022
+ function assertAtOrInside(root, path) {
1023
+ const candidate = relative(resolve(root), resolve(path));
1024
+ if (candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) throw new Error("Visual Review Next.js source-map path escaped the build output");
1025
+ }
1026
+ function assertInside$1(root, path) {
1027
+ const candidate = relative(resolve(root), resolve(path));
1028
+ if (!candidate || candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) throw new Error("Visual Review Next.js source-map path escaped the build output");
1029
+ }
1030
+ function sha256(bytes) {
1031
+ return createHash("sha256").update(bytes).digest("hex");
1032
+ }
1033
+ function isRecord$1(value) {
1034
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1035
+ }
1036
+ function isNodeError$1(value) {
1037
+ return value instanceof Error;
1038
+ }
1039
+ //#endregion
1040
+ //#region src/source-index.ts
1041
+ const VISUAL_REVIEW_SOURCE_MARKER_ATTRIBUTE = "data-visual-review-source";
1042
+ const VISUAL_REVIEW_SOURCE_COORDINATE_ATTRIBUTE = "data-visual-review-source-coordinate";
1043
+ const VISUAL_REVIEW_SOURCE_INDEX_ATTRIBUTE = "data-visual-review-source-index";
1044
+ const SOURCE_MARKER_PREFIX = "vr1_";
1045
+ const SOURCE_COORDINATE_PATTERN = /vrc1_([0-9a-f]{32})/gu;
1046
+ const SOURCE_INDEX_FILENAME_PATTERN = /^visual-review-sources-[0-9a-f]{16}\.json$/u;
1047
+ const SOURCE_INDEX_REFERENCE_SCAN_PATTERN = /\/_next\/static\/(visual-review-sources-[0-9a-f]{16}\.json)(?![A-Za-z0-9._-])/gu;
1048
+ const MAX_SOURCE_INDEX_ENTRIES = 1e5;
1049
+ const MAX_SOURCE_INDEX_BYTES = 8 * 1024 * 1024;
1050
+ const MAX_EDGE_MANIFEST_BYTES = 5 * 1024 * 1024;
1051
+ const MAX_EDGE_MANIFEST_FILES = 5e4;
1052
+ const MAX_NEXT_SOURCE_INDEX_FILES = 16;
1053
+ const VISUAL_REVIEW_SOURCE_SCAN_LIMITS = Object.freeze({
1054
+ maxDirectoryEntries: 1e5,
1055
+ maxFiles: 5e4,
1056
+ maxJavaScriptFiles: 2e4,
1057
+ maxJavaScriptFileBytes: 32 * 1024 * 1024,
1058
+ maxJavaScriptBytes: 512 * 1024 * 1024
1059
+ });
1060
+ function visualReviewSourceMarker(digest) {
1061
+ if (!/^[0-9a-f]{32}$/u.test(digest)) throw new Error("Visual Review source marker digest is invalid");
1062
+ return `${SOURCE_MARKER_PREFIX}${digest}`;
1063
+ }
1064
+ function visualReviewSourceIndexFilename(buildId, markerSalt) {
1065
+ if (!/^[0-9a-f]{64}$/u.test(markerSalt)) throw new Error("Visual Review source index marker salt is invalid");
1066
+ return `visual-review-sources-${createHmac("sha256", Buffer.from(markerSalt, "hex")).update("visual-review-source-index\0").update(buildId).digest("hex").slice(0, 16)}.json`;
1067
+ }
1068
+ function viteSourceIndexUrl(base, filename) {
1069
+ if (base === "" || base === "." || base === "./") return `./.visual-review/${filename}`;
1070
+ return `${normalizeVitePublicPrefix(base)}/.visual-review/${filename}`.replace(/\/{2,}/gu, "/");
1071
+ }
1072
+ function viteGeneratedAssetPath(base, filename) {
1073
+ return `${normalizeVitePublicPrefix(base)}/${normalizeGeneratedFilename(filename)}`.replace(/\/{2,}/gu, "/");
1074
+ }
1075
+ function nextSourceIndexUrl(basePath, filename) {
1076
+ return `${normalizeNextPublicPrefix(basePath ?? "")}/_next/static/${filename}`.replace(/\/{2,}/gu, "/");
1077
+ }
1078
+ function createVisualReviewSourceIndexAccumulator() {
1079
+ return {
1080
+ coordinateCount: 0,
1081
+ index: {
1082
+ schemaVersion: 1,
1083
+ entries: Object.create(null)
1084
+ },
1085
+ locatorKeys: /* @__PURE__ */ new Map()
1086
+ };
1087
+ }
1088
+ function appendVisualReviewSourceIndexAsset(accumulator, asset) {
1089
+ let line1 = 1;
1090
+ let lineStart = 0;
1091
+ let nextNewline = asset.code.indexOf("\n");
1092
+ for (const match of asset.code.matchAll(SOURCE_COORDINATE_PATTERN)) {
1093
+ const digest = match[1];
1094
+ if (!digest) continue;
1095
+ const marker = visualReviewSourceMarker(digest);
1096
+ const index = match.index;
1097
+ while (nextNewline !== -1 && nextNewline < index) {
1098
+ line1 += 1;
1099
+ lineStart = nextNewline + 1;
1100
+ nextNewline = asset.code.indexOf("\n", lineStart);
1101
+ }
1102
+ accumulator.coordinateCount += 1;
1103
+ if (accumulator.coordinateCount > MAX_SOURCE_INDEX_ENTRIES) throw new Error("Visual Review source index exceeds 100,000 generated markers");
1104
+ const locator = {
1105
+ assetPath: asset.assetPath,
1106
+ line1,
1107
+ column0: index - lineStart,
1108
+ runtime: asset.runtime
1109
+ };
1110
+ const current = accumulator.index.entries[marker] ?? [];
1111
+ let locatorKeys = accumulator.locatorKeys.get(marker);
1112
+ if (!locatorKeys) {
1113
+ locatorKeys = /* @__PURE__ */ new Set();
1114
+ accumulator.locatorKeys.set(marker, locatorKeys);
1115
+ }
1116
+ const locatorKey = JSON.stringify([
1117
+ locator.assetPath,
1118
+ locator.line1,
1119
+ locator.column0,
1120
+ locator.runtime
1121
+ ]);
1122
+ if (locatorKeys.has(locatorKey)) continue;
1123
+ if (current.length >= 64) throw new Error("Visual Review source marker exceeds 64 generated locators");
1124
+ locatorKeys.add(locatorKey);
1125
+ current.push(locator);
1126
+ accumulator.index.entries[marker] = current;
1127
+ }
1128
+ }
1129
+ function serializeVisualReviewSourceIndex(index) {
1130
+ const serialized = `${JSON.stringify(index)}\n`;
1131
+ if (Buffer.byteLength(serialized) > MAX_SOURCE_INDEX_BYTES) throw new Error("Visual Review source index exceeds 8 MiB");
1132
+ return serialized;
1133
+ }
1134
+ function createVisualReviewSourceScanBudget() {
1135
+ return {
1136
+ directoryEntries: 0,
1137
+ files: 0,
1138
+ javascriptBytes: 0,
1139
+ javascriptFiles: 0
1140
+ };
1141
+ }
1142
+ function reserveVisualReviewSourceScanEntry(budget, input) {
1143
+ assertSourceScanBudget(budget);
1144
+ budget.directoryEntries += 1;
1145
+ if (budget.directoryEntries > VISUAL_REVIEW_SOURCE_SCAN_LIMITS.maxDirectoryEntries) throw new Error("Visual Review source scan exceeds 100,000 directory entries");
1146
+ if (input.kind === "directory") return;
1147
+ budget.files += 1;
1148
+ if (budget.files > VISUAL_REVIEW_SOURCE_SCAN_LIMITS.maxFiles) throw new Error("Visual Review source scan exceeds 50,000 files");
1149
+ if (input.kind === "file") return;
1150
+ const size = input.size;
1151
+ if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0) throw new Error("Visual Review source scan received an invalid JavaScript file size");
1152
+ if (size > VISUAL_REVIEW_SOURCE_SCAN_LIMITS.maxJavaScriptFileBytes) throw new Error("Visual Review source scan exceeds 32 MiB for one JavaScript file");
1153
+ budget.javascriptFiles += 1;
1154
+ budget.javascriptBytes += size;
1155
+ if (budget.javascriptFiles > VISUAL_REVIEW_SOURCE_SCAN_LIMITS.maxJavaScriptFiles) throw new Error("Visual Review source scan exceeds 20,000 JavaScript files");
1156
+ if (!Number.isSafeInteger(budget.javascriptBytes) || budget.javascriptBytes > VISUAL_REVIEW_SOURCE_SCAN_LIMITS.maxJavaScriptBytes) throw new Error("Visual Review source scan exceeds 512 MiB of JavaScript");
1157
+ }
1158
+ function assertSourceScanBudget(budget) {
1159
+ for (const value of [
1160
+ budget.directoryEntries,
1161
+ budget.files,
1162
+ budget.javascriptBytes,
1163
+ budget.javascriptFiles
1164
+ ]) if (!Number.isSafeInteger(value) || value < 0) throw new Error("Visual Review source scan budget is invalid");
1165
+ }
1166
+ async function writeNextVisualReviewSourceIndex(input) {
1167
+ const projectRoot = resolve(input.projectDir);
1168
+ const outputRoot = resolve(projectRoot, input.distDir);
1169
+ assertInside(projectRoot, outputRoot);
1170
+ const staticRoot = resolve(outputRoot, "static");
1171
+ const serverRoot = resolve(outputRoot, "server");
1172
+ assertInside(outputRoot, staticRoot);
1173
+ assertInside(outputRoot, serverRoot);
1174
+ const budget = createVisualReviewSourceScanBudget();
1175
+ const staticFiles = await collectJavaScriptFiles(staticRoot, budget);
1176
+ const serverFiles = await collectJavaScriptFiles(serverRoot, budget);
1177
+ const edgeFiles = await readNextEdgeRuntimeFiles(serverRoot);
1178
+ const prefix = normalizeNextPublicPrefix(input.assetPrefix || input.basePath || "");
1179
+ const accumulator = createVisualReviewSourceIndexAccumulator();
1180
+ const filenames = createNextSourceIndexFilenameSet(input.filename);
1181
+ for (const file of staticFiles) {
1182
+ const code = await readCapturedFile(file, "Visual Review JavaScript output changed while scanning");
1183
+ appendVisualReviewSourceIndexAsset(accumulator, {
1184
+ assetPath: `${prefix}/_next/${relative(outputRoot, file.path).split(sep).join("/")}`.replace(/\/{2,}/gu, "/"),
1185
+ code,
1186
+ runtime: "browser"
1187
+ });
1188
+ addNextSourceIndexReferences(filenames, code);
1189
+ }
1190
+ for (const file of serverFiles) {
1191
+ const code = await readCapturedFile(file, "Visual Review JavaScript output changed while scanning");
1192
+ const outputPath = relative(outputRoot, file.path).split(sep).join("/");
1193
+ appendVisualReviewSourceIndexAsset(accumulator, {
1194
+ assetPath: `/${relative(projectRoot, file.path).split(sep).join("/")}`,
1195
+ code,
1196
+ runtime: nextServerRuntime(outputPath, edgeFiles)
1197
+ });
1198
+ addNextSourceIndexReferences(filenames, code);
1199
+ }
1200
+ const serialized = serializeVisualReviewSourceIndex(accumulator.index);
1201
+ await mkdir(staticRoot, { recursive: true });
1202
+ for (const filename of [...filenames].sort()) await writeAtomicNextSourceIndex(staticRoot, filename, serialized);
1203
+ }
1204
+ async function writeViteVisualReviewSourceIndex(input) {
1205
+ const projectRoot = resolve(input.projectRoot);
1206
+ const outputRoot = resolve(input.outDir);
1207
+ assertInside(projectRoot, outputRoot);
1208
+ const files = await collectJavaScriptFiles(outputRoot, createVisualReviewSourceScanBudget());
1209
+ const accumulator = createVisualReviewSourceIndexAccumulator();
1210
+ for (const file of files) appendVisualReviewSourceIndexAsset(accumulator, {
1211
+ assetPath: viteGeneratedAssetPath(input.base, relative(outputRoot, file.path).split(sep).join("/")),
1212
+ code: await readCapturedFile(file, "Visual Review JavaScript output changed while scanning"),
1213
+ runtime: "browser"
1214
+ });
1215
+ const destinationRoot = resolve(outputRoot, ".visual-review");
1216
+ const destination = resolve(destinationRoot, input.filename);
1217
+ assertInside(outputRoot, destinationRoot);
1218
+ assertInside(destinationRoot, destination);
1219
+ await mkdir(destinationRoot, { recursive: true });
1220
+ await writeFile(destination, serializeVisualReviewSourceIndex(accumulator.index), { flag: "w" });
1221
+ }
1222
+ async function collectJavaScriptFiles(directory, budget) {
1223
+ const metadata = await lstat(directory, { bigint: true });
1224
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("Visual Review JavaScript output directory is invalid");
1225
+ const files = [];
1226
+ await collect(directory, files, budget);
1227
+ files.sort((left, right) => left.path.localeCompare(right.path));
1228
+ return files;
1229
+ }
1230
+ async function readNextEdgeRuntimeFiles(serverRoot) {
1231
+ const manifestPath = resolve(serverRoot, "middleware-manifest.json");
1232
+ let manifest;
1233
+ try {
1234
+ manifest = await captureRegularFile(manifestPath, MAX_EDGE_MANIFEST_BYTES, "Visual Review Next.js middleware manifest is invalid");
1235
+ } catch (error) {
1236
+ if (isNodeError(error) && error.code === "ENOENT") return /* @__PURE__ */ new Set();
1237
+ throw error;
1238
+ }
1239
+ let value;
1240
+ try {
1241
+ value = JSON.parse(await readCapturedFile(manifest, "Visual Review Next.js middleware manifest is invalid"));
1242
+ } catch {
1243
+ throw new Error("Visual Review Next.js middleware manifest is invalid");
1244
+ }
1245
+ if (!isRecord(value)) throw new Error("Visual Review Next.js middleware manifest is invalid");
1246
+ const files = /* @__PURE__ */ new Set();
1247
+ for (const groupName of ["middleware", "functions"]) {
1248
+ const group = value[groupName];
1249
+ if (group === void 0) continue;
1250
+ if (!isRecord(group)) throw new Error("Visual Review Next.js middleware manifest is invalid");
1251
+ for (const entry of Object.values(group)) {
1252
+ if (!isRecord(entry) || !Array.isArray(entry.files)) throw new Error("Visual Review Next.js middleware manifest is invalid");
1253
+ for (const file of entry.files) {
1254
+ if (typeof file !== "string") throw new Error("Visual Review Next.js middleware manifest is invalid");
1255
+ files.add(normalizeNextOutputPath(file));
1256
+ if (files.size > MAX_EDGE_MANIFEST_FILES) throw new Error("Visual Review Next.js middleware manifest is invalid");
1257
+ }
1258
+ }
1259
+ }
1260
+ return files;
1261
+ }
1262
+ function nextServerRuntime(outputPath, edgeFiles) {
1263
+ if (outputPath.startsWith("server/edge/") || outputPath === "server/edge-runtime-webpack.js" || edgeFiles.has(outputPath)) return "server-edge";
1264
+ return "server-node";
1265
+ }
1266
+ function normalizeNextOutputPath(value) {
1267
+ const normalized = value.replaceAll("\\", "/").replace(/^\/+/, "");
1268
+ if (!normalized || normalized.length > 4096 || normalized.split("/").some((segment) => segment === "..") || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(normalized)) throw new Error("Visual Review Next.js middleware manifest is invalid");
1269
+ return normalized;
1270
+ }
1271
+ function normalizeVitePublicPrefix(base) {
1272
+ if (base === "" || base === "." || base === "./") return "";
1273
+ if (!base.startsWith("/") && !base.startsWith("//") && !/^https?:\/\//iu.test(base)) throw new Error("Visual Review Vite base must be root-absolute, HTTP(S), or ./");
1274
+ return normalizeNextPublicPrefix(base);
1275
+ }
1276
+ function normalizeGeneratedFilename(filename) {
1277
+ const normalized = filename.replace(/^\/+/, "");
1278
+ if (!normalized || normalized.length > 4096 || normalized.includes("\\") || normalized.includes("?") || normalized.includes("#") || normalized.split("/").some((segment) => !segment || segment === "." || segment === "..") || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(normalized)) throw new Error("Visual Review generated asset filename is unsafe");
1279
+ return normalized;
1280
+ }
1281
+ function createNextSourceIndexFilenameSet(configuredFilename) {
1282
+ if (!SOURCE_INDEX_FILENAME_PATTERN.test(configuredFilename)) throw new Error("Visual Review Next.js source index filename is invalid");
1283
+ return /* @__PURE__ */ new Set([configuredFilename]);
1284
+ }
1285
+ function addNextSourceIndexReferences(filenames, code) {
1286
+ for (const match of code.matchAll(SOURCE_INDEX_REFERENCE_SCAN_PATTERN)) {
1287
+ const filename = match[1];
1288
+ if (!filename) continue;
1289
+ filenames.add(filename);
1290
+ if (filenames.size > MAX_NEXT_SOURCE_INDEX_FILES) throw new Error("Visual Review Next.js build references too many source indexes");
1291
+ }
1292
+ }
1293
+ async function writeAtomicNextSourceIndex(staticRoot, filename, serialized) {
1294
+ const destination = resolve(staticRoot, filename);
1295
+ const temporary = resolve(staticRoot, `.${filename}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
1296
+ assertInside(staticRoot, destination);
1297
+ assertInside(staticRoot, temporary);
1298
+ try {
1299
+ await writeFile(temporary, serialized, {
1300
+ flag: "wx",
1301
+ mode: 420
1302
+ });
1303
+ await rename(temporary, destination);
1304
+ } finally {
1305
+ await unlink(temporary).catch(() => void 0);
1306
+ }
1307
+ }
1308
+ function isRecord(value) {
1309
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1310
+ }
1311
+ function isNodeError(value) {
1312
+ return value instanceof Error && "code" in value;
1313
+ }
1314
+ async function collect(directory, files, budget) {
1315
+ const directories = [directory];
1316
+ while (directories.length > 0) {
1317
+ const current = directories.pop();
1318
+ if (!current) continue;
1319
+ const before = await lstat(current, { bigint: true });
1320
+ if (!before.isDirectory() || before.isSymbolicLink()) throw new Error("Visual Review source scan does not follow symbolic links");
1321
+ const handle = await opendir(current);
1322
+ for await (const entry of handle) {
1323
+ const path = join(current, entry.name);
1324
+ const metadata = await lstat(path, { bigint: true });
1325
+ if (metadata.isSymbolicLink()) throw new Error("Visual Review source scan does not follow symbolic links");
1326
+ if (metadata.isDirectory()) {
1327
+ reserveVisualReviewSourceScanEntry(budget, { kind: "directory" });
1328
+ directories.push(path);
1329
+ continue;
1330
+ }
1331
+ if (!metadata.isFile()) throw new Error("Visual Review source scan only accepts regular files and directories");
1332
+ if (/\.(?:c|m)?js$/iu.test(entry.name)) {
1333
+ const size = safeFileSize(metadata.size);
1334
+ reserveVisualReviewSourceScanEntry(budget, {
1335
+ kind: "javascript",
1336
+ size
1337
+ });
1338
+ files.push(capturedFile(path, metadata, size));
1339
+ } else reserveVisualReviewSourceScanEntry(budget, { kind: "file" });
1340
+ }
1341
+ if (!sameCapturedDirectory(before, await lstat(current, { bigint: true }))) throw new Error("Visual Review source directory changed while scanning");
1342
+ }
1343
+ }
1344
+ async function captureRegularFile(path, maxBytes, message) {
1345
+ const metadata = await lstat(path, { bigint: true });
1346
+ if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(message);
1347
+ const size = safeFileSize(metadata.size);
1348
+ if (size > maxBytes) throw new Error(message);
1349
+ return capturedFile(path, metadata, size);
1350
+ }
1351
+ function capturedFile(path, metadata, size) {
1352
+ return {
1353
+ ctimeNs: metadata.ctimeNs,
1354
+ dev: metadata.dev,
1355
+ ino: metadata.ino,
1356
+ mtimeNs: metadata.mtimeNs,
1357
+ path,
1358
+ size
1359
+ };
1360
+ }
1361
+ function safeFileSize(size) {
1362
+ if (size < 0n || size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Visual Review source scan received an invalid file size");
1363
+ return Number(size);
1364
+ }
1365
+ async function readCapturedFile(file, message) {
1366
+ const noFollow = constants.O_NOFOLLOW;
1367
+ if (!Number.isInteger(noFollow)) throw new Error("Visual Review source scan requires no-follow file support");
1368
+ const handle = await open(file.path, constants.O_RDONLY | noFollow);
1369
+ try {
1370
+ if (!sameCapturedFile(file, await handle.stat({ bigint: true }))) throw new Error(message);
1371
+ const bytes = Buffer.allocUnsafe(file.size);
1372
+ let offset = 0;
1373
+ while (offset < bytes.byteLength) {
1374
+ const result = await handle.read(bytes, offset, Math.min(64 * 1024, bytes.byteLength - offset), offset);
1375
+ if (result.bytesRead === 0) throw new Error(message);
1376
+ offset += result.bytesRead;
1377
+ }
1378
+ const trailing = Buffer.allocUnsafe(1);
1379
+ const trailingRead = await handle.read(trailing, 0, 1, file.size);
1380
+ const after = await handle.stat({ bigint: true });
1381
+ if (trailingRead.bytesRead !== 0 || !sameCapturedFile(file, after)) throw new Error(message);
1382
+ return bytes.toString("utf8");
1383
+ } finally {
1384
+ await handle.close();
1385
+ }
1386
+ }
1387
+ function sameCapturedDirectory(left, right) {
1388
+ return right.isDirectory() && !right.isSymbolicLink() && left.dev === right.dev && left.ino === right.ino && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
1389
+ }
1390
+ function sameCapturedFile(captured, current) {
1391
+ return current.isFile() && current.dev === captured.dev && current.ino === captured.ino && current.size === BigInt(captured.size) && current.mtimeNs === captured.mtimeNs && current.ctimeNs === captured.ctimeNs;
1392
+ }
1393
+ function assertInside(root, path) {
1394
+ const candidate = relative(resolve(root), resolve(path));
1395
+ if (!candidate || candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) throw new Error("Visual Review source index escaped the build output");
1396
+ }
1397
+ //#endregion
1398
+ export { visualReviewSourceIndexFilename as a, writeNextVisualReviewSourceIndex as c, uploadNextSourceMaps as d, cleanupViteSourceMapOutput as f, nextSourceIndexUrl as i, writeViteVisualReviewSourceIndex as l, VISUAL_REVIEW_SOURCE_INDEX_ATTRIBUTE as n, visualReviewSourceMarker as o, uploadViteSourceMaps as p, VISUAL_REVIEW_SOURCE_MARKER_ATTRIBUTE as r, viteSourceIndexUrl as s, VISUAL_REVIEW_SOURCE_COORDINATE_ATTRIBUTE as t, cleanupNextSourceMapOutput as u };