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

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 (88) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/codegen/config-dependencies.d.ts +3 -0
  4. package/dist/codegen/config-dependencies.d.ts.map +1 -0
  5. package/dist/codegen/config-dependencies.js +355 -0
  6. package/dist/codegen/config-dependencies.js.map +1 -0
  7. package/dist/codegen/run-codegen.d.ts +3 -2
  8. package/dist/codegen/run-codegen.d.ts.map +1 -1
  9. package/dist/codegen/run-codegen.js +31 -6
  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 +4 -2
  14. package/dist/codegen/store-resolver.js.map +1 -1
  15. package/dist/commands/cleanup.d.ts +9 -0
  16. package/dist/commands/cleanup.d.ts.map +1 -0
  17. package/dist/commands/cleanup.js +31 -0
  18. package/dist/commands/cleanup.js.map +1 -0
  19. package/dist/commands/dev.d.ts.map +1 -1
  20. package/dist/commands/dev.js +4 -2
  21. package/dist/commands/dev.js.map +1 -1
  22. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  23. package/dist/deploy/freedesktop/validate.js +45 -12
  24. package/dist/deploy/freedesktop/validate.js.map +1 -1
  25. package/dist/deploy/payload/stage.d.ts.map +1 -1
  26. package/dist/deploy/payload/stage.js +24 -3
  27. package/dist/deploy/payload/stage.js.map +1 -1
  28. package/dist/deploy/settings/extra-files.d.ts.map +1 -1
  29. package/dist/deploy/settings/extra-files.js +4 -3
  30. package/dist/deploy/settings/extra-files.js.map +1 -1
  31. package/dist/dev/catalog-writes.d.ts +8 -0
  32. package/dist/dev/catalog-writes.d.ts.map +1 -0
  33. package/dist/dev/catalog-writes.js +26 -0
  34. package/dist/dev/catalog-writes.js.map +1 -0
  35. package/dist/dev/runner-deps.d.ts.map +1 -1
  36. package/dist/dev/runner-deps.js +10 -6
  37. package/dist/dev/runner-deps.js.map +1 -1
  38. package/dist/dev/runner.d.ts +1 -0
  39. package/dist/dev/runner.d.ts.map +1 -1
  40. package/dist/dev/runner.js +8 -2
  41. package/dist/dev/runner.js.map +1 -1
  42. package/dist/dev/supervisor.d.ts +2 -1
  43. package/dist/dev/supervisor.d.ts.map +1 -1
  44. package/dist/dev/supervisor.js +128 -26
  45. package/dist/dev/supervisor.js.map +1 -1
  46. package/dist/i18n/catalogs.d.ts +6 -2
  47. package/dist/i18n/catalogs.d.ts.map +1 -1
  48. package/dist/i18n/catalogs.js +9 -7
  49. package/dist/i18n/catalogs.js.map +1 -1
  50. package/dist/i18n/source-messages.d.ts +3 -1
  51. package/dist/i18n/source-messages.d.ts.map +1 -1
  52. package/dist/i18n/source-messages.js +14 -2
  53. package/dist/i18n/source-messages.js.map +1 -1
  54. package/dist/internal/build-output.d.ts.map +1 -1
  55. package/dist/internal/build-output.js +45 -8
  56. package/dist/internal/build-output.js.map +1 -1
  57. package/dist/internal/prepare-project.d.ts +1 -0
  58. package/dist/internal/prepare-project.d.ts.map +1 -1
  59. package/dist/internal/prepare-project.js +8 -2
  60. package/dist/internal/prepare-project.js.map +1 -1
  61. package/dist/vite-plugins/i18n.d.ts +11 -2
  62. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  63. package/dist/vite-plugins/i18n.js +20 -6
  64. package/dist/vite-plugins/i18n.js.map +1 -1
  65. package/dist/vite-plugins/index.d.ts +2 -0
  66. package/dist/vite-plugins/index.d.ts.map +1 -1
  67. package/dist/vite-plugins/index.js +10 -2
  68. package/dist/vite-plugins/index.js.map +1 -1
  69. package/package.json +12 -10
  70. package/src/cli.ts +5 -0
  71. package/src/codegen/config-dependencies.ts +470 -0
  72. package/src/codegen/run-codegen.ts +37 -5
  73. package/src/codegen/store-resolver.ts +5 -2
  74. package/src/commands/cleanup.ts +36 -0
  75. package/src/commands/dev.ts +9 -2
  76. package/src/deploy/freedesktop/validate.ts +63 -13
  77. package/src/deploy/payload/stage.ts +42 -3
  78. package/src/deploy/settings/extra-files.ts +5 -3
  79. package/src/dev/catalog-writes.ts +35 -0
  80. package/src/dev/runner-deps.ts +14 -6
  81. package/src/dev/runner.ts +11 -2
  82. package/src/dev/supervisor.ts +177 -29
  83. package/src/i18n/catalogs.ts +20 -8
  84. package/src/i18n/source-messages.ts +19 -3
  85. package/src/internal/build-output.ts +67 -9
  86. package/src/internal/prepare-project.ts +9 -3
  87. package/src/vite-plugins/i18n.ts +42 -9
  88. package/src/vite-plugins/index.ts +19 -3
