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

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 (114) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +8 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/codegen/reference.d.ts.map +1 -1
  6. package/dist/codegen/reference.js +1 -0
  7. package/dist/codegen/reference.js.map +1 -1
  8. package/dist/codegen/run-codegen.d.ts.map +1 -1
  9. package/dist/codegen/run-codegen.js +2 -8
  10. package/dist/codegen/run-codegen.js.map +1 -1
  11. package/dist/codegen/store-resolver.d.ts +1 -0
  12. package/dist/codegen/store-resolver.d.ts.map +1 -1
  13. package/dist/codegen/store-resolver.js +2 -1
  14. package/dist/codegen/store-resolver.js.map +1 -1
  15. package/dist/commands/dev.d.ts +1 -0
  16. package/dist/commands/dev.d.ts.map +1 -1
  17. package/dist/commands/dev.js +11 -4
  18. package/dist/commands/dev.js.map +1 -1
  19. package/dist/commands/docs.d.ts.map +1 -1
  20. package/dist/commands/docs.js +1 -0
  21. package/dist/commands/docs.js.map +1 -1
  22. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  23. package/dist/deploy/freedesktop/validate.js +10 -4
  24. package/dist/deploy/freedesktop/validate.js.map +1 -1
  25. package/dist/deploy/nfpm/config.d.ts +3 -2
  26. package/dist/deploy/nfpm/config.d.ts.map +1 -1
  27. package/dist/deploy/nfpm/config.js +3 -3
  28. package/dist/deploy/nfpm/config.js.map +1 -1
  29. package/dist/deploy/node-runtime/index.d.ts.map +1 -1
  30. package/dist/deploy/node-runtime/index.js +2 -1
  31. package/dist/deploy/node-runtime/index.js.map +1 -1
  32. package/dist/deploy/targets/nfpm-target.d.ts.map +1 -1
  33. package/dist/deploy/targets/nfpm-target.js +12 -7
  34. package/dist/deploy/targets/nfpm-target.js.map +1 -1
  35. package/dist/dev/entry-env.d.ts +2 -1
  36. package/dist/dev/entry-env.d.ts.map +1 -1
  37. package/dist/dev/entry-env.js +2 -1
  38. package/dist/dev/entry-env.js.map +1 -1
  39. package/dist/dev/runner-deps.d.ts +1 -1
  40. package/dist/dev/runner-deps.d.ts.map +1 -1
  41. package/dist/dev/runner-deps.js +3 -3
  42. package/dist/dev/runner-deps.js.map +1 -1
  43. package/dist/dev/runner-main.d.ts.map +1 -1
  44. package/dist/dev/runner-main.js +8 -3
  45. package/dist/dev/runner-main.js.map +1 -1
  46. package/dist/dev/supervisor.d.ts +1 -0
  47. package/dist/dev/supervisor.d.ts.map +1 -1
  48. package/dist/dev/supervisor.js +4 -3
  49. package/dist/dev/supervisor.js.map +1 -1
  50. package/dist/i18n/catalogs.d.ts.map +1 -1
  51. package/dist/i18n/catalogs.js +1 -0
  52. package/dist/i18n/catalogs.js.map +1 -1
  53. package/dist/i18n/source-messages.d.ts.map +1 -1
  54. package/dist/i18n/source-messages.js +154 -36
  55. package/dist/i18n/source-messages.js.map +1 -1
  56. package/dist/internal/build-output.d.ts.map +1 -1
  57. package/dist/internal/build-output.js +12 -2
  58. package/dist/internal/build-output.js.map +1 -1
  59. package/dist/internal/docs-elements.d.ts +1 -0
  60. package/dist/internal/docs-elements.d.ts.map +1 -1
  61. package/dist/internal/docs-elements.js +5 -4
  62. package/dist/internal/docs-elements.js.map +1 -1
  63. package/dist/internal/errors.d.ts +2 -1
  64. package/dist/internal/errors.d.ts.map +1 -1
  65. package/dist/internal/errors.js +1 -1
  66. package/dist/internal/errors.js.map +1 -1
  67. package/dist/internal/parent-process.d.ts +12 -0
  68. package/dist/internal/parent-process.d.ts.map +1 -0
  69. package/dist/internal/parent-process.js +74 -0
  70. package/dist/internal/parent-process.js.map +1 -0
  71. package/dist/vite-plugins/ensure-store.d.ts +1 -1
  72. package/dist/vite-plugins/ensure-store.d.ts.map +1 -1
  73. package/dist/vite-plugins/ensure-store.js +2 -2
  74. package/dist/vite-plugins/ensure-store.js.map +1 -1
  75. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  76. package/dist/vite-plugins/i18n.js +7 -7
  77. package/dist/vite-plugins/i18n.js.map +1 -1
  78. package/dist/vite-plugins/native.d.ts.map +1 -1
  79. package/dist/vite-plugins/native.js +0 -4
  80. package/dist/vite-plugins/native.js.map +1 -1
  81. package/dist/vite-plugins/settings-worker-env.d.ts +1 -1
  82. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  83. package/dist/vite-plugins/settings-worker-env.js +2 -2
  84. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  85. package/dist/vitest-plugin.d.ts +2 -1
  86. package/dist/vitest-plugin.d.ts.map +1 -1
  87. package/dist/vitest-plugin.js +6 -6
  88. package/dist/vitest-plugin.js.map +1 -1
  89. package/package.json +10 -9
  90. package/src/cli.ts +8 -3
  91. package/src/codegen/reference.ts +1 -0
  92. package/src/codegen/run-codegen.ts +2 -12
  93. package/src/codegen/store-resolver.ts +3 -1
  94. package/src/commands/dev.ts +19 -4
  95. package/src/commands/docs.ts +1 -0
  96. package/src/deploy/freedesktop/validate.ts +12 -4
  97. package/src/deploy/nfpm/config.ts +9 -3
  98. package/src/deploy/node-runtime/index.ts +2 -1
  99. package/src/deploy/targets/nfpm-target.ts +13 -6
  100. package/src/dev/entry-env.ts +2 -1
  101. package/src/dev/runner-deps.ts +3 -3
  102. package/src/dev/runner-main.ts +10 -3
  103. package/src/dev/supervisor.ts +6 -3
  104. package/src/i18n/catalogs.ts +1 -0
  105. package/src/i18n/source-messages.ts +223 -40
  106. package/src/internal/build-output.ts +16 -2
  107. package/src/internal/docs-elements.ts +10 -4
  108. package/src/internal/errors.ts +1 -1
  109. package/src/internal/parent-process.ts +110 -0
  110. package/src/vite-plugins/ensure-store.ts +2 -2
  111. package/src/vite-plugins/i18n.ts +8 -11
  112. package/src/vite-plugins/native.ts +0 -5
  113. package/src/vite-plugins/settings-worker-env.ts +2 -2
  114. 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,98 @@ 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
