@01.works/visual-review 0.2.0 → 0.4.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.
@@ -1,23 +1,17 @@
1
- import { t as resolveVisualReviewBuildIdentity } from "./build-identity-CgoY3-jy.js";
2
- import { a as visualReviewSourceIndexFilename, c as writeNextVisualReviewSourceIndex, d as uploadNextSourceMaps, i as nextSourceIndexUrl, u as cleanupNextSourceMapOutput } from "./source-index-BdWeiNHZ.js";
1
+ import { t as resolveVisualReviewBuildIdentity } from "./build-identity-DebYe8Qe.js";
3
2
  import { randomBytes } from "node:crypto";
4
- import { dirname, join, resolve } from "node:path";
5
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
- import { tmpdir } from "node:os";
3
+ import { resolve } from "node:path";
7
4
  import { fileURLToPath } from "node:url";
8
5
  //#region src/next-config.ts
9
6
  const BUILD_ID_LITERAL = "VISUAL_REVIEW_PUBLIC_BUILD_ID";
10
7
  const GIT_COMMIT_LITERAL = "VISUAL_REVIEW_PUBLIC_GIT_COMMIT";
11
8
  const BUILD_MODE_LITERAL = "VISUAL_REVIEW_PUBLIC_BUILD_MODE";
12
9
  const NEXT_BUILD_IDENTITY_RECORD = "VISUAL_REVIEW_INTERNAL_NEXT_BUILD_IDENTITY";
13
- const NEXT_MARKER_SALT_PATH = "VISUAL_REVIEW_INTERNAL_NEXT_MARKER_SALT_PATH";
14
10
  const FULL_GIT_COMMIT = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
15
11
  const PUBLIC_BUILD_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
16
12
  const DEPLOYMENT_VARIANT = /^[0-9a-f]{32}$/u;
17
13
  const GIT_FALLBACK_BUILD_ID = /^(git:([0-9a-f]{40}|[0-9a-f]{64}):deploy:([0-9a-f]{32}))$/u;
18
14
  const MAX_NEXT_BUILD_IDENTITY_RECORD_BYTES = 512;
19
- const trackedMarkerSaltPaths = /* @__PURE__ */ new Set();
20
- const VISUAL_REVIEW_HOOK = Symbol.for("01works.visual-review.next-source-map-hook");
21
15
  const VISUAL_REVIEW_CONFIG = Symbol.for("01works.visual-review.next-config");
