@gtkx/cli 2.0.0-beta.2 → 2.0.0-beta.4

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.
Files changed (144) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +12 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/codegen/config-dependencies.d.ts +3 -0
  6. package/dist/codegen/config-dependencies.d.ts.map +1 -0
  7. package/dist/codegen/config-dependencies.js +355 -0
  8. package/dist/codegen/config-dependencies.js.map +1 -0
  9. package/dist/codegen/reference.d.ts.map +1 -1
  10. package/dist/codegen/reference.js +1 -0
  11. package/dist/codegen/reference.js.map +1 -1
  12. package/dist/codegen/run-codegen.d.ts +3 -2
  13. package/dist/codegen/run-codegen.d.ts.map +1 -1
  14. package/dist/codegen/run-codegen.js +33 -14
  15. package/dist/codegen/run-codegen.js.map +1 -1
  16. package/dist/codegen/store-resolver.d.ts +2 -0
  17. package/dist/codegen/store-resolver.d.ts.map +1 -1
  18. package/dist/codegen/store-resolver.js +6 -3
  19. package/dist/codegen/store-resolver.js.map +1 -1
  20. package/dist/commands/cleanup.d.ts +9 -0
  21. package/dist/commands/cleanup.d.ts.map +1 -0
  22. package/dist/commands/cleanup.js +31 -0
  23. package/dist/commands/cleanup.js.map +1 -0
  24. package/dist/commands/dev.d.ts +1 -0
  25. package/dist/commands/dev.d.ts.map +1 -1
  26. package/dist/commands/dev.js +13 -4
  27. package/dist/commands/dev.js.map +1 -1
  28. package/dist/commands/docs.d.ts.map +1 -1
  29. package/dist/commands/docs.js +1 -0
  30. package/dist/commands/docs.js.map +1 -1
  31. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  32. package/dist/deploy/freedesktop/validate.js +11 -4
  33. package/dist/deploy/freedesktop/validate.js.map +1 -1
  34. package/dist/deploy/nfpm/config.d.ts +3 -2
  35. package/dist/deploy/nfpm/config.d.ts.map +1 -1
  36. package/dist/deploy/nfpm/config.js +3 -3
  37. package/dist/deploy/nfpm/config.js.map +1 -1
  38. package/dist/deploy/node-runtime/index.d.ts.map +1 -1
  39. package/dist/deploy/node-runtime/index.js +2 -1
  40. package/dist/deploy/node-runtime/index.js.map +1 -1
  41. package/dist/deploy/payload/stage.d.ts.map +1 -1
  42. package/dist/deploy/payload/stage.js +24 -3
  43. package/dist/deploy/payload/stage.js.map +1 -1
  44. package/dist/deploy/settings/extra-files.d.ts.map +1 -1
  45. package/dist/deploy/settings/extra-files.js +4 -3
  46. package/dist/deploy/settings/extra-files.js.map +1 -1
  47. package/dist/deploy/targets/nfpm-target.d.ts.map +1 -1
  48. package/dist/deploy/targets/nfpm-target.js +12 -7
  49. package/dist/deploy/targets/nfpm-target.js.map +1 -1
  50. package/dist/dev/entry-env.d.ts +2 -1
  51. package/dist/dev/entry-env.d.ts.map +1 -1
  52. package/dist/dev/entry-env.js +2 -1
  53. package/dist/dev/entry-env.js.map +1 -1
  54. package/dist/dev/runner-deps.d.ts +1 -1
  55. package/dist/dev/runner-deps.d.ts.map +1 -1
  56. package/dist/dev/runner-deps.js +3 -3
  57. package/dist/dev/runner-deps.js.map +1 -1
  58. package/dist/dev/runner-main.d.ts.map +1 -1
  59. package/dist/dev/runner-main.js +8 -3
  60. package/dist/dev/runner-main.js.map +1 -1
  61. package/dist/dev/supervisor.d.ts +3 -1
  62. package/dist/dev/supervisor.d.ts.map +1 -1
  63. package/dist/dev/supervisor.js +124 -27
  64. package/dist/dev/supervisor.js.map +1 -1
  65. package/dist/i18n/catalogs.d.ts.map +1 -1
  66. package/dist/i18n/catalogs.js +1 -0
  67. package/dist/i18n/catalogs.js.map +1 -1
  68. package/dist/i18n/source-messages.d.ts +3 -1
  69. package/dist/i18n/source-messages.d.ts.map +1 -1
  70. package/dist/i18n/source-messages.js +167 -37
  71. package/dist/i18n/source-messages.js.map +1 -1
  72. package/dist/internal/build-output.d.ts.map +1 -1
  73. package/dist/internal/build-output.js +53 -6
  74. package/dist/internal/build-output.js.map +1 -1
  75. package/dist/internal/docs-elements.d.ts +1 -0
  76. package/dist/internal/docs-elements.d.ts.map +1 -1
  77. package/dist/internal/docs-elements.js +5 -4
  78. package/dist/internal/docs-elements.js.map +1 -1
  79. package/dist/internal/errors.d.ts +2 -1
  80. package/dist/internal/errors.d.ts.map +1 -1
  81. package/dist/internal/errors.js +1 -1
  82. package/dist/internal/errors.js.map +1 -1
  83. package/dist/internal/parent-process.d.ts +12 -0
  84. package/dist/internal/parent-process.d.ts.map +1 -0
  85. package/dist/internal/parent-process.js +74 -0
  86. package/dist/internal/parent-process.js.map +1 -0
  87. package/dist/internal/prepare-project.d.ts +1 -0
  88. package/dist/internal/prepare-project.d.ts.map +1 -1
  89. package/dist/internal/prepare-project.js +8 -2
  90. package/dist/internal/prepare-project.js.map +1 -1
  91. package/dist/vite-plugins/ensure-store.d.ts +1 -1
  92. package/dist/vite-plugins/ensure-store.d.ts.map +1 -1
  93. package/dist/vite-plugins/ensure-store.js +2 -2
  94. package/dist/vite-plugins/ensure-store.js.map +1 -1
  95. package/dist/vite-plugins/i18n.d.ts +1 -1
  96. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  97. package/dist/vite-plugins/i18n.js +24 -11
  98. package/dist/vite-plugins/i18n.js.map +1 -1
  99. package/dist/vite-plugins/index.d.ts.map +1 -1
  100. package/dist/vite-plugins/index.js +3 -1
  101. package/dist/vite-plugins/index.js.map +1 -1
  102. package/dist/vite-plugins/native.d.ts.map +1 -1
  103. package/dist/vite-plugins/native.js +0 -4
  104. package/dist/vite-plugins/native.js.map +1 -1
  105. package/dist/vite-plugins/settings-worker-env.d.ts +1 -1
  106. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  107. package/dist/vite-plugins/settings-worker-env.js +2 -2
  108. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  109. package/dist/vitest-plugin.d.ts +2 -1
  110. package/dist/vitest-plugin.d.ts.map +1 -1
  111. package/dist/vitest-plugin.js +6 -6
  112. package/dist/vitest-plugin.js.map +1 -1
  113. package/package.json +12 -9
  114. package/src/cli.ts +12 -2
  115. package/src/codegen/config-dependencies.ts +470 -0
  116. package/src/codegen/reference.ts +1 -0
  117. package/src/codegen/run-codegen.ts +39 -17
  118. package/src/codegen/store-resolver.ts +8 -3
  119. package/src/commands/cleanup.ts +36 -0
  120. package/src/commands/dev.ts +26 -4
  121. package/src/commands/docs.ts +1 -0
  122. package/src/deploy/freedesktop/validate.ts +13 -4
  123. package/src/deploy/nfpm/config.ts +9 -3
  124. package/src/deploy/node-runtime/index.ts +2 -1
  125. package/src/deploy/payload/stage.ts +42 -3
  126. package/src/deploy/settings/extra-files.ts +5 -3
  127. package/src/deploy/targets/nfpm-target.ts +13 -6
  128. package/src/dev/entry-env.ts +2 -1
  129. package/src/dev/runner-deps.ts +3 -3
  130. package/src/dev/runner-main.ts +10 -3
  131. package/src/dev/supervisor.ts +164 -30
  132. package/src/i18n/catalogs.ts +1 -0
  133. package/src/i18n/source-messages.ts +240 -41
  134. package/src/internal/build-output.ts +79 -7
  135. package/src/internal/docs-elements.ts +10 -4
  136. package/src/internal/errors.ts +1 -1
  137. package/src/internal/parent-process.ts +110 -0
  138. package/src/internal/prepare-project.ts +9 -3
  139. package/src/vite-plugins/ensure-store.ts +2 -2
  140. package/src/vite-plugins/i18n.ts +30 -14
  141. package/src/vite-plugins/index.ts +8 -1
  142. package/src/vite-plugins/native.ts +0 -5
  143. package/src/vite-plugins/settings-worker-env.ts +2 -2
  144. package/src/vitest-plugin.ts +6 -6