+ error instanceof Error ? error.message : String(error);
121
+
122
+ const projectSourcePath = (root: string, path: string): string => {
123
+ const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
124
+
125
+ return toPosixPath(isPathInside(root, absolute) ? relative(root, absolute) : path);
126
+ };
127
+
128
+ const sourceExtractionError = (
129
+ root: string,
130
+ path: string,
131
+ point: SourcePoint | null,
132
+ error: unknown,
133
+ ): SourceExtractionError => {
134
+ if (error instanceof SourceExtractionError) {
135
+ return error;
136
+ }
137
+
138
+ const file = projectSourcePath(root, path);
139
+ const location = point === null
140
+ ? file
141
+ : `${file}:${String(point.line)}:${String(point.column + 1)}`;
142
+
143
+ return new SourceExtractionError(`${location}: ${sourceErrorMessage(error)}`, { cause: error });
144
+ };
145
+
146
+ const locatablePoint = (node: LocatableNode): SourcePoint | null => {
147
+ const location = node.loc?.start;
148
+
149
+ return location === undefined ? null : { column: location.column, line: location.line };
150
+ };
151
+
152
+ const isSourcePoint = (value: unknown): value is SourcePoint =>
153
+ typeof value === "object" &&
154
+ value !== null &&
155
+ "column" in value &&
156
+ typeof value.column === "number" &&
157
+ "line" in value &&
158
+ typeof value.line === "number";
159
+
160
+ const errorSourcePoint = (error: unknown): SourcePoint | null => {
161
+ if (typeof error !== "object" || error === null) {
162
+ return null;
163
+ }
164
+
165
+ if ("loc" in error && isSourcePoint(error.loc)) {
166
+ return error.loc;
167
+ }
168
+
169
+ return "location" in error && isSourcePoint(error.location) ? error.location : null;
170
+ };
171
+
172
+ const atSourceNode = <T>(
173
+ root: string,
174
+ path: string,
175
+ node: LocatableNode,
176
+ operation: () => T,
177
+ ): T => {
178
+ try {
179
+ return operation();
180
+ } catch (error) {
181
+ throw sourceExtractionError(root, path, locatablePoint(node), error);
182
+ }
183
+ };
184
+
185
+ const atExtractedLocation = <T>(
186
+ root: string,
187
+ locations: SourceLocation[],
188
+ operation: () => T,
189
+ ): T => {
190
+ const location = locations.find((candidate) =>
191
+ candidate.line !== undefined && candidate.column !== undefined);
192
+
193
+ if (location?.line === undefined || location.column === undefined) {
194
+ return operation();
195
+ }
196
+
197
+ try {
198
+ return operation();
199
+ } catch (error) {
200
+ throw sourceExtractionError(
201
+ root,
202
+ location.file,
203
+ { column: location.column, line: location.line },
204
+ error,
205
+ );
206
+ }
207
+ };
208
+
115
209
  const quietLogger = (): { logger: Logger; reports: string[] } => {
116
210
  const reports: string[] = [];
117
211
 
@@ -223,11 +317,15 @@ const expandedPlural = (entry: ExtractedKey): { baseKey: string; variant: Plural
223
317
  };
224
318
  };