22
16
  /**
23
17
  * Next evaluates next.config again in compiler workers. A Git fallback contains
@@ -90,24 +84,21 @@ function parseNextBuildIdentityRecord(value) {
90
84
  }
91
85
  function withVisualReview(nextConfig = {}, options = {}) {
92
86
  const identity = resolveNextVisualReviewBuildIdentity(options, process.env);
93
- const sourceMapUpload = resolveSourceMapUpload(options.sourceMaps, process.env);
94
87
  const sourceMetadataEnabled = options.sourceMetadata ?? true;
95
- const sourceMarkerSalt = sourceMetadataEnabled ? resolveBuildLocalMarkerSalt(process.env) : void 0;
96
88
  const injected = {
97
89
  [BUILD_ID_LITERAL]: identity.buildId,
98
90
  [GIT_COMMIT_LITERAL]: identity.gitCommit ?? "",
99
- [BUILD_MODE_LITERAL]: sourceMapUpload?.mode ?? configuredReleaseMode(options.sourceMaps) ?? inferReleaseMode(process.env)
91
+ [BUILD_MODE_LITERAL]: inferReleaseMode(process.env)
100
92
  };
101
93
  if (typeof nextConfig === "function") return function visualReviewNextConfig(...arguments_) {
102
94
  const resolved = nextConfig.apply(this, arguments_);
103
- return isPromiseLike(resolved) ? resolved.then((config) => configureNext(config, injected, identity, sourceMapUpload, sourceMetadataEnabled, sourceMarkerSalt)) : configureNext(resolved, injected, identity, sourceMapUpload, sourceMetadataEnabled, sourceMarkerSalt);
95
+ return isPromiseLike(resolved) ? resolved.then((config) => configureNext(config, injected, sourceMetadataEnabled)) : configureNext(resolved, injected, sourceMetadataEnabled);
104
96
  };
105
- return configureNext(nextConfig, injected, identity, sourceMapUpload, sourceMetadataEnabled, sourceMarkerSalt);
97
+ return configureNext(nextConfig, injected, sourceMetadataEnabled);
106
98
  }
107
- function configureNext(nextConfig, injected, identity, sourceMapUpload, sourceMetadataEnabled, sourceMarkerSalt) {
108
- const previousHookMetadata = getVisualReviewHookMetadata(nextConfig.compiler?.runAfterProductionCompile);
109
- const previouslyWrapped = isVisualReviewConfig(nextConfig) || previousHookMetadata !== void 0;
110
- assertReservedLiterals(nextConfig.env, injected, previouslyWrapped);
99
+ function configureNext(nextConfig, injected, sourceMetadataEnabled) {
100
+ const previousBundlerState = getVisualReviewBundlerState(nextConfig);
101
+ assertReservedLiterals(nextConfig.env, injected, previousBundlerState !== void 0);
111
102
  let configured = {
112
103
  ...nextConfig,
113
104
  env: {
@@ -115,84 +106,19 @@ function configureNext(nextConfig, injected, identity, sourceMapUpload, sourceMe
115
106
  ...injected
116
107
  }
117
108
  };
118
- if (previousHookMetadata) configured = restoreSourceMapConfiguration(configured, previousHookMetadata);
119
- if (!sourceMapUpload && !sourceMetadataEnabled) return markVisualReviewConfig(configured);
120
- const hookMetadata = previousHookMetadata ?? captureHookMetadata(nextConfig);
121
- if (sourceMapUpload && (nextConfig.productionBrowserSourceMaps === false || hookMetadata.productionBrowserSourceMaps.present && hookMetadata.productionBrowserSourceMaps.value === false)) throw new Error("Next productionBrowserSourceMaps=false conflicts with Visual Review source-map upload");
122
- if (sourceMapUpload && (nextConfig.experimental?.serverSourceMaps === false || hookMetadata.serverSourceMaps.present && hookMetadata.serverSourceMaps.value === false)) throw new Error("Next experimental.serverSourceMaps=false conflicts with Visual Review source-map upload");
123
- const sourceMetadata = sourceMetadataEnabled ? resolveNextSourceMetadata(identity, nextConfig.basePath, sourceMarkerSalt) : void 0;
124
- const sourceConfigured = sourceMetadata ? configureNextSourceMetadata(configured, hookMetadata, sourceMetadata) : configured;
125
- const compiler = sourceConfigured.compiler ?? {};
126
- return markVisualReviewConfig({
127
- ...sourceConfigured,
128
- ...sourceMapUpload ? { productionBrowserSourceMaps: true } : {},
129
- ...sourceMapUpload ? { experimental: {
130
- ...sourceConfigured.experimental,
131
- serverSourceMaps: true
132
- } } : {},
133
- compiler: {
134
- ...compiler,
135
- runAfterProductionCompile: composeProductionCompileHook(hookMetadata.originalHook, identity, sourceMapUpload, sourceMetadata, nextConfig.basePath, nextConfig.assetPrefix, hookMetadata)
136
- }
137
- });
138
- }
139
- function composeProductionCompileHook(existingHook, identity, sourceMapUpload, sourceMetadata, basePath, assetPrefix, metadata) {
140
- const hook = async function visualReviewAfterProductionCompile(metadata) {
141
- if (sourceMetadata) removeBuildLocalMarkerSalt(sourceMetadata.saltPath);
142
- if (existingHook) try {
143
- await existingHook.call(this, metadata);
144
- } catch (existingError) {
145
- if (!sourceMapUpload) throw existingError;
146
- try {
147
- await cleanupNextSourceMapOutput(metadata.projectDir, metadata.distDir);
148
- } catch (cleanupError) {
149
- throw new AggregateError([existingError, cleanupError], "Next.js production compile hook failed and Visual Review could not clean source maps");
150
- }
151
- throw existingError;
152
- }
153
- if (sourceMetadata) try {
154
- await writeNextVisualReviewSourceIndex({
155
- projectDir: metadata.projectDir,
156
- distDir: metadata.distDir,
157
- filename: sourceMetadata.filename,
158
- ...basePath ? { basePath } : {},
159
- ...assetPrefix ? { assetPrefix } : {}
160
- });
161
- } catch (sourceIndexError) {
162
- if (!sourceMapUpload) throw sourceIndexError;
163
- try {
164
- await cleanupNextSourceMapOutput(metadata.projectDir, metadata.distDir);
165
- } catch (cleanupError) {
166
- throw new AggregateError([sourceIndexError, cleanupError], "Visual Review source index failed and Next.js source maps could not be cleaned");
167
- }
168
- throw sourceIndexError;
169
- }
170
- if (!sourceMapUpload) return;
171
- await uploadNextSourceMaps({
172
- projectRoot: metadata.projectDir,
173
- distDir: metadata.distDir,
174
- token: sourceMapUpload.token,
175
- ...sourceMapUpload.serviceUrl ? { serviceUrl: sourceMapUpload.serviceUrl } : {},
176
- buildId: identity.buildId,
177
- gitCommit: identity.gitCommit,
178
- mode: sourceMapUpload.mode,
179
- ...basePath ? { basePath } : {},
180
- ...assetPrefix ? { assetPrefix } : {}
181
- });
182
- };
183
- Object.defineProperty(hook, VISUAL_REVIEW_HOOK, { value: metadata });
184
- return hook;
109
+ if (previousBundlerState) {
110
+ restoreProperty(configured, "turbopack", previousBundlerState.turbopack);
111
+ restoreProperty(configured, "webpack", previousBundlerState.webpack);
112
+ }
113
+ const bundlerState = previousBundlerState ?? captureBundlerState(nextConfig);
114
+ if (!sourceMetadataEnabled) return markVisualReviewConfig(configured, bundlerState);
115
+ return markVisualReviewConfig(configureNextSourceMetadata(configured, resolveNextSourceMetadata()), bundlerState);
185
116
  }
186
- function getVisualReviewHookMetadata(hook) {
187
- return hook?.[VISUAL_REVIEW_HOOK];
117
+ function getVisualReviewBundlerState(config) {
118
+ return config[VISUAL_REVIEW_CONFIG];
188
119
  }
189
- function captureHookMetadata(nextConfig) {
120
+ function captureBundlerState(nextConfig) {
190
121
  return {
191
- originalHook: nextConfig.compiler?.runAfterProductionCompile,
192
- compilerPresent: Object.prototype.hasOwnProperty.call(nextConfig, "compiler"),
193
- experimentalPresent: Object.prototype.hasOwnProperty.call(nextConfig, "experimental"),
194
- productionBrowserSourceMaps: propertyState(nextConfig, "productionBrowserSourceMaps"),
195
- serverSourceMaps: propertyState(nextConfig.experimental, "serverSourceMaps"),
196
122
  turbopack: propertyState(nextConfig, "turbopack"),
197
123
  webpack: propertyState(nextConfig, "webpack")
198
124
  };
@@ -203,97 +129,17 @@ function propertyState(value, key) {
203
129
  value: value?.[key]
204
130
  };
205
131
  }
206
- function restoreSourceMapConfiguration(configured, metadata) {
207
- if (!metadata) return configured;
208
- const restored = { ...configured };
209
- if (metadata.productionBrowserSourceMaps.present) restored.productionBrowserSourceMaps = metadata.productionBrowserSourceMaps.value;
210
- else delete restored.productionBrowserSourceMaps;
211
- const experimental = { ...restored.experimental ?? {} };
212
- if (metadata.serverSourceMaps.present) experimental.serverSourceMaps = metadata.serverSourceMaps.value;
213
- else delete experimental.serverSourceMaps;
214
- if (!metadata.experimentalPresent && Object.keys(experimental).length === 0) delete restored.experimental;
215
- else restored.experimental = experimental;
216
- const compiler = { ...restored.compiler ?? {} };
217
- if (metadata.originalHook) compiler.runAfterProductionCompile = metadata.originalHook;
218
- else delete compiler.runAfterProductionCompile;
219
- if (!metadata.compilerPresent && Object.keys(compiler).length === 0) delete restored.compiler;
220
- else restored.compiler = compiler;
221
- restoreProperty(restored, "turbopack", metadata.turbopack);
222
- restoreProperty(restored, "webpack", metadata.webpack);
223
- return restored;
224
- }
225
132
  function restoreProperty(target, key, state) {
226
133
  if (state.present) target[key] = state.value;
227
134
  else delete target[key];
228
135
  }
229
- function resolveNextSourceMetadata(identity, basePath, markerSalt) {
230
- if (!markerSalt) throw new Error("Visual Review source marker salt is unavailable");
231
- const filename = visualReviewSourceIndexFilename(identity.buildId, markerSalt.salt);
136
+ function resolveNextSourceMetadata() {
232
137
  const loaderUrl = new URL("./source-loader.js", import.meta.url);
233
- return {
234
- filename,
235
- indexUrl: nextSourceIndexUrl(basePath, filename),
236
- loader: loaderUrl.protocol === "file:" ? fileURLToPath(loaderUrl) : resolve(process.cwd(), "packages/review-client/dist/source-loader.js"),
237
- saltPath: markerSalt.path
238
- };
138
+ return { loader: loaderUrl.protocol === "file:" ? fileURLToPath(loaderUrl) : resolve(process.cwd(), "packages/review-client/dist/source-loader.js") };
239
139
  }
240
- /**
241
- * Generates the marker salt and persists it outside the project so compiler
242
- * workers can read it without the secret entering any build output. The file is
243
- * owner-only and is removed once the production compile hook has run.
244
- */
245
- function resolveBuildLocalMarkerSalt(env) {
246
- const inherited = env[NEXT_MARKER_SALT_PATH];
247
- if (env.NEXT_PRIVATE_BUILD_WORKER === "1" && env.IS_NEXT_WORKER === "true" && typeof inherited === "string" && inherited.length > 0) return {
248
- salt: readMarkerSaltFile(inherited),
249
- path: inherited
250
- };
251
- const salt = randomBytes(32).toString("hex");
252
- const path = join(mkdtempSync(join(tmpdir(), "visual-review-marker-salt-")), "salt");
253
- writeFileSync(path, salt, {
254
- encoding: "utf8",
255
- mode: 384
256
- });
257
- env[NEXT_MARKER_SALT_PATH] = path;
258
- trackBuildLocalMarkerSalt(path);
259
- return {
260
- salt,
261
- path
262
- };
263
- }
264
- function readMarkerSaltFile(saltPath) {
265
- const salt = readFileSync(saltPath, "utf8").trim();
266
- if (!/^[0-9a-f]{64}$/u.test(salt)) throw new Error("Visual Review inherited marker salt is invalid");
267
- return salt;
268
- }
269
- /**
270
- * `next dev` never reaches the production compile hook, and every compiler
271
- * worker evaluates the config in its own process, so exit is the only point
272
- * that reliably removes each salt file it created.
273
- */
274
- function trackBuildLocalMarkerSalt(saltPath) {
275
- if (trackedMarkerSaltPaths.size === 0) process.once("exit", () => {
276
- for (const tracked of trackedMarkerSaltPaths) removeMarkerSaltDirectory(tracked);
277
- });
278
- trackedMarkerSaltPaths.add(saltPath);
279
- }
280
- function removeBuildLocalMarkerSalt(saltPath) {
281
- trackedMarkerSaltPaths.delete(saltPath);
282
- removeMarkerSaltDirectory(saltPath);
283
- }
284
- function removeMarkerSaltDirectory(saltPath) {
285
- try {
286
- rmSync(dirname(saltPath), {
287
- recursive: true,
288
- force: true
289
- });
290
- } catch {}
291
- }
292
- function configureNextSourceMetadata(configured, metadata, sourceMetadata) {
140
+ function configureNextSourceMetadata(configured, sourceMetadata) {
293
141
  const bundler = resolveNextBundler();
294
142
  const next = { ...configured };
295
- restoreProperty(next, "turbopack", metadata.turbopack);
296
- restoreProperty(next, "webpack", metadata.webpack);
297
143
  if (bundler === "turbopack") next.turbopack = appendTurbopackRules(next.turbopack, sourceMetadata);
298
144
  else next.webpack = appendWebpackRule(next.webpack, sourceMetadata);
299
145
  return next;
@@ -318,10 +164,7 @@ function appendWebpackRule(existing, sourceMetadata) {
318
164
  test: /\.[cm]?[jt]sx?$/u,
319
165
  use: [{
320
166
  loader: sourceMetadata.loader,
321
- options: {
322
- indexUrl: sourceMetadata.indexUrl,
323
- saltPath: sourceMetadata.saltPath
324
- }
167
+ options: {}
325
168
  }]
326
169
  }]
327
170
  }
