@01.works/visual-review 0.3.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 nextSourceIndexUrl, d as cleanupNextSourceMapOutput, f as uploadNextSourceMaps, l as writeNextVisualReviewSourceIndex, o as visualReviewSourceIndexFilename } from "./source-index-o5fwJzns.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,28 +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 sourceMetadataOptions = {
96
- enabled: sourceMetadataEnabled,
97
- embedSourcePath: options.embedSourcePath ?? true,
98
- markerSalt: sourceMetadataEnabled ? resolveBuildLocalMarkerSalt(process.env) : void 0
99
- };
100
88
  const injected = {
101
89
  [BUILD_ID_LITERAL]: identity.buildId,
102
90
  [GIT_COMMIT_LITERAL]: identity.gitCommit ?? "",
103
- [BUILD_MODE_LITERAL]: sourceMapUpload?.mode ?? configuredReleaseMode(options.sourceMaps) ?? inferReleaseMode(process.env)
91
+ [BUILD_MODE_LITERAL]: inferReleaseMode(process.env)
104
92
  };
105
93
  if (typeof nextConfig === "function") return function visualReviewNextConfig(...arguments_) {
106
94
  const resolved = nextConfig.apply(this, arguments_);
107
- return isPromiseLike(resolved) ? resolved.then((config) => configureNext(config, injected, identity, sourceMapUpload, sourceMetadataOptions)) : configureNext(resolved, injected, identity, sourceMapUpload, sourceMetadataOptions);
95
+ return isPromiseLike(resolved) ? resolved.then((config) => configureNext(config, injected, sourceMetadataEnabled)) : configureNext(resolved, injected, sourceMetadataEnabled);
108
96
  };
109
- return configureNext(nextConfig, injected, identity, sourceMapUpload, sourceMetadataOptions);
97
+ return configureNext(nextConfig, injected, sourceMetadataEnabled);
110
98
  }
