@gtkx/cli 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +1 -1
  2. package/dist/builder.d.ts +1 -0
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +2 -2
  5. package/dist/builder.js.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codegen/freshness.d.ts.map +1 -1
  9. package/dist/codegen/freshness.js +2 -1
  10. package/dist/codegen/freshness.js.map +1 -1
  11. package/dist/codegen/reference.d.ts +17 -0
  12. package/dist/codegen/reference.d.ts.map +1 -0
  13. package/dist/codegen/reference.js +37 -0
  14. package/dist/codegen/reference.js.map +1 -0
  15. package/dist/codegen/report.d.ts.map +1 -1
  16. package/dist/codegen/report.js +17 -3
  17. package/dist/codegen/report.js.map +1 -1
  18. package/dist/codegen/run-codegen.d.ts +4 -0
  19. package/dist/codegen/run-codegen.d.ts.map +1 -1
  20. package/dist/codegen/run-codegen.js +93 -27
  21. package/dist/codegen/run-codegen.js.map +1 -1
  22. package/dist/commands/codegen.js +1 -1
  23. package/dist/commands/codegen.js.map +1 -1
  24. package/dist/commands/docs.d.ts.map +1 -1
  25. package/dist/commands/docs.js +10 -16
  26. package/dist/commands/docs.js.map +1 -1
  27. package/dist/commands/mcp.d.ts +13 -0
  28. package/dist/commands/mcp.d.ts.map +1 -0
  29. package/dist/commands/mcp.js +68 -0
  30. package/dist/commands/mcp.js.map +1 -0
  31. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  32. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  33. package/dist/deploy/freedesktop/localize.js +107 -0
  34. package/dist/deploy/freedesktop/localize.js.map +1 -0
  35. package/dist/deploy/install-hints.d.ts.map +1 -1
  36. package/dist/deploy/install-hints.js +5 -0
  37. package/dist/deploy/install-hints.js.map +1 -1
  38. package/dist/deploy/notices/gtkx.js +1 -1
  39. package/dist/deploy/notices/gtkx.js.map +1 -1
  40. package/dist/deploy/notices/libraries.d.ts.map +1 -1
  41. package/dist/deploy/notices/libraries.js +2 -1
  42. package/dist/deploy/notices/libraries.js.map +1 -1
  43. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  44. package/dist/deploy/payload/launcher.js +2 -0
  45. package/dist/deploy/payload/launcher.js.map +1 -1
  46. package/dist/deploy/payload/stage.d.ts +1 -1
  47. package/dist/deploy/payload/stage.d.ts.map +1 -1
  48. package/dist/deploy/payload/stage.js +20 -11
  49. package/dist/deploy/payload/stage.js.map +1 -1
  50. package/dist/deploy/run-deploy.d.ts.map +1 -1
  51. package/dist/deploy/run-deploy.js +87 -15
  52. package/dist/deploy/run-deploy.js.map +1 -1
  53. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  54. package/dist/deploy/settings/libraries.js +7 -9
  55. package/dist/deploy/settings/libraries.js.map +1 -1
  56. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  57. package/dist/deploy/targets/flatpak-source.js +21 -10
  58. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  59. package/dist/deploy/tools.d.ts +6 -1
  60. package/dist/deploy/tools.d.ts.map +1 -1
  61. package/dist/deploy/tools.js +27 -4
  62. package/dist/deploy/tools.js.map +1 -1
  63. package/dist/dev/locale-dir.d.ts +3 -0
  64. package/dist/dev/locale-dir.d.ts.map +1 -0
  65. package/dist/dev/locale-dir.js +17 -0
  66. package/dist/dev/locale-dir.js.map +1 -0
  67. package/dist/dev/runner-main.d.ts.map +1 -1
  68. package/dist/dev/runner-main.js +4 -1
  69. package/dist/dev/runner-main.js.map +1 -1
  70. package/dist/dev/runner.d.ts.map +1 -1
  71. package/dist/dev/runner.js +6 -0
  72. package/dist/dev/runner.js.map +1 -1
  73. package/dist/dev/vite-dev-server.d.ts +1 -0
  74. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  75. package/dist/dev/vite-dev-server.js.map +1 -1
  76. package/dist/i18n/binding-extractor.d.ts +30 -0
  77. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  78. package/dist/i18n/binding-extractor.js +1324 -0
  79. package/dist/i18n/binding-extractor.js.map +1 -0
  80. package/dist/i18n/catalog-template.d.ts +3 -0
  81. package/dist/i18n/catalog-template.d.ts.map +1 -0
  82. package/dist/i18n/catalog-template.js +18 -0
  83. package/dist/i18n/catalog-template.js.map +1 -0
  84. package/dist/i18n/catalogs.d.ts +21 -0
  85. package/dist/i18n/catalogs.d.ts.map +1 -0
  86. package/dist/i18n/catalogs.js +139 -0
  87. package/dist/i18n/catalogs.js.map +1 -0
  88. package/dist/i18n/extractor-plugin.d.ts +8 -0
  89. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  90. package/dist/i18n/extractor-plugin.js +247 -0
  91. package/dist/i18n/extractor-plugin.js.map +1 -0
  92. package/dist/i18n/metadata-templates.d.ts +11 -0
  93. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  94. package/dist/i18n/metadata-templates.js +11 -0
  95. package/dist/i18n/metadata-templates.js.map +1 -0
  96. package/dist/i18n/source-messages.d.ts +23 -0
  97. package/dist/i18n/source-messages.d.ts.map +1 -0
  98. package/dist/i18n/source-messages.js +403 -0
  99. package/dist/i18n/source-messages.js.map +1 -0
  100. package/dist/i18n/types.d.ts +10 -0
  101. package/dist/i18n/types.d.ts.map +1 -0
  102. package/dist/i18n/types.js +108 -0
  103. package/dist/i18n/types.js.map +1 -0
  104. package/dist/internal/agent-rules.d.ts +7 -0
  105. package/dist/internal/agent-rules.d.ts.map +1 -0
  106. package/dist/internal/agent-rules.js +106 -0
  107. package/dist/internal/agent-rules.js.map +1 -0
  108. package/dist/internal/docs-elements.d.ts +8 -0
  109. package/dist/internal/docs-elements.d.ts.map +1 -0
  110. package/dist/internal/docs-elements.js +12 -0
  111. package/dist/internal/docs-elements.js.map +1 -0
  112. package/dist/internal/source-imports.d.ts +5 -1
  113. package/dist/internal/source-imports.d.ts.map +1 -1
  114. package/dist/internal/source-imports.js +25 -47
  115. package/dist/internal/source-imports.js.map +1 -1
  116. package/dist/mcp/client-config.d.ts +22 -0
  117. package/dist/mcp/client-config.d.ts.map +1 -0
  118. package/dist/mcp/client-config.js +94 -0
  119. package/dist/mcp/client-config.js.map +1 -0
  120. package/dist/mcp/client.d.ts +0 -1
  121. package/dist/mcp/client.d.ts.map +1 -1
  122. package/dist/mcp/client.js +11 -39
  123. package/dist/mcp/client.js.map +1 -1
  124. package/dist/mcp/handlers.d.ts +2 -1
  125. package/dist/mcp/handlers.d.ts.map +1 -1
  126. package/dist/mcp/handlers.js.map +1 -1
  127. package/dist/settings/render.d.ts +4 -1
  128. package/dist/settings/render.d.ts.map +1 -1
  129. package/dist/settings/render.js +11 -2
  130. package/dist/settings/render.js.map +1 -1
  131. package/dist/settings/schema.d.ts.map +1 -1
  132. package/dist/settings/schema.js +3 -1
  133. package/dist/settings/schema.js.map +1 -1
  134. package/dist/vite-plugins/animated.d.ts +5 -0
  135. package/dist/vite-plugins/animated.d.ts.map +1 -0
  136. package/dist/vite-plugins/animated.js +325 -0
  137. package/dist/vite-plugins/animated.js.map +1 -0
  138. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  139. package/dist/vite-plugins/asset-imports.js +4 -14
  140. package/dist/vite-plugins/asset-imports.js.map +1 -1
  141. package/dist/vite-plugins/i18n.d.ts +5 -0
  142. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  143. package/dist/vite-plugins/i18n.js +88 -0
  144. package/dist/vite-plugins/i18n.js.map +1 -0
  145. package/dist/vite-plugins/index.d.ts +1 -1
  146. package/dist/vite-plugins/index.d.ts.map +1 -1
  147. package/dist/vite-plugins/index.js +5 -1
  148. package/dist/vite-plugins/index.js.map +1 -1
  149. package/dist/vite-plugins/resources.d.ts.map +1 -1
  150. package/dist/vite-plugins/resources.js +12 -57
  151. package/dist/vite-plugins/resources.js.map +1 -1
  152. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  153. package/dist/vite-plugins/self-contained.js +128 -135
  154. package/dist/vite-plugins/self-contained.js.map +1 -1
  155. package/dist/vite-plugins/settings-worker-env.js +2 -2
  156. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  157. package/dist/vite-plugins/settings.d.ts.map +1 -1
  158. package/dist/vite-plugins/settings.js +2 -1
  159. package/dist/vite-plugins/settings.js.map +1 -1
  160. package/package.json +11 -9
  161. package/src/builder.ts +3 -2
  162. package/src/cli.ts +5 -0
  163. package/src/codegen/freshness.ts +2 -1
  164. package/src/codegen/reference.ts +59 -0
  165. package/src/codegen/report.ts +18 -3
  166. package/src/codegen/run-codegen.ts +134 -31
  167. package/src/commands/codegen.ts +1 -1
  168. package/src/commands/docs.ts +10 -28
  169. package/src/commands/mcp.ts +81 -0
  170. package/src/deploy/freedesktop/localize.ts +144 -0
  171. package/src/deploy/install-hints.ts +5 -0
  172. package/src/deploy/notices/gtkx.ts +1 -1
  173. package/src/deploy/notices/libraries.ts +2 -1
  174. package/src/deploy/payload/launcher.ts +2 -0
  175. package/src/deploy/payload/stage.ts +35 -10
  176. package/src/deploy/run-deploy.ts +128 -13
  177. package/src/deploy/settings/libraries.ts +7 -10
  178. package/src/deploy/targets/flatpak-source.ts +33 -10
  179. package/src/deploy/tools.ts +36 -3
  180. package/src/dev/locale-dir.ts +22 -0
  181. package/src/dev/runner-main.ts +4 -1
  182. package/src/dev/runner.ts +8 -0
  183. package/src/dev/vite-dev-server.ts +1 -0
  184. package/src/i18n/binding-extractor.ts +1969 -0
  185. package/src/i18n/catalog-template.ts +24 -0
  186. package/src/i18n/catalogs.ts +216 -0
  187. package/src/i18n/extractor-plugin.ts +413 -0
  188. package/src/i18n/metadata-templates.ts +29 -0
  189. package/src/i18n/source-messages.ts +608 -0
  190. package/src/i18n/types.ts +154 -0
  191. package/src/internal/agent-rules.ts +135 -0
  192. package/src/internal/docs-elements.ts +21 -0
  193. package/src/internal/source-imports.ts +31 -60
  194. package/src/mcp/client-config.ts +130 -0
  195. package/src/mcp/client.ts +17 -44
  196. package/src/mcp/handlers.ts +7 -6
  197. package/src/settings/render.ts +16 -2
  198. package/src/settings/schema.ts +3 -1
  199. package/src/vite-plugins/animated.ts +486 -0
  200. package/src/vite-plugins/asset-imports.ts +5 -17
  201. package/src/vite-plugins/i18n.ts +146 -0
  202. package/src/vite-plugins/index.ts +5 -0
  203. package/src/vite-plugins/resources.ts +12 -74
  204. package/src/vite-plugins/self-contained.ts +150 -169
  205. package/src/vite-plugins/settings-worker-env.ts +2 -2
  206. package/src/vite-plugins/settings.ts +2 -1