@@ -28,11 +28,17 @@ type CatalogProject = {
28
28
  };
29
29
 
30
30
  type PreparedCatalog = {
31
+ content: Buffer;
31
32
  isChanged: boolean;
32
33
  output: string;
33
34
  target: string;
34
35
  };
35
36
 
37
+ type WrittenCatalog = {
38
+ content: Buffer;
39
+ path: string;
40
+ };
41
+
36
42
  const PO_DIRNAME = "po";
37
43
  const LOCALE_DIRNAME = "locale";
38
44
  const LINGUAS_FILENAME = "LINGUAS";
@@ -165,16 +171,25 @@ const prepareCatalog = (
165
171
  initializeCatalog(catalog, template, output);
166
172
  }
167
173
 
174
+ const content = readFileSync(output);
175
+
168
176
  return {
169
- isChanged: !isExisting || !readFileSync(output).equals(readFileSync(catalog.path)),
177
+ content,
178
+ isChanged: !isExisting || !content.equals(readFileSync(catalog.path)),
170
179
  output,
171
180
  target: catalog.path,
172
181
  };
173
182
  };
174
183
 
175
- const synchronizeCatalogs = (project: CatalogProject): void => {
184
+ const replaceCatalog = (catalog: PreparedCatalog): WrittenCatalog => {
185
+ renameSync(catalog.output, catalog.target);
186
+
187
+ return { content: catalog.content, path: catalog.target };
188
+ };
189
+
190
+ const synchronizeCatalogs = (project: CatalogProject): WrittenCatalog[] => {
176
191
  if (project.catalogs.length === 0) {
177
- return;
192
+ return [];
178
193
  }
179
194
 
180
195
  const template = join(project.poDir, `${project.domain}.pot`);
@@ -185,11 +200,7 @@ const synchronizeCatalogs = (project: CatalogProject): void => {
185
200
  const merged = project.catalogs.map((catalog, index) =>
186
201
  prepareCatalog(catalog, index, template, stagingDir));
187
202
 
188
- for (const catalog of merged) {
189
- if (catalog.isChanged) {
190
- renameSync(catalog.output, catalog.target);
191
- }
192
- }
203
+ return merged.filter((catalog) => catalog.isChanged).map((catalog) => replaceCatalog(catalog));
193
204
  } finally {
194
205
  rmSync(stagingDir, { recursive: true, force: true });
195
206
  }
@@ -215,4 +226,5 @@ export {
215
226
  requiresCatalogInitialization,
216
227
  resolveCatalogProject,
217
228
  synchronizeCatalogs,
229
+ type WrittenCatalog,
218
230
  };
@@ -116,8 +116,24 @@ const STATIC_KEY_WRAPPER_TYPES: ReadonlySet<string> = new Set([
116
116
 
117
117
  class SourceExtractionError extends Error {}
118
118
 
119
- const sourceErrorMessage = (error: unknown): string =>
120
- error instanceof Error ? error.message : String(error);
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
+ };
121
137
 
122
138
  const projectSourcePath = (root: string, path: string): string => {
123
139
  const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
@@ -1968,4 +1984,4 @@ const extractSourceCatalog = async (
1968
1984
  extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages });
1969
1985
  };
1970
1986
 
1971
- export { extractSourceCatalog, extractSourceCatalogTo };
1987
+ export { extractSourceCatalog, extractSourceCatalogTo, SourceExtractionError };
@@ -53,27 +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) || ".";
58
57
 
59
- const hasGtkxBuildAncestor = (root: string, outDir: string): boolean => {
58
+ const gtkxBuildAncestor = (root: string, outDir: string): string | null => {
60
59
  let ancestor = dirname(outDir);
61
60
 
62
61
  while (isPathInside(root, ancestor)) {
63
62
  if (isGtkxBuildDirectory(ancestor)) {
64
- return true;
63
+ return ancestor;
65
64
  }
66
65
 
67
66
  ancestor = dirname(ancestor);
68
67
  }
69
68
 
70
- return false;
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;
71
95
  };
72
96
 
73
97
  const assertSafeBuildLocation = (root: string, outDir: string): void => {
74
- if (!isPathInside(root, outDir) || hasSymlinkComponent(root, outDir) || hasGtkxBuildAncestor(root, outDir)) {
75
- throw buildOutputError(root);
98
+ if (!isPathInside(root, outDir)) {
99
+ throw new Error(`Build output ${outDir} must be below the project root ${root}`);
76
100
  }
101
+
102
+ if (hasSymlinkComponent(root, outDir)) {
103
+ throw new Error(`Build output ${outputName(root, outDir)} passes through a symbolic link`);
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
+ );
77
135
  };
78
136
 
79
137
  const resolveBuildOutDir = (root: string, configured?: string): string => {
@@ -83,7 +141,7 @@ const resolveBuildOutDir = (root: string, configured?: string): string => {
83
141
  assertSafeBuildLocation(root, outDir);
84
142
 
85
143
  if (!isReusableBuildDirectory(outDir)) {
86
- throw buildOutputError(root);
144
+ throw nonReusableBuildOutputError(root, outDir);
87
145
  }
88
146
 
89
147
  return outDir;
@@ -94,7 +152,7 @@ const prepareBuildOutDir = (root: string, outDir: string): PreparedBuildOutDir =
94
152
  const prepared = prepareOutputDirectory(root, outDir, isReusableBuildDirectory);
95
153
 
96
154
  if (prepared.status === "unsafe") {
97
- throw buildOutputError(root);
155
+ throw new Error(`Build output ${outputName(root, outDir)} became unsafe while preparing it`);
98
156
  }
99
157
 
100
158
  const transaction: OutputDirectoryTransaction = prepared.transaction;
@@ -3,21 +3,27 @@ import { type CodegenContext, resolveCodegenContext } from "../codegen/store-res
3
3
  import { resolveCwd, resolveEntry } from "./entry-arg.js";
4
4
 
5
5
  type ProjectArgs = { entry?: string; cwd?: string; config?: string };
6
- type PreparedProject = { cwd: string; entry: string; configFile: string };
6
+ type PreparedProject = { cwd: string; entry: string; configFile: string; configDependencies: string[] };
7
7
  type ResolvedProject = PreparedProject & { context: CodegenContext };
8
8
 
9
9
  const resolveProject = async (args: ProjectArgs, mode: string): Promise<ResolvedProject> => {
10
10
  const cwd = resolveCwd(args);
11
11
  const context = await resolveCodegenContext(cwd, mode, args.config);
12
12
 
13
- return { cwd, entry: resolveEntry(cwd, args.entry), configFile: context.configFile, context };
13
+ return {
14
+ cwd,
15
+ entry: resolveEntry(cwd, args.entry),
16
+ configFile: context.configFile,
17
+ configDependencies: context.configDependencies,
18
+ context,
19
+ };
14
20
  };
15
21
 
16
22
  const prepareProject = async (args: ProjectArgs, mode: string): Promise<PreparedProject> => {
17
23
  const { cwd, entry, context } = await resolveProject(args, mode);
18
24
  await ensureGeneratedIn(context, { shouldAnnounce: true, mode });
19
25
 
20
- return { cwd, entry, configFile: context.configFile };
26
+ return { cwd, entry, configFile: context.configFile, configDependencies: context.configDependencies };
21
27
  };
22
28
 
23
29
  export { prepareProject, resolveProject };
@@ -11,12 +11,23 @@ import {
11
11
  LOCALE_DIRNAME,
12
12
  resolveCatalogProject,
13
13
  synchronizeCatalogs,
14
+ type WrittenCatalog,
14
15
  } from "../i18n/catalogs.js";
15
- import { extractSourceCatalog } from "../i18n/source-messages.js";
16
+ import { extractSourceCatalog, SourceExtractionError } from "../i18n/source-messages.js";
16
17
  import { emitI18nTypes } from "../i18n/types.js";
17
18
  import { discoverSourceFiles, sourceLanguage } from "../internal/source-imports.js";
18
19
  import { stripQuery } from "./strip-query.js";
19
20
 
21
+ type CatalogWriteListener = (catalogs: WrittenCatalog[]) => void;
22
+
23
+ type I18nPluginOptions = {
24
+ entryPath: string;
25
+ loadConfig?: ConfigLoader | undefined;
26
+ onCatalogsWritten?: CatalogWriteListener | undefined;
27
+ shouldPreserveMetadataMessages?: boolean | undefined;
28
+ shouldRecoverExtractionErrors?: boolean | undefined;
29
+ };
30
+
20
31
  type I18nState = {
21
32
  entryPath: string;
22
33
  i18nRoot: string;
@@ -24,6 +35,7 @@ type I18nState = {
24
35
  project: CatalogProject | null;
25
36
  extraction: Promise<void>;
26
37
  hotUpdateTimestamp: number | null;
38
+ onCatalogsWritten: CatalogWriteListener;
27
39
  };
28
40
 
29
41
  const BOOTSTRAP_SPECIFIER = "@gtkx/i18n/bootstrap";
@@ -110,7 +122,7 @@ const extractProjectMessages = async (
110
122
  await extractSourceCatalog(project, sourceFiles, shouldPreserveMetadataMessages);
111
123
 
112
124
  if (shouldSynchronizeCatalogs) {
113
- synchronizeCatalogs(project);
125
+ state.onCatalogsWritten(synchronizeCatalogs(project));
114
126
  }
115
127
 
116
128
  await emitI18nTypes(project.root);
@@ -160,19 +172,37 @@ const isProjectSource = (state: I18nState, path: string): boolean => {
160
172
  const refreshProjectMessages = async (
161
173
  state: I18nState,
162
174
  shouldPreserveMetadataMessages: boolean,
175
+ shouldSynchronizeCatalogs = false,
163
176
  ): Promise<void> => {
164
177
  try {
165
- await queueExtraction(state, shouldPreserveMetadataMessages, false);
178
+ await queueExtraction(state, shouldPreserveMetadataMessages, shouldSynchronizeCatalogs);
166
179
  } catch (error) {
167
180
  logger.error(error instanceof Error ? error.message : String(error));
168
181
  }
169
182
  };
170
183
 
171
- const gtkxI18n = (
172
- entryPath: string,
173
- loadConfig: ConfigLoader = createConfigLoader(),
184
+ const recoverInitialSourceExtraction = async (
185
+ state: I18nState,
186
+ shouldPreserveMetadataMessages: boolean,
187
+ ): Promise<void> => {
188
+ try {
189
+ await queueExtraction(state, shouldPreserveMetadataMessages, true);
190
+ } catch (error) {
191
+ if (!(error instanceof SourceExtractionError)) {
192
+ throw error;
193
+ }
194
+
195
+ logger.error(error.message);
196
+ }
197
+ };
198
+
199
+ const gtkxI18n = ({
200
+ entryPath,
201
+ loadConfig = createConfigLoader(),
202
+ onCatalogsWritten = (): void => undefined,
174
203
  shouldPreserveMetadataMessages = true,
175
- ): Plugin => {
204
+ shouldRecoverExtractionErrors = false,
205
+ }: I18nPluginOptions): Plugin => {
176
206
  const state: I18nState = {
177
207
  entryPath,
178
208
  extraction: Promise.resolve(),
@@ -180,6 +210,7 @@ const gtkxI18n = (
180
210
  i18nRoot: "",
181
211
  outDir: "",
182
212
  project: null,
213
+ onCatalogsWritten,
183
214
  };
184
215
 
185
216
  return {
@@ -188,7 +219,9 @@ const gtkxI18n = (
188
219
 
189
220
  configResolved: (config) => applyResolvedConfig(state, config, loadConfig),
190
221
 
191
- buildStart: () => queueExtraction(state, shouldPreserveMetadataMessages, true),
222
+ buildStart: () => shouldRecoverExtractionErrors
223
+ ? recoverInitialSourceExtraction(state, shouldPreserveMetadataMessages)
224
+ : queueExtraction(state, shouldPreserveMetadataMessages, true),
192
225
 
193
226
  hotUpdate(options) {
194
227
  if (!isProjectSource(state, options.file) || state.hotUpdateTimestamp === options.timestamp) {
@@ -222,4 +255,4 @@ const gtkxI18n = (
222
255
  };
223
256
  };
224
257
 
225
- export { gtkxI18n };
258
+ export { type CatalogWriteListener, gtkxI18n };
@@ -5,7 +5,7 @@ import type { BuildManifestCollector } from "../internal/build-manifest.js";
5
5
  import { gtkxAssetImports } from "./asset-imports.js";
6
6
  import { gtkxBuiltUrl } from "./built-url.js";
7
7
  import { gtkxCss } from "./css.js";
8
- import { gtkxI18n } from "./i18n.js";
8
+ import { type CatalogWriteListener, gtkxI18n } from "./i18n.js";
9
9
  import { gtkxIcons } from "./icons.js";
10
10
  import { gtkxReactCompiler } from "./react-compiler.js";
11
11
  import { gtkxResources } from "./resources.js";
@@ -18,11 +18,19 @@ type GtkxVitePluginOptions = {
18
18
  configFile?: string | undefined;
19
19
  entryPath?: string | undefined;
20
20
  mode?: string | undefined;
21
+ onCatalogsWritten?: CatalogWriteListener | undefined;
21
22
  shouldPreserveI18nMetadata?: boolean | undefined;
22
23
  };
23
24
 
24
25
  const gtkxVitePlugins = (options: GtkxVitePluginOptions = {}): Plugin[] => {
25
- const { buildManifest, configFile, entryPath, mode, shouldPreserveI18nMetadata = true } = options;
26
+ const {
27
+ buildManifest,
28
+ configFile,
29
+ entryPath,
30
+ mode,
31
+ onCatalogsWritten,
32
+ shouldPreserveI18nMetadata = true,
33
+ } = options;
26
34
  const loadConfig = createConfigLoader({
27
35
  ...(mode !== undefined && { mode }),
28
36
  ...(configFile !== undefined && { configFile }),
@@ -30,7 +38,15 @@ const gtkxVitePlugins = (options: GtkxVitePluginOptions = {}): Plugin[] => {
30
38
 
31
39
  return [
32
40
  createConfigPlugin({ name: "gtkx:config", loadConfig }),
33
- ...(entryPath === undefined ? [] : [gtkxI18n(entryPath, loadConfig, shouldPreserveI18nMetadata)]),
41
+ ...(entryPath === undefined
42
+ ? []
43
+ : [gtkxI18n({
44
+ entryPath,
45
+ loadConfig,
46
+ onCatalogsWritten,
47
+ shouldPreserveMetadataMessages: shouldPreserveI18nMetadata,
48
+ shouldRecoverExtractionErrors: mode === "development",
49
+ })]),
34
50
  gtkxStoreLinks(),
35
51
  gtkxUndeclaredLibrary(loadConfig),
36
52
  gtkxSettings(buildManifest),