@@ -71,8 +71,9 @@ type TranslationReferences = {
71
71
  calls: Set<number>;
72
72
  elements: Set<number>;
73
73
  entryLocations: Map<string, SourcePoint[]>;
74
- locations: Set<string>;
74
+ locations: Map<string, SourcePoint>;
75
75
  masks: SourceMask[];
76
+ sourcePoints: Map<number, SourcePoint>;
76
77
  };
77
78
  type SourceMask = { end: number; start: number };
78
79
  type SourcePoint = { column: number; line: number };
@@ -86,6 +87,7 @@ type ImportedBinding = {
86
87
  type TranslationHookBinding = { call: types.CallExpression; isCanonical: boolean };
87
88
  type TranslationMemberExpression = types.MemberExpression | types.OptionalMemberExpression;
88
89
  type ResolvedBinding = NonNullable<ReturnType<Scope["getBinding"]>>;
90
+ type LocatableNode = { loc?: { start: SourcePoint } | null | undefined };
89
91
 
90
92
  const CONTEXT_SEPARATOR = "\u{4}";
91
93
  const DEFAULT_NAMESPACE = "translation";
@@ -112,6 +114,114 @@ const STATIC_KEY_WRAPPER_TYPES: ReadonlySet<string> = new Set([
112
114
  "TSNonNullExpression",
113
115
  ]);
114
116
 
117
+ class SourceExtractionError extends Error {}
118
+
119
+ const sourceErrorMessage = (error: unknown): string => {
120
+ if (!(error instanceof Error)) {
121
+ return String(error);
122
+ }
123
+
124
+ if (
125
+ "code" in error &&
126
+ error.code === "BABEL_PARSE_ERROR"
127
+ ) {
128
+ const separator = error.message.indexOf(": ");
129
+
130
+ if (separator !== -1) {
131
+ return error.message.slice(separator + 2);
132
+ }
133
+ }
134
+
135
+ return error.message;
136
+ };
137
+
138
+ const projectSourcePath = (root: string, path: string): string => {
139
+ const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
140
+
141
+ return toPosixPath(isPathInside(root, absolute) ? relative(root, absolute) : path);
142
+ };
143
+
144
+ const sourceExtractionError = (
145
+ root: string,
146
+ path: string,
147
+ point: SourcePoint | null,
148
+ error: unknown,
149
+ ): SourceExtractionError => {
150
+ if (error instanceof SourceExtractionError) {
151
+ return error;
152
+ }
153
+
154
+ const file = projectSourcePath(root, path);
155
+ const location = point === null
156
+ ? file
157
+ : `${file}:${String(point.line)}:${String(point.column + 1)}`;
158
+
159
+ return new SourceExtractionError(`${location}: ${sourceErrorMessage(error)}`, { cause: error });
160
+ };
161
+
162
+ const locatablePoint = (node: LocatableNode): SourcePoint | null => {
163
+ const location = node.loc?.start;
164
+
165
+ return location === undefined ? null : { column: location.column, line: location.line };
166
+ };
167
+
168
+ const isSourcePoint = (value: unknown): value is SourcePoint =>
169
+ typeof value === "object" &&
170
+ value !== null &&
171
+ "column" in value &&
172
+ typeof value.column === "number" &&
173
+ "line" in value &&
174
+ typeof value.line === "number";
175
+
176
+ const errorSourcePoint = (error: unknown): SourcePoint | null => {
177
+ if (typeof error !== "object" || error === null) {
178
+ return null;
179
+ }
180
+
181
+ if ("loc" in error && isSourcePoint(error.loc)) {
182
+ return error.loc;
183
+ }
184
+
185
+ return "location" in error && isSourcePoint(error.location) ? error.location : null;
186
+ };
187
+
188
+ const atSourceNode = <T>(
189
+ root: string,
190
+ path: string,
191
+ node: LocatableNode,
192
+ operation: () => T,
193
+ ): T => {
194
+ try {
195
+ return operation();
196
+ } catch (error) {
197
+ throw sourceExtractionError(root, path, locatablePoint(node), error);
198
+ }
199
+ };
200
+
201
+ const atExtractedLocation = <T>(
202
+ root: string,
203
+ locations: SourceLocation[],
204
+ operation: () => T,
205
+ ): T => {
206
+ const location = locations.find((candidate) =>
207
+ candidate.line !== undefined && candidate.column !== undefined);
208
+
209
+ if (location?.line === undefined || location.column === undefined) {
210
+ return operation();
211
+ }
212
+
213
+ try {
214
+ return operation();
215
+ } catch (error) {
216
+ throw sourceExtractionError(
217
+ root,
218
+ location.file,
219
+ { column: location.column, line: location.line },
220
+ error,
221
+ );
222
+ }
223
+ };
224
+
115
225
  const quietLogger = (): { logger: Logger; reports: string[] } => {
116
226
  const reports: string[] = [];
117
227
 
@@ -223,11 +333,15 @@ const expandedPlural = (entry: ExtractedKey): { baseKey: string; variant: Plural
223
333
  };
224
334
  };
225
335
 
226
- const groupedPlurals = (entries: ExtractedKey[]): PluralGroup[] => {
336
+ const groupedPlurals = (root: string, entries: ExtractedKey[]): PluralGroup[] => {
227
337
  const groups: Map<string, PluralGroup> = new Map();
228
338
 
229
339
  for (const entry of entries) {
230
- const { baseKey, variant } = expandedPlural(entry);
340
+ const { baseKey, variant } = atExtractedLocation(
341
+ root,
342
+ entry.locations ?? [],
343
+ () => expandedPlural(entry),
344
+ );
231
345
  const namespace = namespaceFor(entry);
232
346
  const key = `${namespace}\0${baseKey}`;
233
347
  const group = groups.get(key) ?? { baseKey, namespace, variants: [] };
@@ -276,8 +390,13 @@ const pluralMessage = ({ baseKey, namespace, variants }: PluralGroup): SourceMes
276
390
  };
277
391
  };
278
392
 
279
- const pluralMessages = (entries: ExtractedKey[]): SourceMessage[] =>
280
- groupedPlurals(entries).map((group) => pluralMessage(group));
393
+ const pluralMessages = (root: string, entries: ExtractedKey[]): SourceMessage[] =>
394
+ groupedPlurals(root, entries).map((group) =>
395
+ atExtractedLocation(
396
+ root,
397
+ group.variants.flatMap((variant) => variant.locations),
398
+ () => pluralMessage(group),
399
+ ));
281
400
 
282
401
  const assertCardinal = (entry: ExtractedKey): void => {
283
402
  if (entry.isOrdinal === true) {
@@ -285,12 +404,14 @@ const assertCardinal = (entry: ExtractedKey): void => {
285
404
  }
286
405
  };
287
406
 
288
- const sourceMessages = (entries: ExtractedKey[]): SourceMessage[] => {
407
+ const sourceMessages = (root: string, entries: ExtractedKey[]): SourceMessage[] => {
289
408
  const points: ExtractedKey[] = [];
290
409
  const plurals: ExtractedKey[] = [];
291
410
 
292
411
  for (const entry of entries) {
293
- assertCardinal(entry);
412
+ atExtractedLocation(root, entry.locations ?? [], () => {
413
+ assertCardinal(entry);
414
+ });
294
415
 
295
416
  if (entry.isExpandedPlural === true) {
296
417
  plurals.push(entry);
@@ -304,7 +425,11 @@ const sourceMessages = (entries: ExtractedKey[]): SourceMessage[] => {
304
425
  points.push(entry);
305
426
  }
306
427
 
307
- return [...points.map((entry) => pointMessage(entry)), ...pluralMessages(plurals)];
428
+ return [
429
+ ...points.map((entry) =>
430
+ atExtractedLocation(root, entry.locations ?? [], () => pointMessage(entry))),
431
+ ...pluralMessages(root, plurals),
432
+ ];
308
433
  };
309
434
 
310
435
  const compareMessages = (left: SourceMessage, right: SourceMessage): number =>
@@ -745,16 +870,14 @@ const registerSourceMask = (node: types.Node, references: TranslationReferences)
745
870
  const sourceLocationKey = (line: number, column: number): string => `${String(line)}:${String(column)}`;
746
871
 
747
872
  const sourcePoint = (node: types.Node): SourcePoint | null => {
748
- const location = node.loc?.start;
749
-
750
- return location === undefined ? null : { column: location.column, line: location.line };
873
+ return locatablePoint(node);
751
874
  };
752
875
 
753
876
  const registerSourceLocation = (node: types.Node, references: TranslationReferences): SourcePoint | null => {
754
877
  const point = sourcePoint(node);
755
878
 
756
879
  if (point !== null) {
757
- references.locations.add(sourceLocationKey(point.line, point.column));
880
+ references.locations.set(sourceLocationKey(point.line, point.column), point);
758
881
  }
759
882
 
760
883
  return point;
@@ -901,6 +1024,11 @@ const registerCallReference = (
901
1024
  references.calls.add(nodeStart(callPath));
902
1025
  const firstArgument = callPath.node.arguments[0];
903
1026
  const point = registerSourceLocation(firstArgument ?? callPath.node, references);
1027
+
1028
+ if (point !== null) {
1029
+ references.sourcePoints.set(nodeStart(callPath), point);
1030
+ }
1031
+
904
1032
  registerEntryLocation(extractedEntryKey(callPath.node, keyPrefix), point, references);
905
1033
  };
906
1034
 
@@ -910,6 +1038,26 @@ const parserPlugins = (lang: ReturnType<typeof sourceLanguage>): ("decorators" |
910
1038
  ...(lang === "jsx" || lang === "tsx" ? ["jsx" as const] : []),
911
1039
  ];
912
1040
 
1041
+ const parseTranslationReferences = (
1042
+ root: string,
1043
+ path: string,
1044
+ source: string,
1045
+ lang: ReturnType<typeof sourceLanguage>,
1046
+ ) => {
1047
+ try {
1048
+ return parseBabelSync(source, {
1049
+ ast: true,
1050
+ babelrc: false,
1051
+ configFile: false,
1052
+ filename: projectSourcePath(root, path),
1053
+ parserOpts: { createParenthesizedExpressions: true, plugins: parserPlugins(lang) },
1054
+ sourceType: "module",
1055
+ });
1056
+ } catch (error) {
1057
+ throw sourceExtractionError(root, path, errorSourcePoint(error), error);
1058
+ }
1059
+ };
1060
+
913
1061
  const registerIdentifierTranslationCall = (
914
1062
  callPath: NodePath<types.CallExpression>,
915
1063
  callee: NodePath,
@@ -1010,6 +1158,19 @@ const registerUnrecognizedElementMasks = (
1010
1158
  }
1011
1159
  };
1012
1160
 
1161
+ const registerElementReference = (
1162
+ elementPath: NodePath,
1163
+ references: TranslationReferences,
1164
+ ): void => {
1165
+ const start = nodeStart(elementPath);
1166
+ const point = registerSourceLocation(elementPath.node, references);
1167
+ references.elements.add(start);
1168
+
1169
+ if (point !== null) {
1170
+ references.sourcePoints.set(start, point);
1171
+ }
1172
+ };
1173
+
1013
1174
  const registerTranslationElement = (elementPath: NodePath, references: TranslationReferences): void => {
1014
1175
  const name = elementPath.get("name");
1015
1176
 
@@ -1024,8 +1185,7 @@ const registerTranslationElement = (elementPath: NodePath, references: Translati
1024
1185
  }
1025
1186
 
1026
1187
  if (kind === "canonical") {
1027
- references.elements.add(nodeStart(elementPath));
1028
- registerSourceLocation(elementPath.node, references);
1188
+ registerElementReference(elementPath, references);
1029
1189
  } else if (TRANS_ELEMENTS.has(name.node.name)) {
1030
1190
  registerUnrecognizedElementMasks(elementPath, name, references);
1031
1191
  }
@@ -1043,18 +1203,12 @@ const maskUnrecognizedTranslations = (source: string, references: TranslationRef
1043
1203
  };
1044
1204
 
1045
1205
  const translationReferences = (
1206
+ root: string,
1046
1207
  path: string,
1047
1208
  source: string,
1048
1209
  lang: ReturnType<typeof sourceLanguage>,
1049
1210
  ): TranslationReferences => {
1050
- const parsed = parseBabelSync(source, {
1051
- ast: true,
1052
- babelrc: false,
1053
- configFile: false,
1054
- filename: path,
1055
- parserOpts: { createParenthesizedExpressions: true, plugins: parserPlugins(lang) },
1056
- sourceType: "module",
1057
- });
1211
+ const parsed = parseTranslationReferences(root, path, source, lang);
1058
1212
 
1059
1213
  if (parsed === null) {
1060
1214
  throw new Error(`Translation extraction could not parse ${path}`);
@@ -1064,22 +1218,31 @@ const translationReferences = (
1064
1218
  calls: new Set(),
1065
1219
  elements: new Set(),
1066
1220
  entryLocations: new Map(),
1067
- locations: new Set(),
1221
+ locations: new Map(),
1068
1222
  masks: [],
1223
+ sourcePoints: new Map(),
1069
1224
  };
1070
1225
 
1071
1226
  traverse(parsed, {
1072
1227
  CallExpression(callPath) {
1073
- registerTranslationCall(callPath, references);
1228
+ atSourceNode(root, path, callPath.node, () => {
1229
+ registerTranslationCall(callPath, references);
1230
+ });
1074
1231
  },
1075
1232
  JSXOpeningElement(elementPath) {
1076
- registerTranslationElement(elementPath, references);
1233
+ atSourceNode(root, path, elementPath.node, () => {
1234
+ registerTranslationElement(elementPath, references);
1235
+ });
1077
1236
  },
1078
1237
  OptionalCallExpression(callPath) {
1079
- assertSupportedOptionalCall(callPath);
1238
+ atSourceNode(root, path, callPath.node, () => {
1239
+ assertSupportedOptionalCall(callPath);
1240
+ });
1080
1241
  },
1081
1242
  TaggedTemplateExpression(tagPath) {
1082
- assertUnsupportedTranslationTag(tagPath);
1243
+ atSourceNode(root, path, tagPath.node, () => {
1244
+ assertUnsupportedTranslationTag(tagPath);
1245
+ });
1083
1246
  },
1084
1247
  });
1085
1248
 
@@ -1165,22 +1328,44 @@ const assertSupportedTransElement = (node: ESTree.JSXOpeningElement, references:
1165
1328
  }
1166
1329
  };
1167
1330
 
1168
- const assertSupportedTranslationSyntax = (path: string, source: string): TranslationReferences => {
1331
+ const assertSupportedTranslationSyntax = (root: string, path: string, source: string): TranslationReferences => {
1169
1332
  const lang = sourceLanguage(path);
1170
1333
 
1171
1334
  if (lang === undefined) {
1172
- return { calls: new Set(), elements: new Set(), entryLocations: new Map(), locations: new Set(), masks: [] };
1335
+ return {
1336
+ calls: new Set(),
1337
+ elements: new Set(),
1338
+ entryLocations: new Map(),
1339
+ locations: new Map(),
1340
+ masks: [],
1341
+ sourcePoints: new Map(),
1342
+ };
1343
+ }
1344
+
1345
+ let parsed: ReturnType<typeof parseSync>;
1346
+
1347
+ try {
1348
+ parsed = parseSync(projectSourcePath(root, path), source, { lang });
1349
+ } catch (error) {
1350
+ throw sourceExtractionError(root, path, errorSourcePoint(error), error);
1173
1351
  }
1174
1352
 
1175
- const parsed = parseSync(path, source, { lang });
1176
- const references = translationReferences(path, source, lang);
1353
+ const references = translationReferences(root, path, source, lang);
1177
1354
 
1178
1355
  new Visitor({
1179
1356
  CallExpression(node) {
1180
- assertSupportedTranslationCall(node, references);
1357
+ try {
1358
+ assertSupportedTranslationCall(node, references);
1359
+ } catch (error) {
1360
+ throw sourceExtractionError(root, path, references.sourcePoints.get(node.start) ?? null, error);
1361
+ }
1181
1362
  },
1182
1363
  JSXOpeningElement(node) {
1183
- assertSupportedTransElement(node, references);
1364
+ try {
1365
+ assertSupportedTransElement(node, references);
1366
+ } catch (error) {
1367
+ throw sourceExtractionError(root, path, references.sourcePoints.get(node.start) ?? null, error);
1368
+ }
1184
1369
  },
1185
1370
  }).visit(parsed.program);
1186
1371
 
@@ -1476,12 +1661,19 @@ const extractedSourcePoints = (root: string, entries: ExtractedKeysMap): Set<str
1476
1661
  return points;
1477
1662
  };
1478
1663
 
1479
- const hasMissingSourcePoint = (
1664
+ const missingSourcePoint = (
1480
1665
  file: string,
1481
1666
  references: TranslationReferences,
1482
1667
  extracted: Set<string>,
1483
- ): boolean =>
1484
- references.locations.values().some((location) => !extracted.has(`${file}\0${location}`));
1668
+ ): SourcePoint | null => {
1669
+ for (const [location, point] of references.locations) {
1670
+ if (!extracted.has(`${file}\0${location}`)) {
1671
+ return point;
1672
+ }
1673
+ }
1674
+
1675
+ return null;
1676
+ };
1485
1677
 
1486
1678
  const assertAllReferencesExtracted = (
1487
1679
  root: string,
@@ -1491,8 +1683,15 @@ const assertAllReferencesExtracted = (
1491
1683
  const extracted = extractedSourcePoints(root, entries);
1492
1684
 
1493
1685
  for (const [file, sourceReferences] of references) {
1494
- if (hasMissingSourcePoint(file, sourceReferences, extracted)) {
1495
- throw new Error("A translation expression could not be extracted safely");
1686
+ const point = missingSourcePoint(file, sourceReferences, extracted);
1687
+
1688
+ if (point !== null) {
1689
+ throw sourceExtractionError(
1690
+ root,
1691
+ file,
1692
+ point,
1693
+ new Error("A translation expression could not be extracted safely"),
1694
+ );
1496
1695
  }
1497
1696
  }
1498
1697
  };
@@ -1508,7 +1707,7 @@ const findSourceMessages = async (root: string, sourceFiles: string[]): Promise<
1508
1707
  const references: Map<string, TranslationReferences> = new Map();
1509
1708
 
1510
1709
  for (const [file, source] of sources) {
1511
- references.set(file, assertSupportedTranslationSyntax(file, source));
1710
+ references.set(file, assertSupportedTranslationSyntax(root, file, source));
1512
1711
  }
1513
1712
 
1514
1713
  let extracted: ExtractedKeysMap | undefined;
@@ -1542,7 +1741,7 @@ const findSourceMessages = async (root: string, sourceFiles: string[]): Promise<
1542
1741
  throw new Error(reports.join("\n") || "i18next extraction failed");
1543
1742
  }
1544
1743
 
1545
- return sourceMessages(extracted.values().toArray())
1744
+ return sourceMessages(root, extracted.values().toArray())
1546
1745
  .map((message) => inferLocations(message, sources))
1547
1746
  .toSorted(compareMessages);
1548
1747
  };
@@ -1785,4 +1984,4 @@ const extractSourceCatalog = async (
1785
1984
  extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages });
1786
1985
  };
1787
1986
 
1788
- export { extractSourceCatalog, extractSourceCatalogTo };
1987
+ export { extractSourceCatalog, extractSourceCatalogTo, SourceExtractionError };
@@ -1,6 +1,6 @@
1
1
  import { isPathInside, isRecord } from "@gtkx/utils";
2
2
  import { lstatSync, readdirSync } from "node:fs";
3
- import { join, relative, resolve, sep } from "node:path";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import {
5
5
  BUILD_MANIFEST_FILENAME,
6
6
  BUILD_MANIFEST_GENERATOR,
@@ -53,13 +53,85 @@ const isReusableBuildDirectory = (path: string): boolean => {
53
53
  return stats.isDirectory() && (readdirSync(path).length === 0 || isGtkxBuildDirectory(path));
54
54
  };
55
55
 
56
- const buildOutputError = (root: string): Error =>
57
- new Error(`Build output must be an empty directory or an earlier GTKX build below the project root ${root}`);
56
+ const outputName = (root: string, path: string): string => relative(root, path) || ".";
57
+
58
+ const gtkxBuildAncestor = (root: string, outDir: string): string | null => {
59
+ let ancestor = dirname(outDir);
60
+
61
+ while (isPathInside(root, ancestor)) {
62
+ if (isGtkxBuildDirectory(ancestor)) {
63
+ return ancestor;
64
+ }
65
+
66
+ ancestor = dirname(ancestor);
67
+ }
68
+
69
+ return null;
70
+ };
71
+
72
+ const childDirectories = (parent: string): string[] =>
73
+ readdirSync(parent, { withFileTypes: true })
74
+ .filter((entry) => entry.isDirectory())
75
+ .map((entry) => join(parent, entry.name));
76
+
77
+ const gtkxBuildDescendant = (root: string): string | null => {
78
+ const pending = childDirectories(root);
79
+
80
+ while (pending.length > 0) {
81
+ const path = pending.pop();
82
+
83
+ if (path === undefined) {
84
+ return null;
85
+ }
86
+
87
+ if (isGtkxBuildDirectory(path)) {
88
+ return path;
89
+ }
90
+
91
+ pending.push(...childDirectories(path));
92
+ }
93
+
94
+ return null;
95
+ };
58
96
 
59
97
  const assertSafeBuildLocation = (root: string, outDir: string): void => {
60
- if (!isPathInside(root, outDir) || hasSymlinkComponent(root, outDir)) {
61
- throw buildOutputError(root);
98
+ if (!isPathInside(root, outDir)) {
99
+ throw new Error(`Build output ${outDir} must be below the project root ${root}`);
100
+ }
101
+
102
+ if (hasSymlinkComponent(root, outDir)) {
103
+ throw new Error(`Build output ${outputName(root, outDir)} passes through a symbolic link`);
62
104
  }
105
+
106
+ const ancestor = gtkxBuildAncestor(root, outDir);
107
+
108
+ if (ancestor !== null) {
109
+ throw new Error(
110
+ `Build output ${outputName(root, outDir)} is nested inside the earlier GTKX build ` +
111
+ outputName(root, ancestor),
112
+ );
113
+ }
114
+ };
115
+
116
+ const nonReusableBuildOutputError = (root: string, outDir: string): Error => {
117
+ const stats = lstatSync(outDir, { throwIfNoEntry: false });
118
+
119
+ if (stats?.isDirectory() !== true) {
120
+ return new Error(`Build output ${outputName(root, outDir)} exists and is not a directory`);
121
+ }
122
+
123
+ const descendant = gtkxBuildDescendant(outDir);
124
+
125
+ if (descendant !== null) {
126
+ return new Error(
127
+ `Build output ${outputName(root, outDir)} contains the earlier GTKX build ` +
128
+ outputName(root, descendant),
129
+ );
130
+ }
131
+
132
+ return new Error(
133
+ `Build output ${outputName(root, outDir)} is nonempty and is not an earlier GTKX build`,
134
+ );
63
135
  };
64
136
 
65
137
  const resolveBuildOutDir = (root: string, configured?: string): string => {
@@ -69,7 +141,7 @@ const resolveBuildOutDir = (root: string, configured?: string): string => {
69
141
  assertSafeBuildLocation(root, outDir);
70
142
 
71
143
  if (!isReusableBuildDirectory(outDir)) {
72
- throw buildOutputError(root);
144
+ throw nonReusableBuildOutputError(root, outDir);
73
145
  }
74
146
 
75
147
  return outDir;
@@ -80,7 +152,7 @@ const prepareBuildOutDir = (root: string, outDir: string): PreparedBuildOutDir =
80
152
  const prepared = prepareOutputDirectory(root, outDir, isReusableBuildDirectory);
81
153
 
82
154
  if (prepared.status === "unsafe") {
83
- throw buildOutputError(root);
155
+ throw new Error(`Build output ${outputName(root, outDir)} became unsafe while preparing it`);
84
156
  }
85
157
 
86
158
  const transaction: OutputDirectoryTransaction = prepared.transaction;
@@ -1,21 +1,27 @@
1
- import { type ElementProps, type OmittedProps, readBuiltinElements, resolveStore } from "@gtkx/codegen";
1
+ import {
2
+ type ElementProps,
3
+ type OmittedProps,
4
+ resolveStore,
5
+ } from "@gtkx/codegen";
6
+ import { readBuiltinElementsForDocs } from "@gtkx/codegen/internal";
2
7
  import { existsSync } from "node:fs";
3
8
 
4
9
  type DocsElements = {
5
10
  props: ElementProps;
6
11
  omittedProps: OmittedProps;
12
+ acceptedChildTypes: Record<string, string[]>;
7
13
  };
8
14
 
9
15
  const resolveDocsElements = async (cwd: string): Promise<DocsElements> => {
10
16
  const { gi } = resolveStore(cwd);
11
17
 
12
18
  if (!existsSync(gi.storeDir)) {
13
- return { props: {}, omittedProps: {} };
19
+ return { props: {}, omittedProps: {}, acceptedChildTypes: {} };
14
20
  }
15
21
 
16
- const { props, omittedProps } = await readBuiltinElements();
22
+ const { props, omittedProps, acceptedChildTypes } = await readBuiltinElementsForDocs();
17
23
 
18
- return { props, omittedProps };
24
+ return { props, omittedProps, acceptedChildTypes };
19
25
  };
20
26
 
21
27
  export { resolveDocsElements };
@@ -33,4 +33,4 @@ const withErrorBoundary = <T extends ArgsDef>(command: CommandDef<T>): CommandDe
33
33
  };
34
34
  };
35
35
 
36
- export { withErrorBoundary };
36
+ export { printError, withErrorBoundary };