@designfever/web-review-kit 0.7.3 → 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/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
- dataFile,
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
786
- const image = await createReviewFigmaImage({
787
- assetDir,
788
- assetEndpoint,
789
- currentImages: data.images,
790
- env,
791
- input,
792
- options,
793
- requestToken
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
806
- const images = reorderReviewFigmaImages(data.images, input);
807
- data.images = images.allImages;
808
- await writeReviewFigmaImageStoreFile(dataFile, data);
809
- return { status: 200, body: images.targetImages };
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
816
- const result = updateReviewFigmaImage(data.images, id, patch);
817
- if (!result) return jsonError(404, `Figma image not found: ${id}`);
818
- if (!isAllowedProjectTarget(result.image.target, options.projectId)) {
819
- return jsonError(403, "target project is not allowed.");
820
- }
821
- data.images = result.images;
822
- await writeReviewFigmaImageStoreFile(dataFile, data);
823
- return { status: 200, body: result.image };
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
827
- const image = data.images.find((item) => item.id === id);
828
- if (!image) return jsonError(404, `Figma image not found: ${id}`);
829
- if (!isAllowedProjectTarget(image.target, options.projectId)) {
830
- return jsonError(403, "target project is not allowed.");
831
- }
832
- data.images = data.images.filter((item) => item.id !== id);
833
- await writeReviewFigmaImageStoreFile(dataFile, data);
834
- await deleteReviewFigmaImageAsset(assetDir, image.storageKey);
835
- return { status: 200, body: { ok: true } };
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
- async function readJsonRequestBody(req) {
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
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
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
- return JSON.parse(raw);
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
- return injectedCode ? { code: injectedCode, map: null } : null;
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
- type,
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);