@@ -3,6 +3,7 @@ import {
3
3
  invalidRequestError,
4
4
  methodNotFoundError,
5
5
  type ParamsSchema,
6
+ type Result,
6
7
  type ServerInitiatedMethod,
7
8
  type ServerRequestParams,
8
9
  ServerRequestParamsSchemas,
@@ -18,10 +19,10 @@ type HandlerContext = {
18
19
  registry: WidgetRegistry;
19
20
  };
20
21
 
21
- type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
22
+ type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<Result>;
22
23
  type WidgetTarget = { testing: TestingModule; widget: Gtk.Widget };
23
24
  type WidgetParams = { widgetId?: string | undefined };
24
- type TargetedHandler<Params> = (ctx: HandlerContext, target: WidgetTarget, params: Params) => Promise<unknown>;
25
+ type TargetedHandler<Params> = (ctx: HandlerContext, target: WidgetTarget, params: Params) => Promise<Result>;
25
26
  type QueryParams = ServerRequestParams<"widget.query">;
26
27
  type QueryBy = QueryParams["by"];
27
28
  type QueryRunner = (testing: TestingModule, app: Gtk.Application, params: QueryParams) => Promise<Gtk.Widget[]>;
@@ -110,7 +111,7 @@ const SEARCHED_BY: Record<QueryBy, string> = {
110
111
 
111
112
  function validated<Params>(
112
113
  schema: ParamsSchema<Params>,
113
- handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
114
+ handler: (ctx: HandlerContext, params: Params) => Promise<Result>,
114
115
  ): ValidatedHandler {
115
116
  return (ctx, params) => {
116
117
  const parsed = schema.safeParse(params ?? {});
@@ -229,7 +230,7 @@ const emptyQueryHint = (params: QueryParams): string =>
229
230
  "Call gtkx_get_widget_tree to see what is mounted; by:\"name\" is the widest match, and by:\"role\" accepts " +
230
231
  "options.name to match the accessible name of a known role.";
231
232
 
232
- async function handleQuery({ app, registry }: HandlerContext, params: QueryParams): Promise<unknown> {
233
+ async function handleQuery({ app, registry }: HandlerContext, params: QueryParams): Promise<Result> {
233
234
  const testing = await loadTestingModule();
234
235
  const widgets = await QUERY_RUNNERS[params.by](testing, app, params);
235
236
  const resolveId = (widget: Gtk.Widget): string => registry.getOrCreateId(widget);
@@ -254,7 +255,7 @@ const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
254
255
  async function handleScreenshot(
255
256
  { registry }: HandlerContext,
256
257
  params: ServerRequestParams<"widget.screenshot">,
257
- ): Promise<unknown> {
258
+ ): Promise<Result> {
258
259
  const testing = await loadTestingModule();
259
260
  const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
260
261
  const result = await testing.screenshot(target, { path: params.path });
@@ -269,7 +270,7 @@ async function handleScreenshot(
269
270
  const isServerInitiatedMethod = (method: string): method is ServerInitiatedMethod =>
270
271
  Object.hasOwn(ServerRequestParamsSchemas, method);
271
272
 
272
- const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
273
+ const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<Result> => {
273
274
  if (!isServerInitiatedMethod(method)) {
274
275
  throw methodNotFoundError(method);
275
276
  }
@@ -13,6 +13,10 @@ type EnvAssetDeclarations = {
13
13
  resources: string[];
14
14
  };
15
15
 
16
+ type EnvModuleOptions = {
17
+ references?: string[];
18
+ };
19
+
16
20
  const GTKX_ENV_MODULE_HEADER = `/**
17
21
  * Project import declarations generated by GTKX. Regenerated by
18
22
  * \`gtkx codegen\`, \`gtkx dev\`, and \`gtkx build\`; do not edit.
@@ -226,6 +230,7 @@ const renderBlockedAssetModule = (specifier: string): string => [
226
230
  const renderEnvModule = (
227
231
  files: ParsedSchemaFile[],
228
232
  assetDeclarations?: EnvAssetDeclarations,
233
+ options: EnvModuleOptions = {},
229
234
  ): string => {
230
235
  const assets = assetDeclarations ?? { blocked: [], icons: [], legacy: [], resources: [] };
231
236
  const usedNames: Set<string> = new Set();
@@ -238,8 +243,17 @@ const renderEnvModule = (
238
243
  const icons = assets.icons.map((specifier) => renderIconModule(specifier));
239
244
  const legacy = assets.legacy.map((specifier) => renderLegacyAssetModule(specifier));
240
245
  const resources = assets.resources.map((specifier) => renderResourceModule(specifier));
241
-
242
- return `${[GTKX_ENV_MODULE_HEADER, ...blocked, ...icons, ...legacy, ...resources, ...schemas].join("\n\n")}\n`;
246
+ const references = (options.references ?? []).map((path) => `/// <reference path=${JSON.stringify(path)} />`);
247
+
248
+ return `${[
249
+ GTKX_ENV_MODULE_HEADER,
250
+ ...references,
251
+ ...blocked,
252
+ ...icons,
253
+ ...legacy,
254
+ ...resources,
255
+ ...schemas,
256
+ ].join("\n\n")}\n`;
243
257
  };
244
258
 
245
259
  export { renderRuntimeModule, renderEnvModule };
@@ -13,6 +13,7 @@ import {
13
13
  } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
16
+ import { I18N_TYPES_FILENAME, i18nTypesPath } from "../i18n/types.js";
16
17
  import { DATA_IMPORT_PREFIX } from "../internal/data-dir.js";
17
18
  import { discoverSourceImports, type SourceImport } from "../internal/source-imports.js";
18
19
  import { removeTempDir } from "../internal/staging-dir.js";
@@ -323,7 +324,8 @@ const emitSchemaEnv = (
323
324
  assertUniqueSchemaBasenames(importedFiles);
324
325
  const imported = parseProjectSchemas(importedFiles, getRelativeModuleSpecifier);
325
326
  const assets = findAssetModuleSpecifiers(imports, isV2ResourceImports);
326
- const content = renderEnvModule([...legacy, ...imported], assets);
327
+ const references = existsSync(i18nTypesPath(rootDir)) ? [`./${I18N_TYPES_FILENAME}`] : [];
328
+ const content = renderEnvModule([...legacy, ...imported], assets, { references });
327
329
  const path = schemaEnvPath(rootDir);
328
330
  const isWritten = didWriteChanges(path, content);
329
331
 
@@ -0,0 +1,486 @@
1
+ import type { ConfigLoader } from "@gtkx/config";
2
+ import type { Plugin, UserConfig } from "vite";
3
+ import { type GeneratedElement, readGeneratedElements } from "@gtkx/codegen";
4
+ import { createConfigLoader } from "@gtkx/config/internal";
5
+ import { createRequire } from "node:module";
6
+ import { dirname, join } from "node:path";
7
+ import { parseSync } from "vite";
8
+ import { sourceLanguage } from "../internal/source-imports.js";
9
+ import { stripQuery } from "./strip-query.js";
10
+
11
+ type AstNode = {
12
+ type: string;
13
+ start: number;
14
+ end: number;
15
+ } & Record<string, unknown>;
16
+
17
+ type Edit = {
18
+ start: number;
19
+ end: number;
20
+ replacement: string;
21
+ };
22
+
23
+ type Rewrite = {
24
+ edits: Edit[];
25
+ hasMemberEdits: boolean;
26
+ hasCallEdits: boolean;
27
+ hasDynamicUses: boolean;
28
+ };
29
+
30
+ type RewrittenModule = { code: string; hasDynamicUses: boolean; hasMemberEdits: boolean };
31
+
32
+ type PluginState = {
33
+ isEnabled: boolean;
34
+ isBuild: boolean;
35
+ root: string;
36
+ elements: Map<string, GeneratedElement> | null;
37
+ };
38
+
39
+ const ANIMATED_PACKAGE = "@gtkx/animated";
40
+ const INTERNAL_SPECIFIER = "@gtkx/animated/internal";
41
+ const VIRTUAL_ID = "virtual:gtkx-animated";
42
+ const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_ID}`;
43
+ const NAMESPACE_HELPER = "__gtkxAnimated";
44
+ const CALL_HELPER = "__gtkxWithAnimated";
45
+
46
+ const BINDING_PARENT_KEYS: Record<string, string> = {
47
+ VariableDeclarator: "id",
48
+ ImportSpecifier: "local",
49
+ ImportDefaultSpecifier: "local",
50
+ ImportNamespaceSpecifier: "local",
51
+ FunctionDeclaration: "id",
52
+ ClassDeclaration: "id",
53
+ CatchClause: "param",
54
+ AssignmentPattern: "left",
55
+ RestElement: "argument",
56
+ Property: "value",
57
+ };
58
+
59
+ const BINDING_PARENT_ARRAY_KEYS: Record<string, string> = {
60
+ ArrowFunctionExpression: "params",
61
+ FunctionDeclaration: "params",
62
+ FunctionExpression: "params",
63
+ ArrayPattern: "elements",
64
+ };
65
+
66
+ const isAstNode = (value: unknown): value is AstNode =>
67
+ typeof value === "object" &&
68
+ value !== null &&
69
+ typeof (value as AstNode).type === "string" &&
70
+ typeof (value as AstNode).start === "number" &&
71
+ typeof (value as AstNode).end === "number";
72
+
73
+ const walkChildren = (node: AstNode, visit: (node: AstNode, parent: AstNode | null) => void): void => {
74
+ for (const [key, child] of Object.entries(node)) {
75
+ if (key !== "type") {
76
+ walk(child, node, visit);
77
+ }
78
+ }
79
+ };
80
+
81
+ const walk = (value: unknown, parent: AstNode | null, visit: (node: AstNode, parent: AstNode | null) => void): void => {
82
+ if (Array.isArray(value)) {
83
+ for (const item of value) {
84
+ walk(item, parent, visit);
85
+ }
86
+
87
+ return;
88
+ }
89
+
90
+ if (!isAstNode(value)) {
91
+ return;
92
+ }
93
+
94
+ visit(value, parent);
95
+ walkChildren(value, visit);
96
+ };
97
+
98
+ const identifierName = (node: unknown): string | undefined => {
99
+ if (!isAstNode(node)) {
100
+ return undefined;
101
+ }
102
+
103
+ if (node.type !== "Identifier" && node.type !== "JSXIdentifier") {
104
+ return undefined;
105
+ }
106
+
107
+ return typeof node.name === "string" ? node.name : undefined;
108
+ };
109
+
110
+ const isValueImportSpecifier = (specifier: unknown): specifier is AstNode =>
111
+ isAstNode(specifier) && specifier.type === "ImportSpecifier" && specifier.importKind !== "type";
112
+
113
+ const animatedSpecifierLocal = (node: AstNode): string | undefined => {
114
+ const specifiers = (node.specifiers as unknown[] | undefined) ?? [];
115
+ let local: string | undefined;
116
+
117
+ for (const specifier of specifiers) {
118
+ if (isValueImportSpecifier(specifier) && identifierName(specifier.imported) === "animated") {
119
+ local ??= identifierName(specifier.local);
120
+ }
121
+ }
122
+
123
+ return local;
124
+ };
125
+
126
+ const animatedLocalName = (program: AstNode): string | undefined => {
127
+ let local: string | undefined;
128
+
129
+ walk(program, null, (node) => {
130
+ if (node.type !== "ImportDeclaration" || (node.source as AstNode | undefined)?.value !== ANIMATED_PACKAGE) {
131
+ return;
132
+ }
133
+
134
+ if (node.importKind === "type") {
135
+ return;
136
+ }
137
+
138
+ local ??= animatedSpecifierLocal(node);
139
+ });
140
+
141
+ return local;
142
+ };
143
+
144
+ const isBindingSlot = (node: AstNode, parent: AstNode): boolean => {
145
+ const key = BINDING_PARENT_KEYS[parent.type];
146
+
147
+ return key !== undefined && parent[key] === node;
148
+ };
149
+
150
+ const isBindingListEntry = (node: AstNode, parent: AstNode): boolean => {
151
+ const arrayKey = BINDING_PARENT_ARRAY_KEYS[parent.type];
152
+ const entries = arrayKey === undefined ? undefined : parent[arrayKey];
153
+
154
+ return Array.isArray(entries) && entries.includes(node);
155
+ };
156
+
157
+ const isBindingParent = (node: AstNode, parent: AstNode): boolean =>
158
+ isBindingSlot(node, parent) || isBindingListEntry(node, parent);
159
+
160
+ const isBindingNamed = (node: AstNode, parent: AstNode, name: string): boolean =>
161
+ node.type === "Identifier" &&
162
+ node.name === name &&
163
+ isBindingParent(node, parent);
164
+
165
+ const countDeclarations = (program: AstNode, name: string): number => {
166
+ const spans: Set<number> = new Set();
167
+
168
+ walk(program, null, (node, parent) => {
169
+ if (parent !== null && isBindingNamed(node, parent, name)) {
170
+ spans.add(node.start);
171
+ }
172
+ });
173
+
174
+ return spans.size;
175
+ };
176
+
177
+ const isRewritableJsxMember = (node: AstNode, local: string): boolean =>
178
+ node.type === "JSXMemberExpression" && identifierName(node.object) === local;
179
+
180
+ const isRewritableObjectMember = (node: AstNode, local: string): boolean =>
181
+ node.type === "MemberExpression" &&
182
+ node.computed !== true &&
183
+ node.optional !== true &&
184
+ identifierName(node.object) === local;
185
+
186
+ const isForbiddenTarget = (node: AstNode, parent: AstNode | null): boolean =>
187
+ parent !== null &&
188
+ ((parent.type === "AssignmentExpression" && parent.left === node) ||
189
+ (parent.type === "UpdateExpression" && parent.argument === node) ||
190
+ (parent.type === "UnaryExpression" && parent.operator === "delete" && parent.argument === node));
191
+
192
+ const isRewritableMember = (node: AstNode, parent: AstNode | null, local: string): boolean => {
193
+ if (isRewritableJsxMember(node, local)) {
194
+ return true;
195
+ }
196
+
197
+ return isRewritableObjectMember(node, local) && !isForbiddenTarget(node, parent);
198
+ };
199
+
200
+ const collectMemberEdit = (
201
+ rewrite: Rewrite,
202
+ claimed: Set<number>,
203
+ node: AstNode,
204
+ elements: Map<string, GeneratedElement>,
205
+ ): void => {
206
+ const property = identifierName(node.property);
207
+
208
+ if (property !== undefined && elements.get(property)?.isMountable === true) {
209
+ const object = node.object as AstNode;
210
+ rewrite.edits.push({ start: object.start, end: object.end, replacement: NAMESPACE_HELPER });
211
+ rewrite.hasMemberEdits = true;
212
+ claimed.add(object.start);
213
+ }
214
+ };
215
+
216
+ const collectCallEdit = (rewrite: Rewrite, claimed: Set<number>, node: AstNode): void => {
217
+ const callee = node.callee as AstNode;
218
+ rewrite.edits.push({ start: callee.start, end: callee.end, replacement: CALL_HELPER });
219
+ rewrite.hasCallEdits = true;
220
+ claimed.add(callee.start);
221
+ };
222
+
223
+ const isPropertyReference = (node: AstNode, parent: AstNode | null): boolean =>
224
+ parent?.type === "ImportSpecifier" ||
225
+ (parent?.type === "MemberExpression" && parent.property === node) ||
226
+ (parent?.type === "JSXMemberExpression" && parent.property === node);
227
+
228
+ const hasDynamicUse = (program: AstNode, local: string, claimed: Set<number>): boolean => {
229
+ let isFound = false;
230
+
231
+ walk(program, null, (node, parent) => {
232
+ if (identifierName(node) === local && !claimed.has(node.start) && !isPropertyReference(node, parent)) {
233
+ isFound = true;
234
+ }
235
+ });
236
+
237
+ return isFound;
238
+ };
239
+
240
+ const collectRewrite = (program: AstNode, local: string, elements: Map<string, GeneratedElement>): Rewrite => {
241
+ const rewrite: Rewrite = { edits: [], hasMemberEdits: false, hasCallEdits: false, hasDynamicUses: false };
242
+ const claimed: Set<number> = new Set();
243
+
244
+ walk(program, null, (node, parent) => {
245
+ if (isRewritableMember(node, parent, local)) {
246
+ collectMemberEdit(rewrite, claimed, node, elements);
247
+
248
+ return;
249
+ }
250
+
251
+ if (node.type === "CallExpression" && identifierName(node.callee) === local) {
252
+ collectCallEdit(rewrite, claimed, node);
253
+ }
254
+ });
255
+
256
+ rewrite.hasDynamicUses = hasDynamicUse(program, local, claimed);
257
+
258
+ return rewrite;
259
+ };
260
+
261
+ const lastImportEnd = (program: AstNode): number => {
262
+ const statements = (program.body as unknown[] | undefined) ?? [];
263
+ let end = 0;
264
+
265
+ for (const statement of statements) {
266
+ if (isAstNode(statement) && statement.type === "ImportDeclaration") {
267
+ end = Math.max(end, statement.end);
268
+ }
269
+ }
270
+
271
+ return end;
272
+ };
273
+
274
+ const helperImports = (rewrite: Rewrite): string => {
275
+ const lines: string[] = [];
276
+
277
+ if (rewrite.hasMemberEdits) {
278
+ lines.push(`import * as ${NAMESPACE_HELPER} from ${JSON.stringify(VIRTUAL_ID)};`);
279
+ }
280
+
281
+ if (rewrite.hasCallEdits) {
282
+ lines.push(`import { withAnimated as ${CALL_HELPER} } from ${JSON.stringify(INTERNAL_SPECIFIER)};`);
283
+ }
284
+
285
+ return lines.join("\n");
286
+ };
287
+
288
+ const applyEdits = (code: string, edits: Edit[]): string => {
289
+ const ordered = edits.toSorted((a, b) => b.start - a.start);
290
+ let result = code;
291
+
292
+ for (const edit of ordered) {
293
+ result = `${result.slice(0, edit.start)}${edit.replacement}${result.slice(edit.end)}`;
294
+ }
295
+
296
+ return result;
297
+ };
298
+
299
+ const parseProgram = (path: string, code: string): AstNode | undefined => {
300
+ const lang = sourceLanguage(path);
301
+
302
+ if (lang === undefined) {
303
+ return undefined;
304
+ }
305
+
306
+ const parsed = parseSync(path, code, { lang });
307
+
308
+ return parsed.errors.length > 0 ? undefined : (parsed.program as unknown as AstNode);
309
+ };
310
+
311
+ const unchangedRewrite = (code: string, rewrite: Rewrite): RewrittenModule | undefined =>
312
+ rewrite.hasDynamicUses ? { code, hasDynamicUses: true, hasMemberEdits: false } : undefined;
313
+
314
+ const hasHelperCollision = (code: string): boolean =>
315
+ code.includes(NAMESPACE_HELPER) || code.includes(CALL_HELPER);
316
+
317
+ const rewriteModule = (
318
+ code: string,
319
+ id: string,
320
+ elements: Map<string, GeneratedElement>,
321
+ ): RewrittenModule | undefined => {
322
+ if (hasHelperCollision(code)) {
323
+ return undefined;
324
+ }
325
+
326
+ const program = parseProgram(stripQuery(id), code);
327
+
328
+ if (program === undefined) {
329
+ return undefined;
330
+ }
331
+
332
+ const local = animatedLocalName(program);
333
+
334
+ if (local === undefined || countDeclarations(program, local) > 1) {
335
+ return undefined;
336
+ }
337
+
338
+ const rewrite = collectRewrite(program, local, elements);
339
+
340
+ if (rewrite.edits.length === 0) {
341
+ return unchangedRewrite(code, rewrite);
342
+ }
343
+
344
+ const insertAt = lastImportEnd(program);
345
+ const helperEdit: Edit = { start: insertAt, end: insertAt, replacement: `\n${helperImports(rewrite)}` };
346
+ const withHelpers = applyEdits(code, [...rewrite.edits, helperEdit]);
347
+
348
+ return { code: withHelpers, hasDynamicUses: rewrite.hasDynamicUses, hasMemberEdits: rewrite.hasMemberEdits };
349
+ };
350
+
351
+ const renderVirtualModule = (elements: Map<string, GeneratedElement>): string => {
352
+ const mountable = elements.values().filter((element) => element.isMountable).toArray();
353
+ const imports = mountable.map((element) => `${element.glibName} as $${element.glibName}`);
354
+
355
+ const consts = mountable.map(
356
+ (element) => `export const ${element.glibName} = /* @__PURE__ */ withAnimated($${element.glibName});`,
357
+ );
358
+
359
+ return (
360
+ `import { ${imports.join(", ")} } from "@gtkx/jsx";\n` +
361
+ `import { withAnimated } from ${JSON.stringify(INTERNAL_SPECIFIER)};\n` +
362
+ `${consts.join("\n")}\n`
363
+ );
364
+ };
365
+
366
+ const loadElements = (state: PluginState): Map<string, GeneratedElement> | null => {
367
+ if (state.elements !== null) {
368
+ return state.elements;
369
+ }
370
+
371
+ try {
372
+ const requireFromRoot = createRequire(join(state.root, "package.json"));
373
+ const jsxStoreDir = dirname(requireFromRoot.resolve("@gtkx/jsx/package.json"));
374
+ state.elements = new Map(readGeneratedElements(jsxStoreDir).map((element) => [element.glibName, element]));
375
+ } catch {
376
+ return null;
377
+ }
378
+
379
+ return state.elements;
380
+ };
381
+
382
+ const resolveVirtualId = (state: PluginState, source: string): string | undefined => {
383
+ if (source === VIRTUAL_ID && state.isEnabled) {
384
+ return RESOLVED_VIRTUAL_ID;
385
+ }
386
+
387
+ return undefined;
388
+ };
389
+
390
+ const loadVirtualModule = (state: PluginState, id: string): string | undefined => {
391
+ if (id !== RESOLVED_VIRTUAL_ID || !state.isEnabled) {
392
+ return undefined;
393
+ }
394
+
395
+ const elements = loadElements(state);
396
+
397
+ return elements === null ? undefined : renderVirtualModule(elements);
398
+ };
399
+
400
+ const dynamicUseWarning = (id: string, hasDynamicUses: boolean): string[] =>
401
+ hasDynamicUses
402
+ ? [
403
+ `${stripQuery(id)} uses the animated binding dynamically, which keeps the whole generated ` +
404
+ "widget namespace in the bundle; use animated(Component) calls to let unused widgets be dropped",
405
+ ]
406
+ : [];
407
+
408
+ const memberUseWarning = (id: string, hasMemberEdits: boolean): string[] =>
409
+ hasMemberEdits
410
+ ? [
411
+ `${stripQuery(id)} reads widget components off the animated binding, which GTKX 2.0 removes; ` +
412
+ "import the component and call animated(Component) instead",
413
+ ]
414
+ : [];
415
+
416
+ const warningsFor = (state: PluginState, id: string, rewritten: RewrittenModule): string[] =>
417
+ state.isBuild
418
+ ? [
419
+ ...dynamicUseWarning(id, rewritten.hasDynamicUses),
420
+ ...memberUseWarning(id, rewritten.hasMemberEdits),
421
+ ]
422
+ : [];
423
+
424
+ const transformCode = (
425
+ state: PluginState,
426
+ code: string,
427
+ id: string,
428
+ ): { code: string; warnings: string[] } | undefined => {
429
+ if (!state.isEnabled || id.startsWith("\0") || !code.includes(ANIMATED_PACKAGE)) {
430
+ return undefined;
431
+ }
432
+
433
+ const elements = loadElements(state);
434
+
435
+ if (elements === null) {
436
+ return undefined;
437
+ }
438
+
439
+ const rewritten = rewriteModule(code, id, elements);
440
+
441
+ if (rewritten === undefined) {
442
+ return undefined;
443
+ }
444
+
445
+ return { code: rewritten.code, warnings: warningsFor(state, id, rewritten) };
446
+ };
447
+
448
+ function gtkxAnimated(loadConfig: ConfigLoader = createConfigLoader()): Plugin {
449
+ const state: PluginState = { isEnabled: false, isBuild: false, root: process.cwd(), elements: null };
450
+
451
+ return {
452
+ name: "gtkx:animated",
453
+ enforce: "pre",
454
+
455
+ async config(config: UserConfig) {
456
+ const loaded = await loadConfig.load(config.root ?? process.cwd());
457
+ state.isEnabled = loaded.config.future?.v2TreeShaking === true;
458
+ },
459
+
460
+ configResolved(config) {
461
+ state.root = config.root;
462
+ state.isBuild = config.command === "build";
463
+ },
464
+
465
+ resolveId(source) {
466
+ return resolveVirtualId(state, source);
467
+ },
468
+
469
+ load(id) {
470
+ return loadVirtualModule(state, id);
471
+ },
472
+
473
+ transform(code, id) {
474
+ const result = transformCode(state, code, id);
475
+ const warnings = result?.warnings ?? [];
476
+
477
+ for (const warning of warnings) {
478
+ this.warn(warning);
479
+ }
480
+
481
+ return result?.code;
482
+ },
483
+ };
484
+ }
485
+
486
+ export { gtkxAnimated };
@@ -1,8 +1,8 @@
1
1
  import type { ConfigLoader } from "@gtkx/config";
2
- import type { ParseResult, ParserOptions, Plugin, UserConfig } from "vite";
3
- import { createConfigLoader } from "@gtkx/config/internal";
4
- import { extname } from "node:path";
2
+ import type { ParseResult, Plugin, UserConfig } from "vite";
3
+ import { createConfigLoader, resolveFuture } from "@gtkx/config/internal";
5
4
  import { parseSync } from "vite";
5
+ import { sourceLanguage } from "../internal/source-imports.js";
6
6
  import { ASSET_MENTION_RE } from "./asset-extensions.js";
7
7
  import {
8
8
  DATA_PREFIX,
@@ -15,7 +15,6 @@ import {
15
15
  import { RESOURCE_PATH_EXPORT } from "./resource-shared.js";
16
16
  import { stripQuery } from "./strip-query.js";
17
17
 
18
- type SourceLanguage = NonNullable<ParserOptions["lang"]>;
19
18
  type ParsedModule = ParseResult["module"];
20
19
  type ImportStatement = ParsedModule["staticImports"][number];
21
20
  type ExportStatement = ParsedModule["staticExports"][number];
@@ -24,17 +23,6 @@ type BindingEntry = { importName: { name: string | null }; isType: boolean };
24
23
  type NamedBinding = { name: string; source: string };
25
24
  type PluginState = { isV2: boolean };
26
25
 
27
- const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
28
- [".cjs", "jsx"],
29
- [".cts", "ts"],
30
- [".js", "jsx"],
31
- [".jsx", "jsx"],
32
- [".mjs", "jsx"],
33
- [".mts", "ts"],
34
- [".ts", "ts"],
35
- [".tsx", "tsx"],
36
- ]);
37
-
38
26
  const NODE_MODULES = /(?:^|\/)node_modules\//;
39
27
  const VIRTUAL_PREFIX = "\0";
40
28
  const DEFAULT_BINDING = "default";
@@ -135,7 +123,7 @@ const hasCheckedAssetMention = (code: string): boolean => ASSET_MENTION_RE.test(
135
123
 
136
124
  const checkAssetImports = (code: string, id: string, isV2: boolean): void => {
137
125
  const path = stripQuery(id);
138
- const lang = SOURCE_LANGUAGES.get(extname(path).toLowerCase());
126
+ const lang = sourceLanguage(path);
139
127
 
140
128
  if (lang === undefined || !isCheckedSource(path) || !hasCheckedAssetMention(code)) {
141
129
  return;
@@ -163,7 +151,7 @@ function gtkxAssetImports(loadConfig: ConfigLoader = createConfigLoader()): Plug
163
151
 
164
152
  async config(config: UserConfig) {
165
153
  const loaded = await loadConfig.load(config.root ?? process.cwd());
166
- state.isV2 = loaded.config.future?.v2ResourceImports === true;
154
+ state.isV2 = resolveFuture(loaded.config.future).isResourceImported;
167
155
  },
168
156
 
169
157
  transform(code, id) {