111
- function configureNext(nextConfig, injected, identity, sourceMapUpload, sourceMetadataOptions) {
112
- const previousHookMetadata = getVisualReviewHookMetadata(nextConfig.compiler?.runAfterProductionCompile);
113
- const previouslyWrapped = isVisualReviewConfig(nextConfig) || previousHookMetadata !== void 0;
114
- assertReservedLiterals(nextConfig.env, injected, previouslyWrapped);
99
+ function configureNext(nextConfig, injected, sourceMetadataEnabled) {
100
+ const previousBundlerState = getVisualReviewBundlerState(nextConfig);
101
+ assertReservedLiterals(nextConfig.env, injected, previousBundlerState !== void 0);
115
102
  let configured = {
116
103
  ...nextConfig,
117
104
  env: {
@@ -119,84 +106,19 @@ function configureNext(nextConfig, injected, identity, sourceMapUpload, sourceMe
119
106
  ...injected
120
107
  }
121
108
  };
122
- if (previousHookMetadata) configured = restoreSourceMapConfiguration(configured, previousHookMetadata);
123
- if (!sourceMapUpload && !sourceMetadataOptions.enabled) return markVisualReviewConfig(configured);
124
- const hookMetadata = previousHookMetadata ?? captureHookMetadata(nextConfig);
125
- 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");
126
- 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");
127
- const sourceMetadata = sourceMetadataOptions.enabled ? resolveNextSourceMetadata(identity, nextConfig.basePath, sourceMetadataOptions.markerSalt, sourceMetadataOptions.embedSourcePath) : void 0;
128
- const sourceConfigured = sourceMetadata ? configureNextSourceMetadata(configured, hookMetadata, sourceMetadata) : configured;
129
- const compiler = sourceConfigured.compiler ?? {};
130
- return markVisualReviewConfig({
131
- ...sourceConfigured,
132
- ...sourceMapUpload ? { productionBrowserSourceMaps: true } : {},
133
- ...sourceMapUpload ? { experimental: {
134
- ...sourceConfigured.experimental,
135
- serverSourceMaps: true
136
- } } : {},
137
- compiler: {
138
- ...compiler,
139
- runAfterProductionCompile: composeProductionCompileHook(hookMetadata.originalHook, identity, sourceMapUpload, sourceMetadata, nextConfig.basePath, nextConfig.assetPrefix, hookMetadata)
140
- }
141
- });
142
- }
143
- function composeProductionCompileHook(existingHook, identity, sourceMapUpload, sourceMetadata, basePath, assetPrefix, metadata) {
144
- const hook = async function visualReviewAfterProductionCompile(metadata) {
145
- if (sourceMetadata) removeBuildLocalMarkerSalt(sourceMetadata.saltPath);
146
- if (existingHook) try {
147
- await existingHook.call(this, metadata);
148
- } catch (existingError) {
149
- if (!sourceMapUpload) throw existingError;
150
- try {
151
- await cleanupNextSourceMapOutput(metadata.projectDir, metadata.distDir);
152
- } catch (cleanupError) {
153
- throw new AggregateError([existingError, cleanupError], "Next.js production compile hook failed and Visual Review could not clean source maps");
154
- }
155
- throw existingError;
156
- }
157
- if (sourceMetadata && !sourceMetadata.embedSourcePath) try {
158
- await writeNextVisualReviewSourceIndex({
159
- projectDir: metadata.projectDir,
160
- distDir: metadata.distDir,
161
- filename: sourceMetadata.filename,
162
- ...basePath ? { basePath } : {},
163
- ...assetPrefix ? { assetPrefix } : {}
164
- });
165
- } catch (sourceIndexError) {
166
- if (!sourceMapUpload) throw sourceIndexError;
167
- try {
168
- await cleanupNextSourceMapOutput(metadata.projectDir, metadata.distDir);
169
- } catch (cleanupError) {
170
- throw new AggregateError([sourceIndexError, cleanupError], "Visual Review source index failed and Next.js source maps could not be cleaned");
171
- }
172
- throw sourceIndexError;
173
- }
174
- if (!sourceMapUpload) return;
175
- await uploadNextSourceMaps({
176
- projectRoot: metadata.projectDir,
177
- distDir: metadata.distDir,
178
- token: sourceMapUpload.token,
179
- ...sourceMapUpload.serviceUrl ? { serviceUrl: sourceMapUpload.serviceUrl } : {},
180
- buildId: identity.buildId,
181
- gitCommit: identity.gitCommit,
182
- mode: sourceMapUpload.mode,
183
- ...basePath ? { basePath } : {},
184
- ...assetPrefix ? { assetPrefix } : {}
185
- });
186
- };
187
- Object.defineProperty(hook, VISUAL_REVIEW_HOOK, { value: metadata });
188
- 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);
189
116
  }
190
- function getVisualReviewHookMetadata(hook) {
191
- return hook?.[VISUAL_REVIEW_HOOK];
117
+ function getVisualReviewBundlerState(config) {
118
+ return config[VISUAL_REVIEW_CONFIG];
192
119
  }
193
- function captureHookMetadata(nextConfig) {
120
+ function captureBundlerState(nextConfig) {
194
121
  return {
195
- originalHook: nextConfig.compiler?.runAfterProductionCompile,
196
- compilerPresent: Object.prototype.hasOwnProperty.call(nextConfig, "compiler"),
197
- experimentalPresent: Object.prototype.hasOwnProperty.call(nextConfig, "experimental"),
198
- productionBrowserSourceMaps: propertyState(nextConfig, "productionBrowserSourceMaps"),
199
- serverSourceMaps: propertyState(nextConfig.experimental, "serverSourceMaps"),
200
122
  turbopack: propertyState(nextConfig, "turbopack"),
201
123
  webpack: propertyState(nextConfig, "webpack")
202
124
  };
@@ -207,98 +129,17 @@ function propertyState(value, key) {
207
129
  value: value?.[key]
208
130
  };
209
131
  }
210
- function restoreSourceMapConfiguration(configured, metadata) {
211
- if (!metadata) return configured;
212
- const restored = { ...configured };
213
- if (metadata.productionBrowserSourceMaps.present) restored.productionBrowserSourceMaps = metadata.productionBrowserSourceMaps.value;
214
- else delete restored.productionBrowserSourceMaps;
215
- const experimental = { ...restored.experimental ?? {} };
216
- if (metadata.serverSourceMaps.present) experimental.serverSourceMaps = metadata.serverSourceMaps.value;
217
- else delete experimental.serverSourceMaps;
218
- if (!metadata.experimentalPresent && Object.keys(experimental).length === 0) delete restored.experimental;
219
- else restored.experimental = experimental;
220
- const compiler = { ...restored.compiler ?? {} };
221
- if (metadata.originalHook) compiler.runAfterProductionCompile = metadata.originalHook;
222
- else delete compiler.runAfterProductionCompile;
223
- if (!metadata.compilerPresent && Object.keys(compiler).length === 0) delete restored.compiler;
224
- else restored.compiler = compiler;
225
- restoreProperty(restored, "turbopack", metadata.turbopack);
226
- restoreProperty(restored, "webpack", metadata.webpack);
227
- return restored;
228
- }
229
132
  function restoreProperty(target, key, state) {
230
133
  if (state.present) target[key] = state.value;
231
134
  else delete target[key];
232
135
  }
233
- function resolveNextSourceMetadata(identity, basePath, markerSalt, embedSourcePath) {
234
- if (!markerSalt) throw new Error("Visual Review source marker salt is unavailable");
235
- const filename = visualReviewSourceIndexFilename(identity.buildId, markerSalt.salt);
136
+ function resolveNextSourceMetadata() {
236
137
  const loaderUrl = new URL("./source-loader.js", import.meta.url);
237
- return {
238
- embedSourcePath,
239
- filename,
240
- indexUrl: nextSourceIndexUrl(basePath, filename),
241
- loader: loaderUrl.protocol === "file:" ? fileURLToPath(loaderUrl) : resolve(process.cwd(), "packages/review-client/dist/source-loader.js"),
242
- saltPath: markerSalt.path
243
- };
244
- }
245
- /**
246
- * Generates the marker salt and persists it outside the project so compiler
247
- * workers can read it without the secret entering any build output. The file is
248
- * owner-only and is removed once the production compile hook has run.
249
- */
250
- function resolveBuildLocalMarkerSalt(env) {
251
- const inherited = env[NEXT_MARKER_SALT_PATH];
252
- if (env.NEXT_PRIVATE_BUILD_WORKER === "1" && env.IS_NEXT_WORKER === "true" && typeof inherited === "string" && inherited.length > 0) return {
253
- salt: readMarkerSaltFile(inherited),
254
- path: inherited
255
- };
256
- const salt = randomBytes(32).toString("hex");
257
- const path = join(mkdtempSync(join(tmpdir(), "visual-review-marker-salt-")), "salt");
258
- writeFileSync(path, salt, {
259
- encoding: "utf8",
260
- mode: 384
261
- });
262
- env[NEXT_MARKER_SALT_PATH] = path;
263
- trackBuildLocalMarkerSalt(path);
264
- return {
265
- salt,
266
- path
267
- };
268
- }
269
- function readMarkerSaltFile(saltPath) {
270
- const salt = readFileSync(saltPath, "utf8").trim();
271
- if (!/^[0-9a-f]{64}$/u.test(salt)) throw new Error("Visual Review inherited marker salt is invalid");
272
- return salt;
273
- }
274
- /**
275
- * `next dev` never reaches the production compile hook, and every compiler
276
- * worker evaluates the config in its own process, so exit is the only point
277
- * that reliably removes each salt file it created.
278
- */
279
- function trackBuildLocalMarkerSalt(saltPath) {
280
- if (trackedMarkerSaltPaths.size === 0) process.once("exit", () => {
281
- for (const tracked of trackedMarkerSaltPaths) removeMarkerSaltDirectory(tracked);
282
- });
283
- trackedMarkerSaltPaths.add(saltPath);
138
+ return { loader: loaderUrl.protocol === "file:" ? fileURLToPath(loaderUrl) : resolve(process.cwd(), "packages/review-client/dist/source-loader.js") };
284
139
  }
285
- function removeBuildLocalMarkerSalt(saltPath) {
286
- trackedMarkerSaltPaths.delete(saltPath);
287
- removeMarkerSaltDirectory(saltPath);
288
- }
289
- function removeMarkerSaltDirectory(saltPath) {
290
- try {
291
- rmSync(dirname(saltPath), {
292
- recursive: true,
293
- force: true
294
- });
295
- } catch {}
296
- }
297
- function configureNextSourceMetadata(configured, metadata, sourceMetadata) {
140
+ function configureNextSourceMetadata(configured, sourceMetadata) {
298
141
  const bundler = resolveNextBundler();
299
142
  const next = { ...configured };
300
- restoreProperty(next, "turbopack", metadata.turbopack);
301
- restoreProperty(next, "webpack", metadata.webpack);
302
143
  if (bundler === "turbopack") next.turbopack = appendTurbopackRules(next.turbopack, sourceMetadata);
303
144
  else next.webpack = appendWebpackRule(next.webpack, sourceMetadata);
304
145
  return next;
@@ -323,11 +164,7 @@ function appendWebpackRule(existing, sourceMetadata) {
323
164
  test: /\.[cm]?[jt]sx?$/u,
324
165
  use: [{
325
166
  loader: sourceMetadata.loader,
326
- options: {
327
- embedSourcePath: sourceMetadata.embedSourcePath,
328
- indexUrl: sourceMetadata.indexUrl,
329
- saltPath: sourceMetadata.saltPath
330
- }
167
+ options: {}
331
168
  }]
332
169
  }]
333
170
  }
@@ -347,12 +184,7 @@ function appendTurbopackRules(existing, sourceMetadata) {
347
184
  condition: { all: [{ not: "foreign" }, "production"] },
348
185
  loaders: [{
349
186
  loader: sourceMetadata.loader,
350
- options: {
351
- compile: true,
352
- embedSourcePath: sourceMetadata.embedSourcePath,
353
- indexUrl: sourceMetadata.indexUrl,
354
- saltPath: sourceMetadata.saltPath
355
- }
187
+ options: { compile: true }
356
188
  }]
357
189
  };
358
190
  const current = rules[pattern];
@@ -369,28 +201,10 @@ function isPromiseLikeUnknown(value) {
369
201
  function isRecord(value) {
370
202
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
371
203
  }
372
- function isVisualReviewConfig(config) {
373
- return Boolean(config[VISUAL_REVIEW_CONFIG]);
374
- }
375
- function markVisualReviewConfig(config) {
376
- Object.defineProperty(config, VISUAL_REVIEW_CONFIG, { value: true });
204
+ function markVisualReviewConfig(config, bundlerState) {
205
+ Object.defineProperty(config, VISUAL_REVIEW_CONFIG, { value: bundlerState });
377
206
  return config;
378
207
  }
379
- function resolveSourceMapUpload(option, env) {
380
- if (option === false || typeof option === "object" && option.enabled === false) return;
381
- const configured = typeof option === "object" ? option : void 0;
382
- const token = configured?.token ?? env.VISUAL_REVIEW_SOURCE_MAP_TOKEN;
383
- if (!(option === true || configured !== void 0 || token !== void 0)) return void 0;
384
- 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");
385
- return {
386
- token,
387
- mode: configured?.mode ?? inferReleaseMode(env),
388
- ...configured?.serviceUrl ? { serviceUrl: configured.serviceUrl } : {}
389
- };
390
- }
391
- function configuredReleaseMode(option) {
392
- return typeof option === "object" ? option.mode : void 0;
393
- }
394
208
  function inferReleaseMode(env) {
395
209
  if (env.VERCEL_ENV === "preview") return "preview";
396
210
  if (env.VERCEL_ENV === "development") return "development";
package/dist/react.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./install-C-4G_vDp.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 { i as VISUAL_REVIEW_SOURCE_MARKER_ATTRIBUTE, n as VISUAL_REVIEW_SOURCE_COORDINATE_ATTRIBUTE, r as VISUAL_REVIEW_SOURCE_INDEX_ATTRIBUTE, s as visualReviewSourceMarker, t as VISUAL_REVIEW_EMBEDDED_SOURCE_ATTRIBUTE } from "./source-index-o5fwJzns.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 = options.embedSourcePath ? ` ${VISUAL_REVIEW_EMBEDDED_SOURCE_ATTRIBUTE}="${escapeAttribute(`${sourcePath}:${line1}:${column1}`)}"` : ` ${VISUAL_REVIEW_SOURCE_MARKER_ATTRIBUTE}="${marker}" ${VISUAL_REVIEW_SOURCE_INDEX_ATTRIBUTE}="${options.indexUrl}"`;
51
- if (!options.embedSourcePath) 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;
@@ -70,9 +61,6 @@ function instrumentVisualReviewSource(source, options) {
70
61
  function escapeAttribute(value) {
71
62
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
72
63
  }
73
- function assertMarkerSalt(value) {
74
- if (!MARKER_SALT.test(value)) throw new Error("Visual Review source marker salt must be a lowercase 32-byte hex secret");
75
- }
76
64
  /**
77
65
  * Reports whether a module is first-party project source. Bundler rules already
78
66
  * try to exclude dependencies, but Next 15's Turbopack does not honour the
@@ -108,7 +96,7 @@ function intrinsicJsxNameEnd(name) {
108
96
  return typeof candidate.end === "number" ? candidate.end : null;
109
97
  }
110
98
  function hasVisualReviewMarker(attributes) {
111
- 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;
112
100
  }
113
101
  function visitAst(node, visitor) {
114
102
  visitor(node);
@@ -1,29 +1,8 @@
1
- import { n as isVisualReviewProjectSource, t as instrumentVisualReviewSource } from "./source-instrumentation-transform-Cx8SIIWh.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,10 +14,7 @@ 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
- projectRoot,
41
- embedSourcePath: options.embedSourcePath
17
+ projectRoot
42
18
  });
43
19
  const code = transformed?.code ?? source;
44
20
  const map = transformed?.map ?? inputMap;
@@ -113,16 +89,9 @@ function isRecord(value) {
113
89
  function parseOptions(value) {
114
90
  if (!value || typeof value !== "object") throw new Error("Visual Review source loader options are missing");
115
91
  const options = value;
116
- 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");
117
92
  if (options.projectRoot !== void 0 && typeof options.projectRoot !== "string") throw new Error("Visual Review source loader project root is invalid");
118
- if (options.embedSourcePath !== void 0 && typeof options.embedSourcePath !== "boolean") throw new Error("Visual Review source loader embedSourcePath is invalid");
119
- const markerSalt = typeof options.saltPath === "string" ? readMarkerSaltFile(options.saltPath) : options.markerSalt;
120
- if (typeof markerSalt !== "string" || !MARKER_SALT.test(markerSalt)) throw new Error("Visual Review source loader marker salt is invalid");
121
93
  return {
122
94
  ...options.compile === true ? { compile: true } : {},
123
- embedSourcePath: options.embedSourcePath ?? true,
124
- indexUrl: options.indexUrl,
125
- markerSalt,
126
95
  ...typeof options.projectRoot === "string" ? { projectRoot: options.projectRoot } : {}
127
96
  };
128
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,129 +1,47 @@
1
- import { t as resolveVisualReviewBuildIdentity } from "./build-identity-CgoY3-jy.js";
2
- import { c as viteSourceIndexUrl, m as uploadViteSourceMaps, o as visualReviewSourceIndexFilename, p as cleanupViteSourceMapOutput, u as writeViteVisualReviewSourceIndex } from "./source-index-o5fwJzns.js";
3
- import { t as instrumentVisualReviewSource } from "./source-instrumentation-transform-Cx8SIIWh.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 embedSourcePath = true;
17
- let sourceIndexFilename;
18
- let sourceIndexUrl;
19
- let sourceMarkerSalt;
20
12
  let releaseMode;
21
- let serving = false;
22
13
  return {
23
14
  name: "01works:visual-review-build-identity",
24
15
  enforce: "pre",
25
16
  config(config, environment) {
26
17
  identity = resolveVisualReviewBuildIdentity(options, {
27
- cwd: resolve(process.cwd(), config.root ?? "."),
18
+ cwd: config.root ?? ".",
28
19
  env: process.env
29
20
  });
30
21
  releaseMode = typeof environment?.mode === "string" && environment.mode.length > 0 ? normalizeReleaseMode(environment.mode) : void 0;
31
22
  const define = publicLiteralDefinitions(identity, releaseMode);
32
23
  assertReservedLiterals(config.define, define, "Vite define");
33
- if (environment?.command === "serve") {
34
- sourceMapUpload = void 0;
35
- serving = true;
36
- embedSourcePath = options.embedSourcePath ?? true;
37
- sourceMetadataEnabled = (options.sourceMetadata ?? true) && embedSourcePath;
38
- sourceMarkerSalt = sourceMetadataEnabled ? randomBytes(32).toString("hex") : void 0;
39
- return { define };
40
- }
41
- sourceMapUpload = resolveSourceMapUpload(options.sourceMaps, process.env);
42
24
  sourceMetadataEnabled = options.sourceMetadata ?? true;
43
- embedSourcePath = options.embedSourcePath ?? true;
44
- sourceMarkerSalt = sourceMetadataEnabled ? randomBytes(32).toString("hex") : void 0;
45
- if (!sourceMapUpload) return { define };
46
- if (config.build?.sourcemap === "inline") throw new Error("Visual Review source-map upload refuses inline Vite source maps");
47
- return {
48
- define,
49
- build: { sourcemap: "hidden" }
50
- };
25
+ return { define };
51
26
  },
52
27
  configResolved(config) {
53
28
  resolvedConfig = config;
54
29
  if (releaseMode !== void 0 && normalizeReleaseMode(config.mode) !== releaseMode) throw new Error("Visual Review build mode changed after the public literals were injected");
55
- if (sourceMetadataEnabled) {
56
- if (!identity || !sourceMarkerSalt) throw new Error("Visual Review source metadata identity is unavailable");
57
- sourceIndexFilename = visualReviewSourceIndexFilename(identity.buildId, sourceMarkerSalt);
58
- sourceIndexUrl = viteSourceIndexUrl(config.base, sourceIndexFilename);
59
- }
60
- if (!sourceMapUpload) return;
61
- if (config.build.sourcemap !== "hidden") throw new Error("Visual Review source-map upload requires build.sourcemap=\"hidden\"");
62
- if (config.build.ssr) throw new Error("Visual Review Vite source-map upload currently supports browser builds only");
63
30
  },
64
31
  transform(source, id) {
65
32
  if (!sourceMetadataEnabled) return null;
66
- 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");
67
34
  const transformed = instrumentVisualReviewSource(source, {
68
35
  id,
69
- indexUrl: sourceIndexUrl,
70
- markerSalt: sourceMarkerSalt,
71
- projectRoot: resolvedConfig.root,
72
- embedSourcePath
36
+ projectRoot: resolvedConfig.root
73
37
  });
74
38
  return transformed ? {
75
39
  code: transformed.code,
76
40
  map: transformed.map
77
41
  } : null;
78
- },
79
- async closeBundle() {
80
- if (serving) return;
81
- if (!identity || !resolvedConfig) throw new Error("Visual Review build adapter was not initialized by Vite");
82
- const outDir = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
83
- if (sourceMetadataEnabled && !embedSourcePath) {
84
- if (!sourceIndexFilename) throw new Error("Visual Review source metadata was not initialized by Vite");
85
- try {
86
- await writeViteVisualReviewSourceIndex({
87
- projectRoot: resolvedConfig.root,
88
- outDir,
89
- base: resolvedConfig.base,
90
- filename: sourceIndexFilename
91
- });
92
- } catch (sourceIndexError) {
93
- if (!sourceMapUpload) throw sourceIndexError;
94
- try {
95
- await cleanupViteSourceMapOutput(resolvedConfig.root, outDir);
96
- } catch (cleanupError) {
97
- throw new AggregateError([sourceIndexError, cleanupError], "Visual Review source index failed and Vite source maps could not be cleaned");
98
- }
99
- throw sourceIndexError;
100
- }
101
- }
102
- if (!sourceMapUpload) return;
103
- await uploadViteSourceMaps({
104
- projectRoot: resolvedConfig.root,
105
- outDir,
106
- base: resolvedConfig.base,
107
- token: sourceMapUpload.token,
108
- ...sourceMapUpload.serviceUrl ? { serviceUrl: sourceMapUpload.serviceUrl } : {},
109
- buildId: identity.buildId,
110
- gitCommit: identity.gitCommit,
111
- mode: normalizeReleaseMode(resolvedConfig.mode)
112
- });
113
42
  }
114
43
  };
115
44
  }
116
- function resolveSourceMapUpload(option, env) {
117
- if (option === false || typeof option === "object" && option.enabled === false) return;
118
- const configured = typeof option === "object" ? option : void 0;
119
- const token = configured?.token ?? env.VISUAL_REVIEW_SOURCE_MAP_TOKEN;
120
- if (!(option === true || configured !== void 0 || token !== void 0)) return void 0;
121
- 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");
122
- return {
123
- token,
124
- ...configured?.serviceUrl ? { serviceUrl: configured.serviceUrl } : {}
125
- };
126
- }
127
45
  function normalizeReleaseMode(mode) {
128
46
  if (mode === "production") return "production";
129
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.3.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",