@@ -341,11 +184,7 @@ function appendTurbopackRules(existing, sourceMetadata) {
341
184
  condition: { all: [{ not: "foreign" }, "production"] },
342
185
  loaders: [{
343
186
  loader: sourceMetadata.loader,
344
- options: {
345
- compile: true,
346
- indexUrl: sourceMetadata.indexUrl,
347
- saltPath: sourceMetadata.saltPath
348
- }
187
+ options: { compile: true }
349
188
  }]
350
189
  };
351
190
  const current = rules[pattern];
@@ -362,28 +201,10 @@ function isPromiseLikeUnknown(value) {
362
201
  function isRecord(value) {
363
202
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
364
203
  }
365
- function isVisualReviewConfig(config) {
366
- return Boolean(config[VISUAL_REVIEW_CONFIG]);
367
- }
368
- function markVisualReviewConfig(config) {
369
- Object.defineProperty(config, VISUAL_REVIEW_CONFIG, { value: true });
204
+ function markVisualReviewConfig(config, bundlerState) {
205
+ Object.defineProperty(config, VISUAL_REVIEW_CONFIG, { value: bundlerState });
370
206
  return config;
371
207
  }
372
- function resolveSourceMapUpload(option, env) {
373
- if (option === false || typeof option === "object" && option.enabled === false) return;
374
- const configured = typeof option === "object" ? option : void 0;
375
- const token = configured?.token ?? env.VISUAL_REVIEW_SOURCE_MAP_TOKEN;
376
- if (!(option === true || configured !== void 0 || token !== void 0)) return void 0;
377
- if (typeof token !== "string" || token.length === 0 || token.length > 4096 || /\s/u.test(token)) throw new Error("Visual Review source-map upload requires VISUAL_REVIEW_SOURCE_MAP_TOKEN in the Node build environment");
378
- return {
379
- token,
380
- mode: configured?.mode ?? inferReleaseMode(env),
381
- ...configured?.serviceUrl ? { serviceUrl: configured.serviceUrl } : {}
382
- };
383
- }
384
- function configuredReleaseMode(option) {
385
- return typeof option === "object" ? option.mode : void 0;
386
- }
387
208
  function inferReleaseMode(env) {
388
209
  if (env.VERCEL_ENV === "preview") return "preview";
389
210
  if (env.VERCEL_ENV === "development") return "development";
package/dist/react.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./install-CyJ52cJi.js";import{useEffect as t,useRef as n}from"react";function r({activationParam:r,build:i,collaboration:a,developerTools:o,enabled:s,onError:c,onReady:l,runtime:u,serviceUrl:d,sourceContextResolver:f,styleNonce:p}){let m=n(c),h=n(l),g=i?.buildId,_=i?.gitCommit,v=i?.generatedAt,y=i?.mode,b=o?.reactGrab===!0,x=a?.cursors===!0,S=a?.chat===!0;return m.current=c,h.current=l,t(()=>{if(s!==!0)return;let t=e({activationParam:r,build:g===void 0&&_===void 0&&v===void 0&&y===void 0?void 0:{buildId:g,gitCommit:_,generatedAt:v,mode:y},collaboration:x||S?{cursors:x,chat:S}:void 0,developerTools:b?{reactGrab:!0}:void 0,enabled:s,onError:e=>m.current?.(e),onReady:e=>h.current?.(e),runtime:u,serviceUrl:d,sourceContextResolver:f,styleNonce:p});return()=>t.dispose()},[r,g,y,S,x,b,s,v,_,u,d,f,p]),null}r.displayName=`VisualReview`;export{r as VisualReview};
1
+ import{t as e}from"./install-x8QQBGO-.js";import{useEffect as t,useRef as n}from"react";function r({activationParam:r,build:i,collaboration:a,developerTools:o,enabled:s,onError:c,onReady:l,runtime:u,serviceUrl:d,sourceContextResolver:f,styleNonce:p}){let m=n(c),h=n(l),g=i?.buildId,_=i?.gitCommit,v=i?.generatedAt,y=i?.mode,b=o?.reactGrab===!0,x=a?.cursors===!0,S=a?.chat===!0;return m.current=c,h.current=l,t(()=>{if(s!==!0)return;let t=e({activationParam:r,build:g===void 0&&_===void 0&&v===void 0&&y===void 0?void 0:{buildId:g,gitCommit:_,generatedAt:v,mode:y},collaboration:x||S?{cursors:x,chat:S}:void 0,developerTools:b?{reactGrab:!0}:void 0,enabled:s,onError:e=>m.current?.(e),onReady:e=>h.current?.(e),runtime:u,serviceUrl:d,sourceContextResolver:f,styleNonce:p});return()=>t.dispose()},[r,g,y,S,x,b,s,v,_,u,d,f,p]),null}r.displayName=`VisualReview`;export{r as VisualReview};
@@ -1,12 +1,10 @@
1
- import { n as VISUAL_REVIEW_SOURCE_INDEX_ATTRIBUTE, o as visualReviewSourceMarker, r as VISUAL_REVIEW_SOURCE_MARKER_ATTRIBUTE, t as VISUAL_REVIEW_SOURCE_COORDINATE_ATTRIBUTE } from "./source-index-BdWeiNHZ.js";
2
- import { createHmac } from "node:crypto";
3
1
  import path, { win32 } from "node:path";
4
2
  import { parse } from "@babel/parser";
5
3
  import MagicString from "magic-string";
6
4
  //#region src/source-instrumentation-transform.ts
5
+ const VISUAL_REVIEW_EMBEDDED_SOURCE_ATTRIBUTE = "data-review-source";
7
6
  const SOURCE_MODULE = /\.(?:[cm]?[jt]sx?)$/iu;
8
7
  const SKIPPED_PATH_SEGMENT = /(?:^|\/)(?:node_modules|\.next|dist|build)(?:\/|$)/u;
9
- const MARKER_SALT = /^[0-9a-f]{64}$/u;
10
8
  /**
11
9
  * Adds opaque, build-local markers to intrinsic JSX nodes. Repository paths
12
10
  * are used only to derive a digest and never enter the transformed browser
@@ -14,7 +12,6 @@ const MARKER_SALT = /^[0-9a-f]{64}$/u;
14
12
  * framework adapter after bundling.
15
13
  */
16
14
  function instrumentVisualReviewSource(source, options) {
17
- assertMarkerSalt(options.markerSalt);
18
15
  const sourcePath = normalizedProjectSourcePath(options.projectRoot, options.id);
19
16
  if (!sourcePath) return null;
20
17
  const ast = parse(source, {
@@ -42,14 +39,8 @@ function instrumentVisualReviewSource(source, options) {
42
39
  for (const candidate of candidates) {
43
40
  const line1 = candidate.line1;
44
41
  const column1 = candidate.column0 + 1;
45
- const digest = createHmac("sha256", Buffer.from(options.markerSalt, "hex")).update(`${sourcePath}\0${line1}\0${column1}`).digest("hex").slice(0, 32);
46
- const marker = visualReviewSourceMarker(digest);
47
- const coordinate = `vrc1_${digest}`;
48
42
  const closingWidth = candidate.selfClosing ? 2 : 1;
49
- const insertionPoint = candidate.end - closingWidth;
50
- const metadata = ` ${VISUAL_REVIEW_SOURCE_MARKER_ATTRIBUTE}="${marker}" ${VISUAL_REVIEW_SOURCE_INDEX_ATTRIBUTE}="${options.indexUrl}"`;
51
- edits.overwrite(candidate.start, candidate.nameEnd, `${source.slice(candidate.start, candidate.nameEnd)} ${VISUAL_REVIEW_SOURCE_COORDINATE_ATTRIBUTE}="${coordinate}"`);
52
- edits.appendLeft(insertionPoint, metadata);
43
+ edits.appendLeft(candidate.end - closingWidth, ` ${VISUAL_REVIEW_EMBEDDED_SOURCE_ATTRIBUTE}="${escapeAttribute(`${sourcePath}:${line1}:${column1}`)}"`);
53
44
  markerCount += 1;
54
45
  }
55
46
  if (markerCount === 0) return null;
@@ -63,8 +54,12 @@ function instrumentVisualReviewSource(source, options) {
63
54
  markerCount
64
55
  };
65
56
  }
66
- function assertMarkerSalt(value) {
67
- if (!MARKER_SALT.test(value)) throw new Error("Visual Review source marker salt must be a lowercase 32-byte hex secret");
57
+ /**
58
+ * Source paths are project-relative and already validated, but an attribute
59
+ * value still has to survive being written into HTML.
60
+ */
61
+ function escapeAttribute(value) {
62
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
68
63
  }
69
64
  /**
70
65
  * Reports whether a module is first-party project source. Bundler rules already
@@ -101,7 +96,7 @@ function intrinsicJsxNameEnd(name) {
101
96
  return typeof candidate.end === "number" ? candidate.end : null;
102
97
  }
103
98
  function hasVisualReviewMarker(attributes) {
104
- return attributes?.some((attribute) => attribute.type === "JSXAttribute" && typeof attribute.name !== "string" && attribute.name?.type === "JSXIdentifier" && (attribute.name.name === "data-visual-review-source" || attribute.name.name === "data-visual-review-source-coordinate" || attribute.name.name === "data-visual-review-source-index" || attribute.name.name === "data-review-source")) ?? false;
99
+ return attributes?.some((attribute) => attribute.type === "JSXAttribute" && typeof attribute.name !== "string" && attribute.name?.type === "JSXIdentifier" && attribute.name.name === "data-review-source") ?? false;
105
100
  }
106
101
  function visitAst(node, visitor) {
107
102
  visitor(node);
@@ -1,29 +1,8 @@
1
- import { n as isVisualReviewProjectSource, t as instrumentVisualReviewSource } from "./source-instrumentation-transform-BwKYKlp0.js";
2
- import { readFileSync } from "node:fs";
1
+ import { n as isVisualReviewProjectSource, t as instrumentVisualReviewSource } from "./source-instrumentation-transform-qmiVeKpI.js";
3
2
  import { transformSync } from "@babel/core";
4
3
  import transformReactJsx from "@babel/plugin-transform-react-jsx";
5
4
  import transformTypeScript from "@babel/plugin-transform-typescript";
6
5
  //#region src/source-loader.ts
7
- const MARKER_SALT = /^[0-9a-f]{64}$/u;
8
- const saltCache = /* @__PURE__ */ new Map();
9
- /**
10
- * Reads a build-local marker salt. Next serializes loader options into
11
- * `required-server-files.json` and into Turbopack edge chunks, so the adapter
12
- * publishes only this path and keeps the secret in an owner-only file.
13
- */
14
- function readMarkerSaltFile(saltPath) {
15
- const cached = saltCache.get(saltPath);
16
- if (cached !== void 0) return cached;
17
- let contents;
18
- try {
19
- contents = readFileSync(saltPath, "utf8").trim();
20
- } catch {
21
- throw new Error("Visual Review source loader could not read the marker salt");
22
- }
23
- if (!MARKER_SALT.test(contents)) throw new Error("Visual Review source loader marker salt is invalid");
24
- saltCache.set(saltPath, contents);
25
- return contents;
26
- }
27
6
  function visualReviewSourceLoader(source, inputMap) {
28
7
  const done = this.async();
29
8
  try {
@@ -35,8 +14,6 @@ function visualReviewSourceLoader(source, inputMap) {
35
14
  }
36
15
  const transformed = instrumentVisualReviewSource(source, {
37
16
  id: this.resourcePath,
38
- indexUrl: options.indexUrl,
39
- markerSalt: options.markerSalt,
40
17
  projectRoot
41
18
  });
42
19
  const code = transformed?.code ?? source;
@@ -112,14 +89,9 @@ function isRecord(value) {
112
89
  function parseOptions(value) {
113
90
  if (!value || typeof value !== "object") throw new Error("Visual Review source loader options are missing");
114
91
  const options = value;
115
- if (typeof options.indexUrl !== "string" || !options.indexUrl.startsWith("/") || options.indexUrl.startsWith("//") || options.indexUrl.includes("\\")) throw new Error("Visual Review source loader index URL is invalid");
116
92
  if (options.projectRoot !== void 0 && typeof options.projectRoot !== "string") throw new Error("Visual Review source loader project root is invalid");
117
- const markerSalt = typeof options.saltPath === "string" ? readMarkerSaltFile(options.saltPath) : options.markerSalt;
118
- if (typeof markerSalt !== "string" || !MARKER_SALT.test(markerSalt)) throw new Error("Visual Review source loader marker salt is invalid");
119
93
  return {
120
94
  ...options.compile === true ? { compile: true } : {},
121
- indexUrl: options.indexUrl,
122
- markerSalt,
123
95
  ...typeof options.projectRoot === "string" ? { projectRoot: options.projectRoot } : {}
124
96
  };
125
97
  }
package/dist/vite.d.ts CHANGED
@@ -7,23 +7,15 @@ export interface VisualReviewBuildIdentityOptions {
7
7
  gitCommit?: string | null;
8
8
  }
9
9
 
10
- export interface VisualReviewViteSourceMapOptions {
11
- /** Enables upload explicitly. A source-map upload token is still required. */
12
- enabled?: boolean;
13
- /** Node-only build secret. Prefer VISUAL_REVIEW_SOURCE_MAP_TOKEN in CI. */
14
- token?: string;
15
- /** Hosted service origin. Primarily useful for local/self-hosted build services. */
16
- serviceUrl?: string;
17
- }
18
-
19
10
  export interface VisualReviewViteOptions extends VisualReviewBuildIdentityOptions {
20
- /** Uploads private hidden source maps and removes them from the deployment output. */
21
- sourceMaps?: boolean | VisualReviewViteSourceMapOptions;
22
- /** Adds opaque production JSX markers by default. Set `false` for identity-only output. */
11
+ /**
12
+ * Writes `data-review-source="src/App.tsx:42:7"` onto JSX elements by
13
+ * default. Set `false` for identity-only output.
14
+ */
23
15
  sourceMetadata?: boolean;
24
16
  }
25
17
 
26
- /** Injects public build provenance and optionally uploads private hidden source maps. */
18
+ /** Injects public build provenance and the source path of every JSX element. */
27
19
  export declare function visualReview(
28
20
  options?: VisualReviewViteOptions,
29
21
  ): Plugin;
package/dist/vite.js CHANGED
@@ -1,122 +1,47 @@
1
- import { t as resolveVisualReviewBuildIdentity } from "./build-identity-CgoY3-jy.js";
2
- import { a as visualReviewSourceIndexFilename, f as cleanupViteSourceMapOutput, l as writeViteVisualReviewSourceIndex, p as uploadViteSourceMaps, s as viteSourceIndexUrl } from "./source-index-BdWeiNHZ.js";
3
- import { t as instrumentVisualReviewSource } from "./source-instrumentation-transform-BwKYKlp0.js";
4
- import { randomBytes } from "node:crypto";
5
- import { resolve } from "node:path";
1
+ import { t as resolveVisualReviewBuildIdentity } from "./build-identity-DebYe8Qe.js";
2
+ import { t as instrumentVisualReviewSource } from "./source-instrumentation-transform-qmiVeKpI.js";
6
3
  //#region src/vite.ts
7
4
  const BUILD_ID_LITERAL = "process.env.VISUAL_REVIEW_PUBLIC_BUILD_ID";
8
5
  const GIT_COMMIT_LITERAL = "process.env.VISUAL_REVIEW_PUBLIC_GIT_COMMIT";
9
6
  const BUILD_MODE_LITERAL = "process.env.VISUAL_REVIEW_PUBLIC_BUILD_MODE";
10
- /** Injects public build provenance and optionally uploads private hidden source maps. */
7
+ /** Injects public build provenance and the source path of every JSX element. */
11
8
  function visualReview(options = {}) {
12
9
  let identity;
13
10
  let resolvedConfig;
14
- let sourceMapUpload;
15
11
  let sourceMetadataEnabled = false;
16
- let sourceIndexFilename;
17
- let sourceIndexUrl;
18
- let sourceMarkerSalt;
19
12
  let releaseMode;
20
13
  return {
21
14
  name: "01works:visual-review-build-identity",
22
15
  enforce: "pre",
23
16
  config(config, environment) {
24
17
  identity = resolveVisualReviewBuildIdentity(options, {
25
- cwd: resolve(process.cwd(), config.root ?? "."),
18
+ cwd: config.root ?? ".",
26
19
  env: process.env
27
20
  });
28
21
  releaseMode = typeof environment?.mode === "string" && environment.mode.length > 0 ? normalizeReleaseMode(environment.mode) : void 0;
29
22
  const define = publicLiteralDefinitions(identity, releaseMode);
30
23
  assertReservedLiterals(config.define, define, "Vite define");
31
- if (environment?.command === "serve") {
32
- sourceMapUpload = void 0;
33
- sourceMetadataEnabled = false;
34
- sourceMarkerSalt = void 0;
35
- return { define };
36
- }
37
- sourceMapUpload = resolveSourceMapUpload(options.sourceMaps, process.env);
38
24
  sourceMetadataEnabled = options.sourceMetadata ?? true;
39
- sourceMarkerSalt = sourceMetadataEnabled ? randomBytes(32).toString("hex") : void 0;
40
- if (!sourceMapUpload) return { define };
41
- if (config.build?.sourcemap === "inline") throw new Error("Visual Review source-map upload refuses inline Vite source maps");
42
- return {
43
- define,
44
- build: { sourcemap: "hidden" }
45
- };
25
+ return { define };
46
26
  },
47
27
  configResolved(config) {
48
28
  resolvedConfig = config;
49
29
  if (releaseMode !== void 0 && normalizeReleaseMode(config.mode) !== releaseMode) throw new Error("Visual Review build mode changed after the public literals were injected");
50
- if (sourceMetadataEnabled) {
51
- if (!identity || !sourceMarkerSalt) throw new Error("Visual Review source metadata identity is unavailable");
52
- sourceIndexFilename = visualReviewSourceIndexFilename(identity.buildId, sourceMarkerSalt);
53
- sourceIndexUrl = viteSourceIndexUrl(config.base, sourceIndexFilename);
54
- }
55
- if (!sourceMapUpload) return;
56
- if (config.build.sourcemap !== "hidden") throw new Error("Visual Review source-map upload requires build.sourcemap=\"hidden\"");
57
- if (config.build.ssr) throw new Error("Visual Review Vite source-map upload currently supports browser builds only");
58
30
  },
59
31
  transform(source, id) {
60
32
  if (!sourceMetadataEnabled) return null;
61
- if (!resolvedConfig || !sourceIndexUrl || !sourceMarkerSalt) throw new Error("Visual Review source metadata was not initialized by Vite");
33
+ if (!resolvedConfig) throw new Error("Visual Review source metadata was not initialized by Vite");
62
34
  const transformed = instrumentVisualReviewSource(source, {
63
35
  id,
64
- indexUrl: sourceIndexUrl,
65
- markerSalt: sourceMarkerSalt,
66
36
  projectRoot: resolvedConfig.root
67
37
  });
68
38
  return transformed ? {
69
39
  code: transformed.code,
70
40
  map: transformed.map
71
41
  } : null;
72
- },
73
- async closeBundle() {
74
- if (!identity || !resolvedConfig) throw new Error("Visual Review build adapter was not initialized by Vite");
75
- const outDir = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
76
- if (sourceMetadataEnabled) {
77
- if (!sourceIndexFilename) throw new Error("Visual Review source metadata was not initialized by Vite");
78
- try {
79
- await writeViteVisualReviewSourceIndex({
80
- projectRoot: resolvedConfig.root,
81
- outDir,
82
- base: resolvedConfig.base,
83
- filename: sourceIndexFilename
84
- });
85
- } catch (sourceIndexError) {
86
- if (!sourceMapUpload) throw sourceIndexError;
87
- try {
88
- await cleanupViteSourceMapOutput(resolvedConfig.root, outDir);
89
- } catch (cleanupError) {
90
- throw new AggregateError([sourceIndexError, cleanupError], "Visual Review source index failed and Vite source maps could not be cleaned");
91
- }
92
- throw sourceIndexError;
93
- }
94
- }
95
- if (!sourceMapUpload) return;
96
- await uploadViteSourceMaps({
97
- projectRoot: resolvedConfig.root,
98
- outDir,
99
- base: resolvedConfig.base,
100
- token: sourceMapUpload.token,
101
- ...sourceMapUpload.serviceUrl ? { serviceUrl: sourceMapUpload.serviceUrl } : {},
102
- buildId: identity.buildId,
103
- gitCommit: identity.gitCommit,
104
- mode: normalizeReleaseMode(resolvedConfig.mode)
105
- });
106
42
  }
107
43
  };
108
44
  }
109
- function resolveSourceMapUpload(option, env) {
110
- if (option === false || typeof option === "object" && option.enabled === false) return;
111
- const configured = typeof option === "object" ? option : void 0;
112
- const token = configured?.token ?? env.VISUAL_REVIEW_SOURCE_MAP_TOKEN;
113
- if (!(option === true || configured !== void 0 || token !== void 0)) return void 0;
114
- if (typeof token !== "string" || token.length === 0 || /\s/.test(token)) throw new Error("Visual Review source-map upload requires VISUAL_REVIEW_SOURCE_MAP_TOKEN in the Node build environment");
115
- return {
116
- token,
117
- ...configured?.serviceUrl ? { serviceUrl: configured.serviceUrl } : {}
118
- };
119
- }
120
45
  function normalizeReleaseMode(mode) {
121
46
  if (mode === "production") return "production";
122
47
  if (mode === "development") return "development";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@01.works/visual-review",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Invitation-only visual feedback for staging websites, hosted by 01.works.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- import{t as e}from"./generated-frame-CkUKpj5U.js";function t(e){let t=new Map,r=e.sourceIndexBaseUrl;return{unavailableProvenance:()=>f(e,`unavailable`),async resolve(i,a){let o=a?.signal;p(o);let s=i.closest(`[data-visual-review-source]`),c=s?await n(s,t,o,r):null;return p(o),{source:c,componentStack:c?[c]:[],provenance:f(e,c?`fallback`:`unavailable`)}}}}async function n(e,t,n,i){let o=e.getAttribute(`data-visual-review-source`),c=e.getAttribute(`data-visual-review-source-index`);if(!o||!/^vr1_[0-9a-f]{32}$/u.test(o)||!c)return null;let l=r(c,e.ownerDocument,i);if(!l||typeof globalThis.fetch!=`function`)return null;let d=t.get(l);if(!d){if(t.size>=v)return null;d=a(l),t.set(l,d)}let f;try{f=await m(d,n)}catch{return null}let p=s(f,o);return p?u(p):null}function r(e,t,n){if(e.length>2048||e.trim()!==e||e.includes(`\\`)||/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u.test(e))return null;try{let r=new URL(e,n??t.baseURI);return r.origin!==t.location.origin||r.username||r.password||r.search||r.hash||!i(r.pathname)?null:r.href}catch{return null}}function i(e){return x.test(e)||S.test(e)}async function a(e){let t=await globalThis.fetch(e,{cache:`force-cache`,credentials:`same-origin`});if(!t.ok)throw Error(`Visual Review source index returned HTTP ${t.status}`);let n=t.headers.get(`content-length`),r=n===null?null:Number(n);if(r!==null&&(!Number.isSafeInteger(r)||r<0||r>_))throw Error(`Visual Review source index exceeds 8 MiB`);let i=await o(t);return JSON.parse(i)}async function o(e){let t=e.body?.getReader();if(!t)return``;let n=new TextDecoder(`utf-8`,{fatal:!0}),r=[],i=0;try{for(;;){let{done:e,value:a}=await t.read();if(e)break;if(i+=a.byteLength,i>_)throw await t.cancel(`Visual Review source index exceeds 8 MiB`),Error(`Visual Review source index exceeds 8 MiB`);r.push(n.decode(a,{stream:!0}))}return r.push(n.decode()),r.join(``)}finally{t.releaseLock()}}function s(t,n){if(!l(t)||t.schemaVersion!==1||!l(t.entries))return;let r=t.entries[n];if(!(!Array.isArray(r)||r.length===0||r.length>y))for(let t of r){if(!l(t)||!c(t.runtime))continue;let n=e(t.assetPath),r=d(t.line1,1,h),i=d(t.column0,0,g);if(!(!n||r===null||i===null))return{assetPath:n,line1:r,column0:i,runtime:t.runtime}}}function c(e){return e===`browser`||e===`server-node`||e===`server-edge`}function l(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function u(e){return{filePath:e.assetPath,lineNumber:e.line1,columnNumber:e.column0+1,componentName:null,origin:`generated`,generated:e}}function d(e,t,n){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=t&&e<=n?e:null}function f(e,t){return{provider:`embedded`,build:e.build,resolvedAt:e.now?.()??Date.now(),sourceMapStatus:t}}function p(e){if(e?.aborted)throw e.reason??Error(`Source context resolution aborted`)}function m(e,t){return t?t.aborted?Promise.reject(t.reason??Error(`Source context resolution aborted`)):new Promise((n,r)=>{let i=()=>r(t.reason??Error(`Source context resolution aborted`));t.addEventListener(`abort`,i,{once:!0}),e.then(e=>{t.removeEventListener(`abort`,i),n(e)},e=>{t.removeEventListener(`abort`,i),r(e)})}):e}const h=1e7,g=1e7,_=8*1024*1024,v=16,y=64,b=`visual-review-sources-[0-9a-f]{16}\\.json`,x=RegExp(`^/(?:[^/]+/)*\\.visual-review/${b}$`,`u`),S=RegExp(`^/(?:[^/]+/)*_next/static/${b}$`,`u`);export{t as createGeneratedSourceContextResolver};