@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/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
- dataFile,
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
446
- const image = await createReviewFigmaImage({
447
- assetDir,
448
- assetEndpoint,
449
- currentImages: data.images,
450
- env,
451
- input,
452
- options,
453
- requestToken
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
466
- const images = reorderReviewFigmaImages(data.images, input);
467
- data.images = images.allImages;
468
- await writeReviewFigmaImageStoreFile(dataFile, data);
469
- return { status: 200, body: images.targetImages };
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
476
- const result = updateReviewFigmaImage(data.images, id, patch);
477
- if (!result) return jsonError(404, `Figma image not found: ${id}`);
478
- if (!isAllowedProjectTarget(result.image.target, options.projectId)) {
479
- return jsonError(403, "target project is not allowed.");
480
- }
481
- data.images = result.images;
482
- await writeReviewFigmaImageStoreFile(dataFile, data);
483
- return { status: 200, body: result.image };
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
- const data = await readReviewFigmaImageStoreFile(dataFile);
487
- const image = data.images.find((item) => item.id === id);
488
- if (!image) return jsonError(404, `Figma image not found: ${id}`);
489
- if (!isAllowedProjectTarget(image.target, options.projectId)) {
490
- return jsonError(403, "target project is not allowed.");
491
- }
492
- data.images = data.images.filter((item) => item.id !== id);
493
- await writeReviewFigmaImageStoreFile(dataFile, data);
494
- await deleteReviewFigmaImageAsset(assetDir, image.storageKey);
495
- return { status: 200, body: { ok: true } };
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
- async function readJsonRequestBody(req) {
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
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
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
- return JSON.parse(raw);
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
- return injectedCode ? { code: injectedCode, map: null } : null;
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
- type,
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);