@designfever/web-review-kit 0.7.3 → 0.8.1
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 +8 -4
- package/dist/{chunk-RPVLRULC.js → chunk-4ZP7B7R6.js} +2345 -1786
- package/dist/chunk-4ZP7B7R6.js.map +1 -0
- package/dist/index.cjs +2348 -1793
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/react-shell.cjs +14936 -11239
- package/dist/react-shell.cjs.map +1 -1
- package/dist/react-shell.d.cts +6 -4
- package/dist/react-shell.d.ts +6 -4
- package/dist/react-shell.js +11615 -8399
- 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 +345 -49
- package/dist/vite.js.map +1 -1
- package/docs/README.md +20 -13
- package/docs/adapters.md +27 -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 +46 -8
- package/docs/release-notes-0.8.0.md +175 -0
- package/docs/release-notes-0.8.1.md +23 -0
- package/docs/testing.md +68 -0
- package/package.json +14 -5
- 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.js
CHANGED
|
@@ -32,8 +32,18 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
32
32
|
import path2 from "path";
|
|
33
33
|
|
|
34
34
|
// src/vite/figma-image-store.image.ts
|
|
35
|
-
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
35
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
36
36
|
import path from "path";
|
|
37
|
+
var reviewImageStoreTaskQueues = /* @__PURE__ */ new Map();
|
|
38
|
+
function runExclusiveReviewImageStoreTask(dataFile, task) {
|
|
39
|
+
const previous = reviewImageStoreTaskQueues.get(dataFile) ?? Promise.resolve();
|
|
40
|
+
const result = previous.then(task, task);
|
|
41
|
+
reviewImageStoreTaskQueues.set(
|
|
42
|
+
dataFile,
|
|
43
|
+
result.catch(() => void 0)
|
|
44
|
+
);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
37
47
|
function requireReviewFigmaRequestToken({
|
|
38
48
|
enabled,
|
|
39
49
|
env,
|
|
@@ -360,12 +370,19 @@ async function readReviewFigmaImageStoreFile(dataFile) {
|
|
|
360
370
|
}
|
|
361
371
|
async function writeReviewFigmaImageStoreFile(dataFile, data) {
|
|
362
372
|
await mkdir(path.dirname(dataFile), { recursive: true });
|
|
373
|
+
const tempFile = `${dataFile}.${process.pid}.${Date.now()}.tmp`;
|
|
363
374
|
await writeFile(
|
|
364
|
-
|
|
375
|
+
tempFile,
|
|
365
376
|
`${JSON.stringify({ version: 1, images: data.images }, null, 2)}
|
|
366
377
|
`,
|
|
367
378
|
"utf8"
|
|
368
379
|
);
|
|
380
|
+
try {
|
|
381
|
+
await rename(tempFile, dataFile);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
await rm(tempFile, { force: true });
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
369
386
|
}
|
|
370
387
|
function getNextImageOrder(images, target) {
|
|
371
388
|
const targetImages = listImagesForTarget(images, target);
|
|
@@ -442,19 +459,21 @@ async function handleReviewFigmaImageStoreRequest({
|
|
|
442
459
|
if (!isAllowedProjectTarget(input.target, options.projectId)) {
|
|
443
460
|
return jsonError(403, "target project is not allowed.");
|
|
444
461
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
462
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
463
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
464
|
+
const image = await createReviewFigmaImage({
|
|
465
|
+
assetDir,
|
|
466
|
+
assetEndpoint,
|
|
467
|
+
currentImages: data.images,
|
|
468
|
+
env,
|
|
469
|
+
input,
|
|
470
|
+
options,
|
|
471
|
+
requestToken
|
|
472
|
+
});
|
|
473
|
+
data.images = [image, ...data.images];
|
|
474
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
475
|
+
return { status: 201, body: image };
|
|
454
476
|
});
|
|
455
|
-
data.images = [image, ...data.images];
|
|
456
|
-
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
457
|
-
return { status: 201, body: image };
|
|
458
477
|
}
|
|
459
478
|
if (method === "PATCH" && pathname === `${endpoint}/reorder`) {
|
|
460
479
|
const input = parseReorderImagesInput(body);
|
|
@@ -462,49 +481,109 @@ async function handleReviewFigmaImageStoreRequest({
|
|
|
462
481
|
if (!isAllowedProjectTarget(input.target, options.projectId)) {
|
|
463
482
|
return jsonError(403, "target project is not allowed.");
|
|
464
483
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
484
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
485
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
486
|
+
const images = reorderReviewFigmaImages(data.images, input);
|
|
487
|
+
data.images = images.allImages;
|
|
488
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
489
|
+
return { status: 200, body: images.targetImages };
|
|
490
|
+
});
|
|
470
491
|
}
|
|
471
492
|
const id = getEndpointItemId(pathname, endpoint);
|
|
472
493
|
if (id && method === "PATCH") {
|
|
473
494
|
const patch = parseUpdateImageInput(body);
|
|
474
495
|
if (!patch) return jsonError(400, "valid update patch is required.");
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
496
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
497
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
498
|
+
const result = updateReviewFigmaImage(data.images, id, patch);
|
|
499
|
+
if (!result) return jsonError(404, `Figma image not found: ${id}`);
|
|
500
|
+
if (!isAllowedProjectTarget(result.image.target, options.projectId)) {
|
|
501
|
+
return jsonError(403, "target project is not allowed.");
|
|
502
|
+
}
|
|
503
|
+
data.images = result.images;
|
|
504
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
505
|
+
return { status: 200, body: result.image };
|
|
506
|
+
});
|
|
484
507
|
}
|
|
485
508
|
if (id && method === "DELETE") {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
509
|
+
return runExclusiveReviewImageStoreTask(dataFile, async () => {
|
|
510
|
+
const data = await readReviewFigmaImageStoreFile(dataFile);
|
|
511
|
+
const image = data.images.find((item) => item.id === id);
|
|
512
|
+
if (!image) return jsonError(404, `Figma image not found: ${id}`);
|
|
513
|
+
if (!isAllowedProjectTarget(image.target, options.projectId)) {
|
|
514
|
+
return jsonError(403, "target project is not allowed.");
|
|
515
|
+
}
|
|
516
|
+
data.images = data.images.filter((item) => item.id !== id);
|
|
517
|
+
await writeReviewFigmaImageStoreFile(dataFile, data);
|
|
518
|
+
await deleteReviewFigmaImageAsset(assetDir, image.storageKey);
|
|
519
|
+
return { status: 200, body: { ok: true } };
|
|
520
|
+
});
|
|
496
521
|
}
|
|
497
522
|
return jsonError(405, "method not allowed.");
|
|
498
523
|
}
|
|
499
|
-
|
|
524
|
+
var DEFAULT_REVIEW_IMAGE_STORE_MAX_BODY_BYTES = 25 * 1024 * 1024;
|
|
525
|
+
var ReviewImageStoreRequestError = class extends Error {
|
|
526
|
+
constructor(status, message) {
|
|
527
|
+
super(message);
|
|
528
|
+
this.status = status;
|
|
529
|
+
this.name = "ReviewImageStoreRequestError";
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function isReviewImageStoreRequestError(error) {
|
|
533
|
+
return error instanceof ReviewImageStoreRequestError;
|
|
534
|
+
}
|
|
535
|
+
function assertTrustedReviewImageStoreRequest(req) {
|
|
536
|
+
const origin = req.headers.origin;
|
|
537
|
+
if (!origin || typeof origin !== "string") return;
|
|
538
|
+
let originUrl;
|
|
539
|
+
try {
|
|
540
|
+
originUrl = new URL(origin);
|
|
541
|
+
} catch {
|
|
542
|
+
throw new ReviewImageStoreRequestError(
|
|
543
|
+
403,
|
|
544
|
+
"Cross-origin request is not allowed."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const host = req.headers.host;
|
|
548
|
+
if (host && originUrl.host === host) return;
|
|
549
|
+
if (isLoopbackHostname(originUrl.hostname)) return;
|
|
550
|
+
throw new ReviewImageStoreRequestError(
|
|
551
|
+
403,
|
|
552
|
+
"Cross-origin request is not allowed."
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
function isLoopbackHostname(hostname) {
|
|
556
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
557
|
+
}
|
|
558
|
+
async function readJsonRequestBody(req, maxBytes = DEFAULT_REVIEW_IMAGE_STORE_MAX_BODY_BYTES) {
|
|
500
559
|
if (req.method === "GET" || req.method === "DELETE") return null;
|
|
501
560
|
const chunks = [];
|
|
561
|
+
let totalBytes = 0;
|
|
502
562
|
for await (const chunk of req) {
|
|
503
|
-
|
|
563
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
564
|
+
totalBytes += buffer.length;
|
|
565
|
+
if (totalBytes > maxBytes) {
|
|
566
|
+
throw new ReviewImageStoreRequestError(
|
|
567
|
+
413,
|
|
568
|
+
`Request body exceeds ${maxBytes} bytes.`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
chunks.push(buffer);
|
|
504
572
|
}
|
|
505
573
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
506
574
|
if (!raw) return null;
|
|
507
|
-
|
|
575
|
+
const contentType = req.headers["content-type"];
|
|
576
|
+
if (typeof contentType !== "string" || !contentType.split(";")[0].trim().toLowerCase().endsWith("/json")) {
|
|
577
|
+
throw new ReviewImageStoreRequestError(
|
|
578
|
+
415,
|
|
579
|
+
"Request body must be application/json."
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
return JSON.parse(raw);
|
|
584
|
+
} catch {
|
|
585
|
+
throw new ReviewImageStoreRequestError(400, "Invalid JSON request body.");
|
|
586
|
+
}
|
|
508
587
|
}
|
|
509
588
|
function sendJson(res, status, body) {
|
|
510
589
|
res.statusCode = status;
|
|
@@ -742,6 +821,7 @@ var reviewFigmaImageStore = (options = {}) => {
|
|
|
742
821
|
return;
|
|
743
822
|
}
|
|
744
823
|
try {
|
|
824
|
+
assertTrustedReviewImageStoreRequest(req);
|
|
745
825
|
const response = await handleReviewFigmaImageStoreRequest({
|
|
746
826
|
dataFile,
|
|
747
827
|
assetDir,
|
|
@@ -752,11 +832,15 @@ var reviewFigmaImageStore = (options = {}) => {
|
|
|
752
832
|
pathname,
|
|
753
833
|
requestUrl,
|
|
754
834
|
method: req.method ?? "GET",
|
|
755
|
-
body: await readJsonRequestBody(req),
|
|
835
|
+
body: await readJsonRequestBody(req, options.maxRequestBytes),
|
|
756
836
|
requestToken: readRequestFigmaToken(req)
|
|
757
837
|
});
|
|
758
838
|
sendJson(res, response.status, response.body);
|
|
759
839
|
} catch (error) {
|
|
840
|
+
if (isReviewImageStoreRequestError(error)) {
|
|
841
|
+
sendJson(res, error.status, { error: error.message });
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
760
844
|
sendJson(res, 500, {
|
|
761
845
|
error: error instanceof Error ? error.message : "Figma image store request failed."
|
|
762
846
|
});
|
|
@@ -777,6 +861,7 @@ function getServerEnv() {
|
|
|
777
861
|
|
|
778
862
|
// src/vite.ts
|
|
779
863
|
var VIRTUAL_JSX_DEV_RUNTIME_ID = "\0@designfever/web-review-kit/source-locator/jsx-dev-runtime";
|
|
864
|
+
var typescriptModulePromise;
|
|
780
865
|
var REVIEW_SOURCE_ENV_DEFINE_KEYS = [
|
|
781
866
|
["__DF_WRK_REVIEW_SOURCE_ROOT__", "VITE_REVIEW_SOURCE_ROOT"],
|
|
782
867
|
["__DF_WRK_REVIEW_SOURCE_EDITOR__", "VITE_REVIEW_SOURCE_EDITOR"],
|
|
@@ -820,9 +905,11 @@ var reviewSourceLocator = (options = {}) => {
|
|
|
820
905
|
if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
|
|
821
906
|
return createJsxDevRuntime(runtimeOptions);
|
|
822
907
|
},
|
|
823
|
-
transform(code) {
|
|
908
|
+
async transform(code, id) {
|
|
824
909
|
const injectedCode = injectReviewSourceEnv(code, sourceEnvReplacements);
|
|
825
|
-
|
|
910
|
+
const inputCode = injectedCode ?? code;
|
|
911
|
+
const componentInjectedCode = runtimeOptions.enabled ? await injectReviewSourceComponentHints(inputCode, id, runtimeOptions) : null;
|
|
912
|
+
return injectedCode || componentInjectedCode ? { code: componentInjectedCode ?? inputCode, map: null } : null;
|
|
826
913
|
}
|
|
827
914
|
};
|
|
828
915
|
};
|
|
@@ -875,6 +962,112 @@ var reviewDataLocator = (options = {}) => {
|
|
|
875
962
|
}
|
|
876
963
|
};
|
|
877
964
|
};
|
|
965
|
+
async function injectReviewSourceComponentHints(code, id, options) {
|
|
966
|
+
const file = normalizePath(id.split("?")[0]);
|
|
967
|
+
if (!isJsxSourceFile(file, code)) return null;
|
|
968
|
+
const relativeFile = options.root && file.startsWith(options.root + "/") ? file.slice(options.root.length + 1) : file;
|
|
969
|
+
if (options.include.length > 0 && !options.include.some((matcher) => matchesPath(matcher, file, relativeFile))) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
if (options.exclude.some((matcher) => matchesPath(matcher, file, relativeFile))) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const ts = await loadTypeScript();
|
|
976
|
+
if (!ts) return null;
|
|
977
|
+
const sourceFile = ts.createSourceFile(
|
|
978
|
+
file,
|
|
979
|
+
code,
|
|
980
|
+
ts.ScriptTarget.Latest,
|
|
981
|
+
true,
|
|
982
|
+
file.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TSX
|
|
983
|
+
);
|
|
984
|
+
const insertions = getSourceComponentInsertions(
|
|
985
|
+
ts,
|
|
986
|
+
sourceFile,
|
|
987
|
+
options.componentAttribute,
|
|
988
|
+
options.parentComponentAttribute
|
|
989
|
+
);
|
|
990
|
+
if (insertions.length === 0) return null;
|
|
991
|
+
return applySourceComponentInsertions(code, insertions);
|
|
992
|
+
}
|
|
993
|
+
async function loadTypeScript() {
|
|
994
|
+
const importTypeScript = new Function(
|
|
995
|
+
"specifier",
|
|
996
|
+
"return import(specifier)"
|
|
997
|
+
);
|
|
998
|
+
typescriptModulePromise ?? (typescriptModulePromise = importTypeScript("typescript").then((module) => module).catch(() => null));
|
|
999
|
+
return typescriptModulePromise;
|
|
1000
|
+
}
|
|
1001
|
+
function isJsxSourceFile(file, code) {
|
|
1002
|
+
return /\.[cm]?[jt]sx$/.test(file) && code.includes("<");
|
|
1003
|
+
}
|
|
1004
|
+
function getSourceComponentInsertions(ts, sourceFile, componentAttribute, parentComponentAttribute) {
|
|
1005
|
+
const insertions = [];
|
|
1006
|
+
const visit = (node, currentComponent) => {
|
|
1007
|
+
const component = getComponentNameForNode(ts, node) ?? currentComponent;
|
|
1008
|
+
if (component && isIntrinsicJsxElement(ts, node, sourceFile) && !hasJsxAttribute(ts, node, componentAttribute) && !hasJsxAttribute(ts, node, "data-component")) {
|
|
1009
|
+
insertions.push({
|
|
1010
|
+
offset: node.tagName.end,
|
|
1011
|
+
value: ` ${componentAttribute}=${JSON.stringify(component)}`
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
if (component && isCustomJsxElement(ts, node, sourceFile) && !hasJsxAttribute(ts, node, parentComponentAttribute)) {
|
|
1015
|
+
insertions.push({
|
|
1016
|
+
offset: node.tagName.end,
|
|
1017
|
+
value: ` ${parentComponentAttribute}=${JSON.stringify(component)}`
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
ts.forEachChild(node, (child) => visit(child, component));
|
|
1021
|
+
};
|
|
1022
|
+
visit(sourceFile, void 0);
|
|
1023
|
+
return insertions;
|
|
1024
|
+
}
|
|
1025
|
+
function getComponentNameForNode(ts, node) {
|
|
1026
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) {
|
|
1027
|
+
const name = node.name?.text;
|
|
1028
|
+
return isComponentName(name) ? name : void 0;
|
|
1029
|
+
}
|
|
1030
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
1031
|
+
const name = node.name.text;
|
|
1032
|
+
return isComponentName(name) && node.initializer ? name : void 0;
|
|
1033
|
+
}
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
function isIntrinsicJsxElement(ts, node, sourceFile) {
|
|
1037
|
+
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
const tagName = node.tagName.getText(sourceFile);
|
|
1041
|
+
return /^[a-z]/.test(tagName) || tagName.includes("-");
|
|
1042
|
+
}
|
|
1043
|
+
function isCustomJsxElement(ts, node, sourceFile) {
|
|
1044
|
+
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
const tagName = node.tagName.getText(sourceFile);
|
|
1048
|
+
if (isReactRuntimeElementName(tagName)) return false;
|
|
1049
|
+
return /^[A-Z]/.test(tagName);
|
|
1050
|
+
}
|
|
1051
|
+
function isReactRuntimeElementName(tagName) {
|
|
1052
|
+
const name = tagName.startsWith("React.") ? tagName.slice("React.".length) : tagName;
|
|
1053
|
+
return name === "Fragment" || name === "StrictMode" || name === "Profiler";
|
|
1054
|
+
}
|
|
1055
|
+
function hasJsxAttribute(ts, node, name) {
|
|
1056
|
+
return node.attributes.properties.some(
|
|
1057
|
+
(property) => ts.isJsxAttribute(property) && property.name.getText() === name
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
function isComponentName(name) {
|
|
1061
|
+
return Boolean(name && /^[A-Z][A-Za-z0-9_$]*$/.test(name));
|
|
1062
|
+
}
|
|
1063
|
+
function applySourceComponentInsertions(code, insertions) {
|
|
1064
|
+
return insertions.slice().sort((a, b) => b.offset - a.offset).reduce(
|
|
1065
|
+
(nextCode, insertion) => `${nextCode.slice(0, insertion.offset)}${insertion.value}${nextCode.slice(
|
|
1066
|
+
insertion.offset
|
|
1067
|
+
)}`,
|
|
1068
|
+
code
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
878
1071
|
function matchesPath(matcher, absoluteFile, relativeFile) {
|
|
879
1072
|
if (matcher.type === "regex") {
|
|
880
1073
|
const regex = new RegExp(matcher.value, matcher.flags);
|
|
@@ -902,7 +1095,12 @@ function createRuntimeOptions(options, config) {
|
|
|
902
1095
|
column: options.column ?? true,
|
|
903
1096
|
fileAttribute: `${attributePrefix}-file`,
|
|
904
1097
|
lineAttribute: `${attributePrefix}-line`,
|
|
905
|
-
columnAttribute: `${attributePrefix}-column
|
|
1098
|
+
columnAttribute: `${attributePrefix}-column`,
|
|
1099
|
+
componentAttribute: `${attributePrefix}-component`,
|
|
1100
|
+
parentFileAttribute: `${attributePrefix}-parent-file`,
|
|
1101
|
+
parentLineAttribute: `${attributePrefix}-parent-line`,
|
|
1102
|
+
parentColumnAttribute: `${attributePrefix}-parent-column`,
|
|
1103
|
+
parentComponentAttribute: `${attributePrefix}-parent-component`
|
|
906
1104
|
};
|
|
907
1105
|
}
|
|
908
1106
|
function createRuntimeMatcher(pattern) {
|
|
@@ -919,13 +1117,17 @@ function createJsxDevRuntime(options) {
|
|
|
919
1117
|
import { Fragment, jsxDEV as baseJsxDEV } from 'react/jsx-dev-runtime';
|
|
920
1118
|
|
|
921
1119
|
const OPTIONS = ${JSON.stringify(options)};
|
|
1120
|
+
const sourceUsageStack = [];
|
|
1121
|
+
const sourceUsageWrapperCache = new WeakMap();
|
|
922
1122
|
|
|
923
1123
|
export { Fragment };
|
|
924
1124
|
|
|
925
1125
|
export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
1126
|
+
const sourceUsage = getSourceUsage(type, props, source);
|
|
1127
|
+
const nextType = sourceUsage ? getSourceUsageWrapper(type, sourceUsage) : type;
|
|
926
1128
|
return baseJsxDEV(
|
|
927
|
-
|
|
928
|
-
injectSourceProps(type, props, source),
|
|
1129
|
+
nextType,
|
|
1130
|
+
injectSourceProps(type, props, source, sourceUsage),
|
|
929
1131
|
key,
|
|
930
1132
|
isStaticChildren,
|
|
931
1133
|
source,
|
|
@@ -933,14 +1135,18 @@ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
|
933
1135
|
);
|
|
934
1136
|
}
|
|
935
1137
|
|
|
936
|
-
function injectSourceProps(type, props, source) {
|
|
937
|
-
if (typeof type !== 'string') return props;
|
|
1138
|
+
function injectSourceProps(type, props, source, sourceUsage) {
|
|
938
1139
|
if (!source || typeof source.fileName !== 'string') return props;
|
|
939
1140
|
|
|
940
1141
|
const sourceFile = getSourceFile(source.fileName);
|
|
941
1142
|
if (!sourceFile) return props;
|
|
942
1143
|
|
|
943
1144
|
const nextProps = props ? { ...props } : {};
|
|
1145
|
+
if (typeof type !== 'string') {
|
|
1146
|
+
injectParentSourceProps(nextProps, sourceUsage);
|
|
1147
|
+
return nextProps;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
944
1150
|
if (nextProps[OPTIONS.fileAttribute] == null) {
|
|
945
1151
|
nextProps[OPTIONS.fileAttribute] = sourceFile;
|
|
946
1152
|
}
|
|
@@ -950,10 +1156,100 @@ function injectSourceProps(type, props, source) {
|
|
|
950
1156
|
if (OPTIONS.column && source.columnNumber != null && nextProps[OPTIONS.columnAttribute] == null) {
|
|
951
1157
|
nextProps[OPTIONS.columnAttribute] = String(source.columnNumber);
|
|
952
1158
|
}
|
|
1159
|
+
injectParentSourceProps(nextProps, getCurrentSourceUsage());
|
|
953
1160
|
|
|
954
1161
|
return nextProps;
|
|
955
1162
|
}
|
|
956
1163
|
|
|
1164
|
+
function getSourceUsage(type, props, source) {
|
|
1165
|
+
if (!isSourceUsageComponentType(type)) return null;
|
|
1166
|
+
if (!source || typeof source.fileName !== 'string') return null;
|
|
1167
|
+
|
|
1168
|
+
const sourceFile = getSourceFile(source.fileName);
|
|
1169
|
+
if (!sourceFile) return null;
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
file: sourceFile,
|
|
1173
|
+
line: OPTIONS.line && source.lineNumber != null ? String(source.lineNumber) : '',
|
|
1174
|
+
column: OPTIONS.column && source.columnNumber != null ? String(source.columnNumber) : '',
|
|
1175
|
+
component: readSourceUsageComponent(props),
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function isSourceUsageComponentType(type) {
|
|
1180
|
+
return (
|
|
1181
|
+
typeof type === 'function' &&
|
|
1182
|
+
!isClassComponent(type)
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function isClassComponent(type) {
|
|
1187
|
+
return Boolean(type?.prototype?.isReactComponent);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function getSourceUsageWrapper(type, usage) {
|
|
1191
|
+
let wrappers = sourceUsageWrapperCache.get(type);
|
|
1192
|
+
if (!wrappers) {
|
|
1193
|
+
wrappers = new Map();
|
|
1194
|
+
sourceUsageWrapperCache.set(type, wrappers);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const key = getSourceUsageKey(usage);
|
|
1198
|
+
const existing = wrappers.get(key);
|
|
1199
|
+
if (existing) return existing;
|
|
1200
|
+
|
|
1201
|
+
const wrapped = function ReviewSourceUsageWrapper(props) {
|
|
1202
|
+
sourceUsageStack.push(usage);
|
|
1203
|
+
try {
|
|
1204
|
+
return type(props);
|
|
1205
|
+
} finally {
|
|
1206
|
+
sourceUsageStack.pop();
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
wrapped.displayName = 'ReviewSourceUsage(' + getComponentDisplayName(type) + ')';
|
|
1210
|
+
wrappers.set(key, wrapped);
|
|
1211
|
+
return wrapped;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function getComponentDisplayName(type) {
|
|
1215
|
+
return type.displayName || type.name || 'Component';
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function getSourceUsageKey(usage) {
|
|
1219
|
+
return [
|
|
1220
|
+
usage.file,
|
|
1221
|
+
usage.line,
|
|
1222
|
+
usage.column,
|
|
1223
|
+
usage.component,
|
|
1224
|
+
].join('|');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function getCurrentSourceUsage() {
|
|
1228
|
+
return sourceUsageStack[sourceUsageStack.length - 1] || null;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function readSourceUsageComponent(props) {
|
|
1232
|
+
const value = props?.[OPTIONS.parentComponentAttribute];
|
|
1233
|
+
return typeof value === 'string' ? value : '';
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function injectParentSourceProps(props, usage) {
|
|
1237
|
+
if (!usage?.file) return;
|
|
1238
|
+
|
|
1239
|
+
if (props[OPTIONS.parentFileAttribute] == null) {
|
|
1240
|
+
props[OPTIONS.parentFileAttribute] = usage.file;
|
|
1241
|
+
}
|
|
1242
|
+
if (usage.line && props[OPTIONS.parentLineAttribute] == null) {
|
|
1243
|
+
props[OPTIONS.parentLineAttribute] = usage.line;
|
|
1244
|
+
}
|
|
1245
|
+
if (usage.column && props[OPTIONS.parentColumnAttribute] == null) {
|
|
1246
|
+
props[OPTIONS.parentColumnAttribute] = usage.column;
|
|
1247
|
+
}
|
|
1248
|
+
if (usage.component && props[OPTIONS.parentComponentAttribute] == null) {
|
|
1249
|
+
props[OPTIONS.parentComponentAttribute] = usage.component;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
957
1253
|
function getSourceFile(fileName) {
|
|
958
1254
|
const absoluteFile = normalizePath(fileName);
|
|
959
1255
|
const relativeFile = getRelativeFile(absoluteFile);
|