225
319
 
226
- const groupedPlurals = (entries: ExtractedKey[]): PluralGroup[] => {
320
+ const groupedPlurals = (root: string, entries: ExtractedKey[]): PluralGroup[] => {
227
321
  const groups: Map<string, PluralGroup> = new Map();
228
322
 
229
323
  for (const entry of entries) {
230
- const { baseKey, variant } = expandedPlural(entry);
324
+ const { baseKey, variant } = atExtractedLocation(
325
+ root,
326
+ entry.locations ?? [],
327
+ () => expandedPlural(entry),
328
+ );
231
329
  const namespace = namespaceFor(entry);
232
330
  const key = `${namespace}\0${baseKey}`;
233
331
  const group = groups.get(key) ?? { baseKey, namespace, variants: [] };
@@ -276,8 +374,13 @@ const pluralMessage = ({ baseKey, namespace, variants }: PluralGroup): SourceMes
276
374
  };
277
375
  };
278
376
 
279
- const pluralMessages = (entries: ExtractedKey[]): SourceMessage[] =>
280
- groupedPlurals(entries).map((group) => pluralMessage(group));
377
+ const pluralMessages = (root: string, entries: ExtractedKey[]): SourceMessage[] =>
378
+ groupedPlurals(root, entries).map((group) =>
379
+ atExtractedLocation(
380
+ root,
381
+ group.variants.flatMap((variant) => variant.locations),
382
+ () => pluralMessage(group),
383
+ ));
281
384
 
282
385
  const assertCardinal = (entry: ExtractedKey): void => {
283
386
  if (entry.isOrdinal === true) {
@@ -285,12 +388,14 @@ const assertCardinal = (entry: ExtractedKey): void => {
285
388
  }
286
389
  };
287
390
 
288
- const sourceMessages = (entries: ExtractedKey[]): SourceMessage[] => {
391
+ const sourceMessages = (root: string, entries: ExtractedKey[]): SourceMessage[] => {
289
392
  const points: ExtractedKey[] = [];
290
393
  const plurals: ExtractedKey[] = [];
291
394
 
292
395
  for (const entry of entries) {
293
- assertCardinal(entry);
396
+ atExtractedLocation(root, entry.locations ?? [], () => {
397
+ assertCardinal(entry);
398
+ });
294
399
 
295
400
  if (entry.isExpandedPlural === true) {
296
401
  plurals.push(entry);
@@ -304,7 +409,11 @@ const sourceMessages = (entries: ExtractedKey[]): SourceMessage[] => {
304
409
  points.push(entry);
305
410
  }
306
411
 
307
- return [...points.map((entry) => pointMessage(entry)), ...pluralMessages(plurals)];
412
+ return [
413
+ ...points.map((entry) =>
414
+ atExtractedLocation(root, entry.locations ?? [], () => pointMessage(entry))),
415
+ ...pluralMessages(root, plurals),
416
+ ];
308
417
  };
309
418
 
310
419
  const compareMessages = (left: SourceMessage, right: SourceMessage): number =>
@@ -745,16 +854,14 @@ const registerSourceMask = (node: types.Node, references: TranslationReferences)
745
854
  const sourceLocationKey = (line: number, column: number): string => `${String(line)}:${String(column)}`;
746
855
 
747
856
  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 };
857
+ return locatablePoint(node);
751
858
  };
752
859
 
753
860
  const registerSourceLocation = (node: types.Node, references: TranslationReferences): SourcePoint | null => {
754
861
  const point = sourcePoint(node);
755
862
 
756
863
  if (point !== null) {
757
- references.locations.add(sourceLocationKey(point.line, point.column));
864
+ references.locations.set(sourceLocationKey(point.line, point.column), point);
758
865
  }
759
866
 
760
867
  return point;
@@ -901,6 +1008,11 @@ const registerCallReference = (
901
1008
  references.calls.add(nodeStart(callPath));
902
1009
  const firstArgument = callPath.node.arguments[0];
903
1010
  const point = registerSourceLocation(firstArgument ?? callPath.node, references);
1011
+
1012
+ if (point !== null) {
1013
+ references.sourcePoints.set(nodeStart(callPath), point);
1014
+ }
1015
+
904
1016
  registerEntryLocation(extractedEntryKey(callPath.node, keyPrefix), point, references);
905
1017
  };
906
1018
 
@@ -910,6 +1022,26 @@ const parserPlugins = (lang: ReturnType<typeof sourceLanguage>): ("decorators" |
910
1022
  ...(lang === "jsx" || lang === "tsx" ? ["jsx" as const] : []),
911
1023
  ];
912
1024
 
1025
+ const parseTranslationReferences = (
1026
+ root: string,
1027
+ path: string,
1028
+ source: string,
1029
+ lang: ReturnType<typeof sourceLanguage>,
1030
+ ) => {
1031
+ try {
1032
+ return parseBabelSync(source, {
1033
+ ast: true,
1034
+ babelrc: false,
1035
+ configFile: false,
1036
+ filename: projectSourcePath(root, path),
1037
+ parserOpts: { createParenthesizedExpressions: true, plugins: parserPlugins(lang) },
1038
+ sourceType: "module",
1039
+ });
1040
+ } catch (error) {
1041
+ throw sourceExtractionError(root, path, errorSourcePoint(error), error);
1042
+ }
1043
+ };
1044
+
913
1045
  const registerIdentifierTranslationCall = (
914
1046
  callPath: NodePath<types.CallExpression>,
915
1047
  callee: NodePath,
@@ -1010,6 +1142,19 @@ const registerUnrecognizedElementMasks = (
1010
1142
  }
1011
1143
  };
1012
1144
 
1145
+ const registerElementReference = (
1146
+ elementPath: NodePath,
1147
+ references: TranslationReferences,
1148
+ ): void => {
1149
+ const start = nodeStart(elementPath);
1150
+ const point = registerSourceLocation(elementPath.node, references);
1151
+ references.elements.add(start);
1152
+
1153
+ if (point !== null) {
1154
+ references.sourcePoints.set(start, point);
1155
+ }
1156
+ };
1157
+
1013
1158
  const registerTranslationElement = (elementPath: NodePath, references: TranslationReferences): void => {
1014
1159
  const name = elementPath.get("name");
1015
1160
 
@@ -1024,8 +1169,7 @@ const registerTranslationElement = (elementPath: NodePath, references: Translati
1024
1169
  }
1025
1170
 
1026
1171
  if (kind === "canonical") {
1027
- references.elements.add(nodeStart(elementPath));
1028
- registerSourceLocation(elementPath.node, references);
1172
+ registerElementReference(elementPath, references);
1029
1173
  } else if (TRANS_ELEMENTS.has(name.node.name)) {
1030
1174
  registerUnrecognizedElementMasks(elementPath, name, references);
1031
1175
  }
@@ -1043,18 +1187,12 @@ const maskUnrecognizedTranslations = (source: string, references: TranslationRef
1043
1187
  };
1044
1188
 
1045
1189
  const translationReferences = (
1190
+ root: string,
1046
1191
  path: string,
1047
1192
  source: string,
1048
1193
  lang: ReturnType<typeof sourceLanguage>,
1049
1194
  ): 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
- });
1195
+ const parsed = parseTranslationReferences(root, path, source, lang);
1058
1196
 
1059
1197
  if (parsed === null) {
1060
1198
  throw new Error(`Translation extraction could not parse ${path}`);
@@ -1064,22 +1202,31 @@ const translationReferences = (
1064
1202
  calls: new Set(),
1065
1203
  elements: new Set(),
1066
1204
  entryLocations: new Map(),
1067
- locations: new Set(),
1205
+ locations: new Map(),
1068
1206
  masks: [],
1207
+ sourcePoints: new Map(),
1069
1208
  };
1070
1209
 
1071
1210
  traverse(parsed, {
1072
1211
  CallExpression(callPath) {
1073
- registerTranslationCall(callPath, references);
1212
+ atSourceNode(root, path, callPath.node, () => {
1213
+ registerTranslationCall(callPath, references);
1214
+ });
1074
1215
  },
1075
1216
  JSXOpeningElement(elementPath) {
1076
- registerTranslationElement(elementPath, references);
1217
+ atSourceNode(root, path, elementPath.node, () => {
1218
+ registerTranslationElement(elementPath, references);
1219
+ });
1077
1220
  },
1078
1221
  OptionalCallExpression(callPath) {
1079
- assertSupportedOptionalCall(callPath);
1222
+ atSourceNode(root, path, callPath.node, () => {
1223
+ assertSupportedOptionalCall(callPath);
1224
+ });
1080
1225
  },
1081
1226
  TaggedTemplateExpression(tagPath) {
1082
- assertUnsupportedTranslationTag(tagPath);
1227
+ atSourceNode(root, path, tagPath.node, () => {
1228
+ assertUnsupportedTranslationTag(tagPath);
1229
+ });
1083
1230
  },
1084
1231
  });
1085
1232
 
@@ -1165,22 +1312,44 @@ const assertSupportedTransElement = (node: ESTree.JSXOpeningElement, references:
1165
1312
  }
1166
1313
  };
1167
1314
 
1168
- const assertSupportedTranslationSyntax = (path: string, source: string): TranslationReferences => {
1315
+ const assertSupportedTranslationSyntax = (root: string, path: string, source: string): TranslationReferences => {
1169
1316
  const lang = sourceLanguage(path);
1170
1317
 
1171
1318
  if (lang === undefined) {
1172
- return { calls: new Set(), elements: new Set(), entryLocations: new Map(), locations: new Set(), masks: [] };
1319
+ return {
1320
+ calls: new Set(),
1321
+ elements: new Set(),
1322
+ entryLocations: new Map(),
1323
+ locations: new Map(),
1324
+ masks: [],
1325
+ sourcePoints: new Map(),
1326
+ };
1173
1327
  }
1174
1328
 
1175
- const parsed = parseSync(path, source, { lang });
1176
- const references = translationReferences(path, source, lang);
1329
+ let parsed: ReturnType<typeof parseSync>;
1330
+
1331
+ try {
1332
+ parsed = parseSync(projectSourcePath(root, path), source, { lang });
1333
+ } catch (error) {
1334
+ throw sourceExtractionError(root, path, errorSourcePoint(error), error);
1335
+ }
1336
+
1337
+ const references = translationReferences(root, path, source, lang);
1177
1338
 
1178
1339
  new Visitor({
1179
1340
  CallExpression(node) {
1180
- assertSupportedTranslationCall(node, references);
1341
+ try {
1342
+ assertSupportedTranslationCall(node, references);
1343
+ } catch (error) {
1344
+ throw sourceExtractionError(root, path, references.sourcePoints.get(node.start) ?? null, error);
1345
+ }
1181
1346
  },
1182
1347
  JSXOpeningElement(node) {
1183
- assertSupportedTransElement(node, references);
1348
+ try {
1349
+ assertSupportedTransElement(node, references);
1350
+ } catch (error) {
1351
+ throw sourceExtractionError(root, path, references.sourcePoints.get(node.start) ?? null, error);
1352
+ }
1184
1353
  },
1185
1354
  }).visit(parsed.program);
1186
1355
 
@@ -1476,12 +1645,19 @@ const extractedSourcePoints = (root: string, entries: ExtractedKeysMap): Set<str
1476
1645
  return points;
1477
1646
  };
1478
1647
 
1479
- const hasMissingSourcePoint = (
1648
+ const missingSourcePoint = (
1480
1649
  file: string,
1481
1650
  references: TranslationReferences,
1482
1651
  extracted: Set<string>,
1483
- ): boolean =>
1484
- references.locations.values().some((location) => !extracted.has(`${file}\0${location}`));
1652
+ ): SourcePoint | null => {
1653
+ for (const [location, point] of references.locations) {
1654
+ if (!extracted.has(`${file}\0${location}`)) {
1655
+ return point;
1656
+ }
1657
+ }
1658
+
1659
+ return null;
1660
+ };
1485
1661
 
1486
1662
  const assertAllReferencesExtracted = (
1487
1663
  root: string,
@@ -1491,8 +1667,15 @@ const assertAllReferencesExtracted = (
1491
1667
  const extracted = extractedSourcePoints(root, entries);
1492
1668
 
1493
1669
  for (const [file, sourceReferences] of references) {
1494
- if (hasMissingSourcePoint(file, sourceReferences, extracted)) {
1495
- throw new Error("A translation expression could not be extracted safely");
1670
+ const point = missingSourcePoint(file, sourceReferences, extracted);
1671
+
1672
+ if (point !== null) {
1673
+ throw sourceExtractionError(
1674
+ root,
1675
+ file,
1676
+ point,
1677
+ new Error("A translation expression could not be extracted safely"),
1678
+ );
1496
1679
  }
1497
1680
  }
1498
1681
  };
@@ -1508,7 +1691,7 @@ const findSourceMessages = async (root: string, sourceFiles: string[]): Promise<
1508
1691
  const references: Map<string, TranslationReferences> = new Map();
1509
1692
 
1510
1693
  for (const [file, source] of sources) {
1511
- references.set(file, assertSupportedTranslationSyntax(file, source));
1694
+ references.set(file, assertSupportedTranslationSyntax(root, file, source));
1512
1695
  }
1513
1696
 
1514
1697
  let extracted: ExtractedKeysMap | undefined;
@@ -1542,7 +1725,7 @@ const findSourceMessages = async (root: string, sourceFiles: string[]): Promise<
1542
1725
  throw new Error(reports.join("\n") || "i18next extraction failed");
1543
1726
  }
1544
1727
 
1545
- return sourceMessages(extracted.values().toArray())
1728
+ return sourceMessages(root, extracted.values().toArray())
1546
1729
  .map((message) => inferLocations(message, sources))
1547
1730
  .toSorted(compareMessages);
1548
1731
  };
@@ -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,
@@ -56,8 +56,22 @@ const isReusableBuildDirectory = (path: string): boolean => {
56
56
  const buildOutputError = (root: string): Error =>
57
57
  new Error(`Build output must be an empty directory or an earlier GTKX build below the project root ${root}`);
58
58
 
59
+ const hasGtkxBuildAncestor = (root: string, outDir: string): boolean => {
60
+ let ancestor = dirname(outDir);
61
+
62
+ while (isPathInside(root, ancestor)) {
63
+ if (isGtkxBuildDirectory(ancestor)) {
64
+ return true;
65
+ }
66
+
67
+ ancestor = dirname(ancestor);
68
+ }
69
+
70
+ return false;
71
+ };
72
+
59
73
  const assertSafeBuildLocation = (root: string, outDir: string): void => {
60
- if (!isPathInside(root, outDir) || hasSymlinkComponent(root, outDir)) {
74
+ if (!isPathInside(root, outDir) || hasSymlinkComponent(root, outDir) || hasGtkxBuildAncestor(root, outDir)) {
61
75
  throw buildOutputError(root);
62
76
  }
63
77
  };
@@ -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 };
@@ -0,0 +1,110 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ type ProcessIdentity = {
4
+ pid: number;
5
+ parentId: number;
6
+ processGroupId: number;
7
+ startTime: string;
8
+ state: string;
9
+ };
10
+
11
+ type ProcessGroupOwner = Pick<ProcessIdentity, "pid" | "startTime">;
12
+ type ProcessGroupOwnerCapture = { isCaptured: true; owner: ProcessGroupOwner | undefined } |
13
+ { isCaptured: false; error: Error };
14
+
15
+ const initialParentId = process.ppid;
16
+ const STOPPED_PROCESS_STATES: Set<string> = new Set(["Z", "X", "x"]);
17
+
18
+ const processIdentity = (pid: number): ProcessIdentity => {
19
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
20
+ const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ");
21
+ const state = fields[0];
22
+ const parentId = Number(fields[1]);
23
+ const processGroupId = Number(fields[2]);
24
+ const startTime = fields[19];
25
+
26
+ if (
27
+ state === undefined ||
28
+ startTime === undefined ||
29
+ !Number.isSafeInteger(parentId) ||
30
+ !Number.isSafeInteger(processGroupId)
31
+ ) {
32
+ throw new Error(`Failed to identify process ${String(pid)}`);
33
+ }
34
+
35
+ return { pid, parentId, processGroupId, startTime, state };
36
+ };
37
+
38
+ const ancestorFor = (process: ProcessIdentity, ancestorId: number): ProcessIdentity | undefined => {
39
+ let current = process;
40
+
41
+ while (current.parentId > 1) {
42
+ const parent = processIdentity(current.parentId);
43
+
44
+ if (parent.pid === ancestorId) {
45
+ return parent;
46
+ }
47
+
48
+ current = parent;
49
+ }
50
+
51
+ return undefined;
52
+ };
53
+
54
+ const processGroupOwner = (process: ProcessIdentity): ProcessGroupOwner => {
55
+ if (process.processGroupId !== process.pid || STOPPED_PROCESS_STATES.has(process.state)) {
56
+ throw new Error(`Failed to identify process group ${String(process.pid)}`);
57
+ }
58
+
59
+ return { pid: process.pid, startTime: process.startTime };
60
+ };
61
+
62
+ const processGroupOwnerFor = (process: ProcessIdentity): ProcessGroupOwner | undefined => {
63
+ if (process.processGroupId === process.pid) {
64
+ return undefined;
65
+ }
66
+
67
+ const groupLeader = processGroupOwner(processIdentity(process.processGroupId));
68
+ const owner = ancestorFor(process, groupLeader.pid);
69
+
70
+ if (owner !== undefined) {
71
+ return groupLeader;
72
+ }
73
+
74
+ const currentGroupLeader = processGroupOwner(processIdentity(groupLeader.pid));
75
+
76
+ if (currentGroupLeader.startTime !== groupLeader.startTime) {
77
+ throw new Error(`Failed to identify process group ${String(groupLeader.pid)}`);
78
+ }
79
+
80
+ return undefined;
81
+ };
82
+
83
+ const captureProcessGroupOwner = (): ProcessGroupOwnerCapture => {
84
+ try {
85
+ const initialProcess = processIdentity(process.pid);
86
+
87
+ if (initialProcess.parentId !== initialParentId) {
88
+ throw new Error("The process that launched GTKX exited during startup");
89
+ }
90
+
91
+ return { isCaptured: true, owner: processGroupOwnerFor(initialProcess) };
92
+ } catch (error) {
93
+ return {
94
+ isCaptured: false,
95
+ error: error instanceof Error ? error : new Error("Failed to identify the process that launched GTKX"),
96
+ };
97
+ }
98
+ };
99
+
100
+ const capturedProcessGroupOwner = captureProcessGroupOwner();
101
+
102
+ const getInitialProcessGroupOwner = (): ProcessGroupOwner | undefined => {
103
+ if (!capturedProcessGroupOwner.isCaptured) {
104
+ throw capturedProcessGroupOwner.error;
105
+ }
106
+
107
+ return capturedProcessGroupOwner.owner;
108
+ };
109
+
110
+ export { getInitialProcessGroupOwner, initialParentId };
@@ -3,11 +3,11 @@ import { ensureGenerated } from "../codegen/run-codegen.js";
3
3
 
4
4
  const TEST_MODE = "test";
5
5
 
6
- const gtkxEnsureStore = (): Plugin => ({
6
+ const gtkxEnsureStore = (configFile?: string): Plugin => ({
7
7
  name: "gtkx:ensure-store",
8
8
 
9
9
  async config() {
10
- await ensureGenerated(process.cwd(), { shouldAnnounce: true, mode: TEST_MODE });
10
+ await ensureGenerated(process.cwd(), { shouldAnnounce: true, mode: TEST_MODE, configFile });
11
11
  },
12
12
  });
13
13
 
@@ -1,7 +1,7 @@
1
1
  import type { ConfigLoader } from "@gtkx/config";
2
2
  import type { Plugin, ResolvedConfig } from "vite";
3
3
  import { createConfigLoader } from "@gtkx/config/internal";
4
- import { isPathWithin, toPosixPath } from "@gtkx/utils";
4
+ import { isPathWithin, logger, toPosixPath } from "@gtkx/utils";
5
5
  import { existsSync, mkdirSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { dirname, join, posix, relative, resolve } from "node:path";
@@ -23,6 +23,7 @@ type I18nState = {
23
23
  outDir: string;
24
24
  project: CatalogProject | null;
25
25
  extraction: Promise<void>;
26
+ hotUpdateTimestamp: number | null;
26
27
  };
27
28
 
28
29
  const BOOTSTRAP_SPECIFIER = "@gtkx/i18n/bootstrap";
@@ -159,12 +160,11 @@ const isProjectSource = (state: I18nState, path: string): boolean => {
159
160
  const refreshProjectMessages = async (
160
161
  state: I18nState,
161
162
  shouldPreserveMetadataMessages: boolean,
162
- reportError: (message: string) => void,
163
163
  ): Promise<void> => {
164
164
  try {
165
165
  await queueExtraction(state, shouldPreserveMetadataMessages, false);
166
166
  } catch (error) {
167
- reportError(error instanceof Error ? error.message : String(error));
167
+ logger.error(error instanceof Error ? error.message : String(error));
168
168
  }
169
169
  };
170
170
 
@@ -176,6 +176,7 @@ const gtkxI18n = (
176
176
  const state: I18nState = {
177
177
  entryPath,
178
178
  extraction: Promise.resolve(),
179
+ hotUpdateTimestamp: null,
179
180
  i18nRoot: "",
180
181
  outDir: "",
181
182
  project: null,
@@ -190,17 +191,13 @@ const gtkxI18n = (
190
191
  buildStart: () => queueExtraction(state, shouldPreserveMetadataMessages, true),
191
192
 
192
193
  hotUpdate(options) {
193
- if (!isProjectSource(state, options.file)) {
194
+ if (!isProjectSource(state, options.file) || state.hotUpdateTimestamp === options.timestamp) {
194
195
  return;
195
196
  }
196
197
 
197
- return refreshProjectMessages(
198
- state,
199
- shouldPreserveMetadataMessages,
200
- (message) => {
201
- options.server.config.logger.error(message);
202
- },
203
- );
198
+ state.hotUpdateTimestamp = options.timestamp;
199
+
200
+ return refreshProjectMessages(state, shouldPreserveMetadataMessages);
204
201
  },
205
202
 
206
203
  transform(code, id) {