@designfever/web-review-kit 0.7.2 → 0.8.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.
- package/README.md +7 -4
- package/dist/{chunk-RPVLRULC.js → chunk-4ZP7B7R6.js} +2345 -1786
- package/dist/chunk-4ZP7B7R6.js.map +1 -0
- package/dist/{chunk-K26WLRRP.js → chunk-UNDQZ4Y2.js} +33 -9
- package/dist/chunk-UNDQZ4Y2.js.map +1 -0
- package/dist/index.cjs +2381 -1801
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -5
- package/dist/index.d.ts +17 -5
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/react-shell.cjs +14508 -11099
- package/dist/react-shell.cjs.map +1 -1
- package/dist/react-shell.d.cts +5 -4
- package/dist/react-shell.d.ts +5 -4
- package/dist/react-shell.js +10634 -7710
- package/dist/react-shell.js.map +1 -1
- package/dist/{types-DT9Z66mV.d.cts → types-BM8E7BFV.d.cts} +63 -3
- package/dist/{types-DT9Z66mV.d.ts → types-BM8E7BFV.d.ts} +63 -3
- package/dist/vite.cjs +344 -48
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +2 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +346 -50
- package/dist/vite.js.map +1 -1
- package/docs/README.md +19 -11
- package/docs/adapters.md +56 -4
- package/docs/adaptor.sample.ts +53 -3
- package/docs/architecture.md +117 -3
- package/docs/figma-overlay.md +51 -0
- package/docs/grid-overlay.md +5 -0
- package/docs/installation.md +23 -8
- package/docs/release-notes-0.7.3.md +30 -0
- package/docs/release-notes-0.8.0.md +175 -0
- package/docs/testing.md +68 -0
- package/package.json +14 -5
- package/dist/chunk-K26WLRRP.js.map +0 -1
- package/dist/chunk-RPVLRULC.js.map +0 -1
- package/docs/code-review-0.6.0.md +0 -232
- package/docs/figma-image-mvp-0.7.0.md +0 -330
package/dist/vite.cjs
CHANGED
|
@@ -374,6 +374,16 @@ function compareReviewFigmaSnapshotImages(a, b) {
|
|
|
374
374
|
// src/vite/figma-image-store.image.ts
|
|
375
375
|
var import_promises = require("fs/promises");
|
|
376
376
|
var import_node_path = __toESM(require("path"), 1);
|
|
377
|
+
var reviewImageStoreTaskQueues = /* @__PURE__ */ new Map();
|
|
378
|
+
function runExclusiveReviewImageStoreTask(dataFile, task) {
|
|
379
|
+
const previous = reviewImageStoreTaskQueues.get(dataFile) ?? Promise.resolve();
|
|
380
|
+
const result = previous.then(task, task);
|
|
381
|
+
reviewImageStoreTaskQueues.set(
|
|
382
|
+
dataFile,
|
|
383
|
+
result.catch(() => void 0)
|
|
384
|
+
);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
377
387
|
function requireReviewFigmaRequestToken({
|
|
378
388
|
enabled,
|
|
379
389
|
env,
|
|
@@ -700,12 +710,19 @@ async function readReviewFigmaImageStoreFile(dataFile) {
|
|
|
700
710
|
}
|
|
701
711
|
async function writeReviewFigmaImageStoreFile(dataFile, data) {
|
|
702
712
|
await (0, import_promises.mkdir)(import_node_path.default.dirname(dataFile), { recursive: true });
|
|
713
|
+
const tempFile = `${dataFile}.${process.pid}.${Date.now()}.tmp`;
|
|
703
714
|
await (0, import_promises.writeFile)(
|
|
704
|
-
|
|
715
|
+
tempFile,
|
|
705
716
|
`${JSON.stringify({ version: 1, images: data.images }, null, 2)}
|
|
706
717
|
`,
|
|
707
718
|
"utf8"
|
|
708
719
|
);
|
|
720
|
+
try {
|
|
721
|
+
await (0, import_promises.rename)(tempFile, dataFile);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
await (0, import_promises.rm)(tempFile, { force: true });
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
709
726
|
}
|
|
710
727
|
function getNextImageOrder(images, target) {
|
|
711
728
|
const targetImages = listImagesForTarget(images, target);
|
|
@@ -782,19 +799,21 @@ async function handleReviewFigmaImageStoreRequest({
|
|
|
782
799
|
if (!isAllowedProjectTarget(input.target, options.projectId)) {
|
|
783
800
|
return jsonError(403, "target project is not allowed.");
|
|
784
801
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
802
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
803
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
804
|
+
const image = await createReviewFigmaImage({
|
|
805
|
+
assetDir,
|
|
806
|
+
assetEndpoint,
|
|
807
|
+
currentImages: data.images,
|
|
808
|
+
env,
|
|
809
|
+
input,
|
|
810
|
+
options,
|
|
811
|
+
requestToken
|
|
812
|
+
});
|
|
813
|
+
data.images = [image, ...data.images];
|
|
814
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
815
|
+
return { status: 201, body: image };
|
|
794
816
|
});
|
|
795
|
-
data.images = [image, ...data.images];
|
|
796
|
-
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
797
|
-
return { status: 201, body: image };
|
|
798
817
|
}
|
|
799
818
|
if (method === "PATCH" && pathname === `${endpoint}/reorder`) {
|
|
800
819
|
const input = parseReorderImagesInput(body);
|
|
@@ -802,49 +821,109 @@ async function handleReviewFigmaImageStoreRequest({
|
|
|
802
821
|
if (!isAllowedProjectTarget(input.target, options.projectId)) {
|
|
803
822
|
return jsonError(403, "target project is not allowed.");
|
|
804
823
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
824
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
825
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
826
|
+
const images = reorderReviewFigmaImages(data.images, input);
|
|
827
|
+
data.images = images.allImages;
|
|
828
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
829
|
+
return { status: 200, body: images.targetImages };
|
|
830
|
+
});
|
|
810
831
|
}
|
|
811
832
|
const id = getEndpointItemId(pathname, endpoint);
|
|
812
833
|
if (id && method === "PATCH") {
|
|
813
834
|
const patch = parseUpdateImageInput(body);
|
|
814
835
|
if (!patch) return jsonError(400, "valid update patch is required.");
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
836
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
837
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
838
|
+
const result = updateReviewFigmaImage(data.images, id, patch);
|
|
839
|
+
if (!result) return jsonError(404, `Figma image not found: ${id}`);
|
|
840
|
+
if (!isAllowedProjectTarget(result.image.target, options.projectId)) {
|
|
841
|
+
return jsonError(403, "target project is not allowed.");
|
|
842
|
+
}
|
|
843
|
+
data.images = result.images;
|
|
844
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
845
|
+
return { status: 200, body: result.image };
|
|
846
|
+
});
|
|
824
847
|
}
|
|
825
848
|
if (id && method === "DELETE") {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
849
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
850
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
851
|
+
const image = data.images.find((item) => item.id === id);
|
|
852
|
+
if (!image) return jsonError(404, `Figma image not found: ${id}`);
|
|
853
|
+
if (!isAllowedProjectTarget(image.target, options.projectId)) {
|
|
854
|
+
return jsonError(403, "target project is not allowed.");
|
|
855
|
+
}
|
|
856
|
+
data.images = data.images.filter((item) => item.id !== id);
|
|
857
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
858
|
+
await deleteReviewFigmaImageAsset(assetDir, image.storageKey);
|
|
859
|
+
return { status: 200, body: { ok: true } };
|
|
860
|
+
});
|
|
836
861
|
}
|
|
837
862
|
return jsonError(405, "method not allowed.");
|
|
838
863
|
}
|
|
839
|
-
|
|
864
|
+
var DEFAULT_REVIEW_IMAGE_STORE_MAX_BODY_BYTES = 25 * 1024 * 1024;
|
|
865
|
+
var ReviewImageStoreRequestError = class extends Error {
|
|
866
|
+
constructor(status, message) {
|
|
867
|
+
super(message);
|
|
868
|
+
this.status = status;
|
|
869
|
+
this.name = "ReviewImageStoreRequestError";
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
function isReviewImageStoreRequestError(error) {
|
|
873
|
+
return error instanceof ReviewImageStoreRequestError;
|
|
874
|
+
}
|
|
875
|
+
function assertTrustedReviewImageStoreRequest(req) {
|
|
876
|
+
const origin = req.headers.origin;
|
|
877
|
+
if (!origin || typeof origin !== "string") return;
|
|
878
|
+
let originUrl;
|
|
879
|
+
try {
|
|
880
|
+
originUrl = new URL(origin);
|
|
881
|
+
} catch {
|
|
882
|
+
throw new ReviewImageStoreRequestError(
|
|
883
|
+
403,
|
|
884
|
+
"Cross-origin request is not allowed."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const host = req.headers.host;
|
|
888
|
+
if (host && originUrl.host === host) return;
|
|
889
|
+
if (isLoopbackHostname(originUrl.hostname)) return;
|
|
890
|
+
throw new ReviewImageStoreRequestError(
|
|
891
|
+
403,
|
|
892
|
+
"Cross-origin request is not allowed."
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
function isLoopbackHostname(hostname) {
|
|
896
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
897
|
+
}
|
|
898
|
+
async function readJsonRequestBody(req, maxBytes = DEFAULT_REVIEW_IMAGE_STORE_MAX_BODY_BYTES) {
|
|
840
899
|
if (req.method === "GET" || req.method === "DELETE") return null;
|
|
841
900
|
const chunks = [];
|
|
901
|
+
let totalBytes = 0;
|
|
842
902
|
for await (const chunk of req) {
|
|
843
|
-
|
|
903
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
904
|
+
totalBytes += buffer.length;
|
|
905
|
+
if (totalBytes > maxBytes) {
|
|
906
|
+
throw new ReviewImageStoreRequestError(
|
|
907
|
+
413,
|
|
908
|
+
`Request body exceeds ${maxBytes} bytes.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
chunks.push(buffer);
|
|
844
912
|
}
|
|
845
913
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
846
914
|
if (!raw) return null;
|
|
847
|
-
|
|
915
|
+
const contentType = req.headers["content-type"];
|
|
916
|
+
if (typeof contentType !== "string" || !contentType.split(";")[0].trim().toLowerCase().endsWith("/json")) {
|
|
917
|
+
throw new ReviewImageStoreRequestError(
|
|
918
|
+
415,
|
|
919
|
+
"Request body must be application/json."
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
return JSON.parse(raw);
|
|
924
|
+
} catch {
|
|
925
|
+
throw new ReviewImageStoreRequestError(400, "Invalid JSON request body.");
|
|
926
|
+
}
|
|
848
927
|
}
|
|
849
928
|
function sendJson(res, status, body) {
|
|
850
929
|
res.statusCode = status;
|
|
@@ -1082,6 +1161,7 @@ var reviewFigmaImageStore = (options = {}) => {
|
|
|
1082
1161
|
return;
|
|
1083
1162
|
}
|
|
1084
1163
|
try {
|
|
1164
|
+
assertTrustedReviewImageStoreRequest(req);
|
|
1085
1165
|
const response = await handleReviewFigmaImageStoreRequest({
|
|
1086
1166
|
dataFile,
|
|
1087
1167
|
assetDir,
|
|
@@ -1092,11 +1172,15 @@ var reviewFigmaImageStore = (options = {}) => {
|
|
|
1092
1172
|
pathname,
|
|
1093
1173
|
requestUrl,
|
|
1094
1174
|
method: req.method ?? "GET",
|
|
1095
|
-
body: await readJsonRequestBody(req),
|
|
1175
|
+
body: await readJsonRequestBody(req, options.maxRequestBytes),
|
|
1096
1176
|
requestToken: readRequestFigmaToken(req)
|
|
1097
1177
|
});
|
|
1098
1178
|
sendJson(res, response.status, response.body);
|
|
1099
1179
|
} catch (error) {
|
|
1180
|
+
if (isReviewImageStoreRequestError(error)) {
|
|
1181
|
+
sendJson(res, error.status, { error: error.message });
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1100
1184
|
sendJson(res, 500, {
|
|
1101
1185
|
error: error instanceof Error ? error.message : "Figma image store request failed."
|
|
1102
1186
|
});
|
|
@@ -1117,6 +1201,7 @@ function getServerEnv() {
|
|
|
1117
1201
|
|
|
1118
1202
|
// src/vite.ts
|
|
1119
1203
|
var VIRTUAL_JSX_DEV_RUNTIME_ID = "\0@designfever/web-review-kit/source-locator/jsx-dev-runtime";
|
|
1204
|
+
var typescriptModulePromise;
|
|
1120
1205
|
var REVIEW_SOURCE_ENV_DEFINE_KEYS = [
|
|
1121
1206
|
["__DF_WRK_REVIEW_SOURCE_ROOT__", "VITE_REVIEW_SOURCE_ROOT"],
|
|
1122
1207
|
["__DF_WRK_REVIEW_SOURCE_EDITOR__", "VITE_REVIEW_SOURCE_EDITOR"],
|
|
@@ -1160,9 +1245,11 @@ var reviewSourceLocator = (options = {}) => {
|
|
|
1160
1245
|
if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
|
|
1161
1246
|
return createJsxDevRuntime(runtimeOptions);
|
|
1162
1247
|
},
|
|
1163
|
-
transform(code) {
|
|
1248
|
+
async transform(code, id) {
|
|
1164
1249
|
const injectedCode = injectReviewSourceEnv(code, sourceEnvReplacements);
|
|
1165
|
-
|
|
1250
|
+
const inputCode = injectedCode ?? code;
|
|
1251
|
+
const componentInjectedCode = runtimeOptions.enabled ? await injectReviewSourceComponentHints(inputCode, id, runtimeOptions) : null;
|
|
1252
|
+
return injectedCode || componentInjectedCode ? { code: componentInjectedCode ?? inputCode, map: null } : null;
|
|
1166
1253
|
}
|
|
1167
1254
|
};
|
|
1168
1255
|
};
|
|
@@ -1215,6 +1302,112 @@ var reviewDataLocator = (options = {}) => {
|
|
|
1215
1302
|
}
|
|
1216
1303
|
};
|
|
1217
1304
|
};
|
|
1305
|
+
async function injectReviewSourceComponentHints(code, id, options) {
|
|
1306
|
+
const file = normalizePath(id.split("?")[0]);
|
|
1307
|
+
if (!isJsxSourceFile(file, code)) return null;
|
|
1308
|
+
const relativeFile = options.root && file.startsWith(options.root + "/") ? file.slice(options.root.length + 1) : file;
|
|
1309
|
+
if (options.include.length > 0 && !options.include.some((matcher) => matchesPath(matcher, file, relativeFile))) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
if (options.exclude.some((matcher) => matchesPath(matcher, file, relativeFile))) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
const ts = await loadTypeScript();
|
|
1316
|
+
if (!ts) return null;
|
|
1317
|
+
const sourceFile = ts.createSourceFile(
|
|
1318
|
+
file,
|
|
1319
|
+
code,
|
|
1320
|
+
ts.ScriptTarget.Latest,
|
|
1321
|
+
true,
|
|
1322
|
+
file.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TSX
|
|
1323
|
+
);
|
|
1324
|
+
const insertions = getSourceComponentInsertions(
|
|
1325
|
+
ts,
|
|
1326
|
+
sourceFile,
|
|
1327
|
+
options.componentAttribute,
|
|
1328
|
+
options.parentComponentAttribute
|
|
1329
|
+
);
|
|
1330
|
+
if (insertions.length === 0) return null;
|
|
1331
|
+
return applySourceComponentInsertions(code, insertions);
|
|
1332
|
+
}
|
|
1333
|
+
async function loadTypeScript() {
|
|
1334
|
+
const importTypeScript = new Function(
|
|
1335
|
+
"specifier",
|
|
1336
|
+
"return import(specifier)"
|
|
1337
|
+
);
|
|
1338
|
+
typescriptModulePromise ?? (typescriptModulePromise = importTypeScript("typescript").then((module2) => module2).catch(() => null));
|
|
1339
|
+
return typescriptModulePromise;
|
|
1340
|
+
}
|
|
1341
|
+
function isJsxSourceFile(file, code) {
|
|
1342
|
+
return /\.[cm]?[jt]sx$/.test(file) && code.includes("<");
|
|
1343
|
+
}
|
|
1344
|
+
function getSourceComponentInsertions(ts, sourceFile, componentAttribute, parentComponentAttribute) {
|
|
1345
|
+
const insertions = [];
|
|
1346
|
+
const visit = (node, currentComponent) => {
|
|
1347
|
+
const component = getComponentNameForNode(ts, node) ?? currentComponent;
|
|
1348
|
+
if (component && isIntrinsicJsxElement(ts, node, sourceFile) && !hasJsxAttribute(ts, node, componentAttribute) && !hasJsxAttribute(ts, node, "data-component")) {
|
|
1349
|
+
insertions.push({
|
|
1350
|
+
offset: node.tagName.end,
|
|
1351
|
+
value: ` ${componentAttribute}=${JSON.stringify(component)}`
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
if (component && isCustomJsxElement(ts, node, sourceFile) && !hasJsxAttribute(ts, node, parentComponentAttribute)) {
|
|
1355
|
+
insertions.push({
|
|
1356
|
+
offset: node.tagName.end,
|
|
1357
|
+
value: ` ${parentComponentAttribute}=${JSON.stringify(component)}`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
ts.forEachChild(node, (child) => visit(child, component));
|
|
1361
|
+
};
|
|
1362
|
+
visit(sourceFile, void 0);
|
|
1363
|
+
return insertions;
|
|
1364
|
+
}
|
|
1365
|
+
function getComponentNameForNode(ts, node) {
|
|
1366
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) {
|
|
1367
|
+
const name = node.name?.text;
|
|
1368
|
+
return isComponentName(name) ? name : void 0;
|
|
1369
|
+
}
|
|
1370
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
1371
|
+
const name = node.name.text;
|
|
1372
|
+
return isComponentName(name) && node.initializer ? name : void 0;
|
|
1373
|
+
}
|
|
1374
|
+
return void 0;
|
|
1375
|
+
}
|
|
1376
|
+
function isIntrinsicJsxElement(ts, node, sourceFile) {
|
|
1377
|
+
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
const tagName = node.tagName.getText(sourceFile);
|
|
1381
|
+
return /^[a-z]/.test(tagName) || tagName.includes("-");
|
|
1382
|
+
}
|
|
1383
|
+
function isCustomJsxElement(ts, node, sourceFile) {
|
|
1384
|
+
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
const tagName = node.tagName.getText(sourceFile);
|
|
1388
|
+
if (isReactRuntimeElementName(tagName)) return false;
|
|
1389
|
+
return /^[A-Z]/.test(tagName);
|
|
1390
|
+
}
|
|
1391
|
+
function isReactRuntimeElementName(tagName) {
|
|
1392
|
+
const name = tagName.startsWith("React.") ? tagName.slice("React.".length) : tagName;
|
|
1393
|
+
return name === "Fragment" || name === "StrictMode" || name === "Profiler";
|
|
1394
|
+
}
|
|
1395
|
+
function hasJsxAttribute(ts, node, name) {
|
|
1396
|
+
return node.attributes.properties.some(
|
|
1397
|
+
(property) => ts.isJsxAttribute(property) && property.name.getText() === name
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
function isComponentName(name) {
|
|
1401
|
+
return Boolean(name && /^[A-Z][A-Za-z0-9_$]*$/.test(name));
|
|
1402
|
+
}
|
|
1403
|
+
function applySourceComponentInsertions(code, insertions) {
|
|
1404
|
+
return insertions.slice().sort((a, b) => b.offset - a.offset).reduce(
|
|
1405
|
+
(nextCode, insertion) => `${nextCode.slice(0, insertion.offset)}${insertion.value}${nextCode.slice(
|
|
1406
|
+
insertion.offset
|
|
1407
|
+
)}`,
|
|
1408
|
+
code
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1218
1411
|
function matchesPath(matcher, absoluteFile, relativeFile) {
|
|
1219
1412
|
if (matcher.type === "regex") {
|
|
1220
1413
|
const regex = new RegExp(matcher.value, matcher.flags);
|
|
@@ -1242,7 +1435,12 @@ function createRuntimeOptions(options, config) {
|
|
|
1242
1435
|
column: options.column ?? true,
|
|
1243
1436
|
fileAttribute: `${attributePrefix}-file`,
|
|
1244
1437
|
lineAttribute: `${attributePrefix}-line`,
|
|
1245
|
-
columnAttribute: `${attributePrefix}-column
|
|
1438
|
+
columnAttribute: `${attributePrefix}-column`,
|
|
1439
|
+
componentAttribute: `${attributePrefix}-component`,
|
|
1440
|
+
parentFileAttribute: `${attributePrefix}-parent-file`,
|
|
1441
|
+
parentLineAttribute: `${attributePrefix}-parent-line`,
|
|
1442
|
+
parentColumnAttribute: `${attributePrefix}-parent-column`,
|
|
1443
|
+
parentComponentAttribute: `${attributePrefix}-parent-component`
|
|
1246
1444
|
};
|
|
1247
1445
|
}
|
|
1248
1446
|
function createRuntimeMatcher(pattern) {
|
|
@@ -1259,13 +1457,17 @@ function createJsxDevRuntime(options) {
|
|
|
1259
1457
|
import { Fragment, jsxDEV as baseJsxDEV } from 'react/jsx-dev-runtime';
|
|
1260
1458
|
|
|
1261
1459
|
const OPTIONS = ${JSON.stringify(options)};
|
|
1460
|
+
const sourceUsageStack = [];
|
|
1461
|
+
const sourceUsageWrapperCache = new WeakMap();
|
|
1262
1462
|
|
|
1263
1463
|
export { Fragment };
|
|
1264
1464
|
|
|
1265
1465
|
export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
1466
|
+
const sourceUsage = getSourceUsage(type, props, source);
|
|
1467
|
+
const nextType = sourceUsage ? getSourceUsageWrapper(type, sourceUsage) : type;
|
|
1266
1468
|
return baseJsxDEV(
|
|
1267
|
-
|
|
1268
|
-
injectSourceProps(type, props, source),
|
|
1469
|
+
nextType,
|
|
1470
|
+
injectSourceProps(type, props, source, sourceUsage),
|
|
1269
1471
|
key,
|
|
1270
1472
|
isStaticChildren,
|
|
1271
1473
|
source,
|
|
@@ -1273,14 +1475,18 @@ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
|
1273
1475
|
);
|
|
1274
1476
|
}
|
|
1275
1477
|
|
|
1276
|
-
function injectSourceProps(type, props, source) {
|
|
1277
|
-
if (typeof type !== 'string') return props;
|
|
1478
|
+
function injectSourceProps(type, props, source, sourceUsage) {
|
|
1278
1479
|
if (!source || typeof source.fileName !== 'string') return props;
|
|
1279
1480
|
|
|
1280
1481
|
const sourceFile = getSourceFile(source.fileName);
|
|
1281
1482
|
if (!sourceFile) return props;
|
|
1282
1483
|
|
|
1283
1484
|
const nextProps = props ? { ...props } : {};
|
|
1485
|
+
if (typeof type !== 'string') {
|
|
1486
|
+
injectParentSourceProps(nextProps, sourceUsage);
|
|
1487
|
+
return nextProps;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1284
1490
|
if (nextProps[OPTIONS.fileAttribute] == null) {
|
|
1285
1491
|
nextProps[OPTIONS.fileAttribute] = sourceFile;
|
|
1286
1492
|
}
|
|
@@ -1290,10 +1496,100 @@ function injectSourceProps(type, props, source) {
|
|
|
1290
1496
|
if (OPTIONS.column && source.columnNumber != null && nextProps[OPTIONS.columnAttribute] == null) {
|
|
1291
1497
|
nextProps[OPTIONS.columnAttribute] = String(source.columnNumber);
|
|
1292
1498
|
}
|
|
1499
|
+
injectParentSourceProps(nextProps, getCurrentSourceUsage());
|
|
1293
1500
|
|
|
1294
1501
|
return nextProps;
|
|
1295
1502
|
}
|
|
1296
1503
|
|
|
1504
|
+
function getSourceUsage(type, props, source) {
|
|
1505
|
+
if (!isSourceUsageComponentType(type)) return null;
|
|
1506
|
+
if (!source || typeof source.fileName !== 'string') return null;
|
|
1507
|
+
|
|
1508
|
+
const sourceFile = getSourceFile(source.fileName);
|
|
1509
|
+
if (!sourceFile) return null;
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
file: sourceFile,
|
|
1513
|
+
line: OPTIONS.line && source.lineNumber != null ? String(source.lineNumber) : '',
|
|
1514
|
+
column: OPTIONS.column && source.columnNumber != null ? String(source.columnNumber) : '',
|
|
1515
|
+
component: readSourceUsageComponent(props),
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function isSourceUsageComponentType(type) {
|
|
1520
|
+
return (
|
|
1521
|
+
typeof type === 'function' &&
|
|
1522
|
+
!isClassComponent(type)
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function isClassComponent(type) {
|
|
1527
|
+
return Boolean(type?.prototype?.isReactComponent);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function getSourceUsageWrapper(type, usage) {
|
|
1531
|
+
let wrappers = sourceUsageWrapperCache.get(type);
|
|
1532
|
+
if (!wrappers) {
|
|
1533
|
+
wrappers = new Map();
|
|
1534
|
+
sourceUsageWrapperCache.set(type, wrappers);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const key = getSourceUsageKey(usage);
|
|
1538
|
+
const existing = wrappers.get(key);
|
|
1539
|
+
if (existing) return existing;
|
|
1540
|
+
|
|
1541
|
+
const wrapped = function ReviewSourceUsageWrapper(props) {
|
|
1542
|
+
sourceUsageStack.push(usage);
|
|
1543
|
+
try {
|
|
1544
|
+
return type(props);
|
|
1545
|
+
} finally {
|
|
1546
|
+
sourceUsageStack.pop();
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
wrapped.displayName = 'ReviewSourceUsage(' + getComponentDisplayName(type) + ')';
|
|
1550
|
+
wrappers.set(key, wrapped);
|
|
1551
|
+
return wrapped;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function getComponentDisplayName(type) {
|
|
1555
|
+
return type.displayName || type.name || 'Component';
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function getSourceUsageKey(usage) {
|
|
1559
|
+
return [
|
|
1560
|
+
usage.file,
|
|
1561
|
+
usage.line,
|
|
1562
|
+
usage.column,
|
|
1563
|
+
usage.component,
|
|
1564
|
+
].join('|');
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function getCurrentSourceUsage() {
|
|
1568
|
+
return sourceUsageStack[sourceUsageStack.length - 1] || null;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function readSourceUsageComponent(props) {
|
|
1572
|
+
const value = props?.[OPTIONS.parentComponentAttribute];
|
|
1573
|
+
return typeof value === 'string' ? value : '';
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function injectParentSourceProps(props, usage) {
|
|
1577
|
+
if (!usage?.file) return;
|
|
1578
|
+
|
|
1579
|
+
if (props[OPTIONS.parentFileAttribute] == null) {
|
|
1580
|
+
props[OPTIONS.parentFileAttribute] = usage.file;
|
|
1581
|
+
}
|
|
1582
|
+
if (usage.line && props[OPTIONS.parentLineAttribute] == null) {
|
|
1583
|
+
props[OPTIONS.parentLineAttribute] = usage.line;
|
|
1584
|
+
}
|
|
1585
|
+
if (usage.column && props[OPTIONS.parentColumnAttribute] == null) {
|
|
1586
|
+
props[OPTIONS.parentColumnAttribute] = usage.column;
|
|
1587
|
+
}
|
|
1588
|
+
if (usage.component && props[OPTIONS.parentComponentAttribute] == null) {
|
|
1589
|
+
props[OPTIONS.parentComponentAttribute] = usage.component;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1297
1593
|
function getSourceFile(fileName) {
|
|
1298
1594
|
const absoluteFile = normalizePath(fileName);
|
|
1299
1595
|
const relativeFile = getRelativeFile(absoluteFile);
|