@gtkx/cli 1.3.0 → 1.4.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 (217) hide show
  1. package/README.md +5 -4
  2. package/dist/builder.d.ts +0 -1
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +6 -6
  5. package/dist/builder.js.map +1 -1
  6. package/dist/codegen/run-codegen.d.ts +1 -2
  7. package/dist/codegen/run-codegen.d.ts.map +1 -1
  8. package/dist/codegen/run-codegen.js +11 -6
  9. package/dist/codegen/run-codegen.js.map +1 -1
  10. package/dist/commands/build.d.ts +0 -4
  11. package/dist/commands/build.d.ts.map +1 -1
  12. package/dist/commands/build.js +0 -5
  13. package/dist/commands/build.js.map +1 -1
  14. package/dist/commands/codegen.d.ts.map +1 -1
  15. package/dist/commands/codegen.js +1 -3
  16. package/dist/commands/codegen.js.map +1 -1
  17. package/dist/deploy/depends.d.ts +2 -2
  18. package/dist/deploy/depends.d.ts.map +1 -1
  19. package/dist/deploy/depends.js +30 -15
  20. package/dist/deploy/depends.js.map +1 -1
  21. package/dist/deploy/nfpm/config.js +1 -1
  22. package/dist/deploy/nfpm/config.js.map +1 -1
  23. package/dist/deploy/node-runtime/elf.d.ts +1 -1
  24. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  25. package/dist/deploy/node-runtime/elf.js +2 -2
  26. package/dist/deploy/node-runtime/elf.js.map +1 -1
  27. package/dist/deploy/node-runtime/index.js +2 -2
  28. package/dist/deploy/node-runtime/index.js.map +1 -1
  29. package/dist/deploy/notices/collect.d.ts +3 -1
  30. package/dist/deploy/notices/collect.d.ts.map +1 -1
  31. package/dist/deploy/notices/collect.js +2 -2
  32. package/dist/deploy/notices/collect.js.map +1 -1
  33. package/dist/deploy/notices/packages.d.ts +2 -1
  34. package/dist/deploy/notices/packages.d.ts.map +1 -1
  35. package/dist/deploy/notices/packages.js +4 -27
  36. package/dist/deploy/notices/packages.js.map +1 -1
  37. package/dist/deploy/payload/icons.d.ts +7 -3
  38. package/dist/deploy/payload/icons.d.ts.map +1 -1
  39. package/dist/deploy/payload/icons.js +46 -28
  40. package/dist/deploy/payload/icons.js.map +1 -1
  41. package/dist/deploy/payload/stage.d.ts.map +1 -1
  42. package/dist/deploy/payload/stage.js +3 -2
  43. package/dist/deploy/payload/stage.js.map +1 -1
  44. package/dist/deploy/run-deploy.d.ts.map +1 -1
  45. package/dist/deploy/run-deploy.js +63 -11
  46. package/dist/deploy/run-deploy.js.map +1 -1
  47. package/dist/deploy/settings/build-manifest.d.ts +9 -0
  48. package/dist/deploy/settings/build-manifest.d.ts.map +1 -0
  49. package/dist/deploy/settings/build-manifest.js +80 -0
  50. package/dist/deploy/settings/build-manifest.js.map +1 -0
  51. package/dist/deploy/settings/index.d.ts.map +1 -1
  52. package/dist/deploy/settings/index.js +9 -3
  53. package/dist/deploy/settings/index.js.map +1 -1
  54. package/dist/deploy/settings/libraries.d.ts +8 -0
  55. package/dist/deploy/settings/libraries.d.ts.map +1 -0
  56. package/dist/deploy/settings/libraries.js +39 -0
  57. package/dist/deploy/settings/libraries.js.map +1 -0
  58. package/dist/deploy/settings/paths.d.ts +3 -0
  59. package/dist/deploy/settings/paths.d.ts.map +1 -1
  60. package/dist/deploy/settings/paths.js +6 -32
  61. package/dist/deploy/settings/paths.js.map +1 -1
  62. package/dist/deploy/targets/appimage.d.ts.map +1 -1
  63. package/dist/deploy/targets/appimage.js +19 -13
  64. package/dist/deploy/targets/appimage.js.map +1 -1
  65. package/dist/deploy/targets/flatpak-manifest.d.ts +2 -1
  66. package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
  67. package/dist/deploy/targets/flatpak-manifest.js +5 -1
  68. package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
  69. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  70. package/dist/deploy/targets/flatpak-source.js +9 -6
  71. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  72. package/dist/deploy/types.d.ts +4 -4
  73. package/dist/deploy/types.d.ts.map +1 -1
  74. package/dist/deploy/types.js.map +1 -1
  75. package/dist/dev/icon-dir.d.ts +1 -1
  76. package/dist/dev/icon-dir.d.ts.map +1 -1
  77. package/dist/dev/icon-dir.js +20 -7
  78. package/dist/dev/icon-dir.js.map +1 -1
  79. package/dist/dev/runner-deps.d.ts.map +1 -1
  80. package/dist/dev/runner-deps.js +5 -1
  81. package/dist/dev/runner-deps.js.map +1 -1
  82. package/dist/dev/runner-main.js +4 -4
  83. package/dist/dev/runner-main.js.map +1 -1
  84. package/dist/dev/runner.d.ts +1 -1
  85. package/dist/dev/runner.d.ts.map +1 -1
  86. package/dist/dev/runner.js +1 -1
  87. package/dist/dev/runner.js.map +1 -1
  88. package/dist/dev/schema-dir.d.ts +1 -1
  89. package/dist/dev/schema-dir.d.ts.map +1 -1
  90. package/dist/dev/schema-dir.js +3 -1
  91. package/dist/dev/schema-dir.js.map +1 -1
  92. package/dist/internal/build-manifest.d.ts +20 -0
  93. package/dist/internal/build-manifest.d.ts.map +1 -0
  94. package/dist/internal/build-manifest.js +6 -0
  95. package/dist/internal/build-manifest.js.map +1 -0
  96. package/dist/internal/icon-path.d.ts +13 -0
  97. package/dist/internal/icon-path.d.ts.map +1 -0
  98. package/dist/internal/icon-path.js +86 -0
  99. package/dist/internal/icon-path.js.map +1 -0
  100. package/dist/internal/source-imports.d.ts +8 -0
  101. package/dist/internal/source-imports.d.ts.map +1 -0
  102. package/dist/internal/source-imports.js +144 -0
  103. package/dist/internal/source-imports.js.map +1 -0
  104. package/dist/mcp/widget-properties.d.ts.map +1 -1
  105. package/dist/mcp/widget-properties.js +3 -12
  106. package/dist/mcp/widget-properties.js.map +1 -1
  107. package/dist/settings/render.d.ts +7 -1
  108. package/dist/settings/render.d.ts.map +1 -1
  109. package/dist/settings/render.js +69 -20
  110. package/dist/settings/render.js.map +1 -1
  111. package/dist/settings/schema.d.ts +4 -3
  112. package/dist/settings/schema.d.ts.map +1 -1
  113. package/dist/settings/schema.js +122 -15
  114. package/dist/settings/schema.js.map +1 -1
  115. package/dist/vite-plugins/asset-emitter.d.ts +1 -1
  116. package/dist/vite-plugins/asset-emitter.d.ts.map +1 -1
  117. package/dist/vite-plugins/asset-emitter.js.map +1 -1
  118. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  119. package/dist/vite-plugins/asset-extensions.js +1 -0
  120. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  121. package/dist/vite-plugins/asset-imports.d.ts +2 -1
  122. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  123. package/dist/vite-plugins/asset-imports.js +37 -16
  124. package/dist/vite-plugins/asset-imports.js.map +1 -1
  125. package/dist/vite-plugins/asset-specifier.d.ts +13 -1
  126. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  127. package/dist/vite-plugins/asset-specifier.js +39 -1
  128. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  129. package/dist/vite-plugins/build-manifest.d.ts +5 -0
  130. package/dist/vite-plugins/build-manifest.d.ts.map +1 -0
  131. package/dist/vite-plugins/{bundled-packages.js → build-manifest.js} +20 -14
  132. package/dist/vite-plugins/build-manifest.js.map +1 -0
  133. package/dist/vite-plugins/built-url.d.ts +1 -1
  134. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  135. package/dist/vite-plugins/built-url.js +37 -24
  136. package/dist/vite-plugins/built-url.js.map +1 -1
  137. package/dist/vite-plugins/icons.d.ts +2 -1
  138. package/dist/vite-plugins/icons.d.ts.map +1 -1
  139. package/dist/vite-plugins/icons.js +25 -12
  140. package/dist/vite-plugins/icons.js.map +1 -1
  141. package/dist/vite-plugins/index.d.ts +2 -1
  142. package/dist/vite-plugins/index.d.ts.map +1 -1
  143. package/dist/vite-plugins/index.js +7 -5
  144. package/dist/vite-plugins/index.js.map +1 -1
  145. package/dist/vite-plugins/native.d.ts.map +1 -1
  146. package/dist/vite-plugins/native.js +23 -4
  147. package/dist/vite-plugins/native.js.map +1 -1
  148. package/dist/vite-plugins/resource-init-module.d.ts +7 -3
  149. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  150. package/dist/vite-plugins/resource-init-module.js +39 -14
  151. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  152. package/dist/vite-plugins/resource-shared.d.ts +3 -1
  153. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  154. package/dist/vite-plugins/resource-shared.js +3 -1
  155. package/dist/vite-plugins/resource-shared.js.map +1 -1
  156. package/dist/vite-plugins/resources.d.ts +1 -1
  157. package/dist/vite-plugins/resources.d.ts.map +1 -1
  158. package/dist/vite-plugins/resources.js +922 -86
  159. package/dist/vite-plugins/resources.js.map +1 -1
  160. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  161. package/dist/vite-plugins/settings-worker-env.js +6 -3
  162. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  163. package/dist/vite-plugins/settings.d.ts +3 -1
  164. package/dist/vite-plugins/settings.d.ts.map +1 -1
  165. package/dist/vite-plugins/settings.js +63 -23
  166. package/dist/vite-plugins/settings.js.map +1 -1
  167. package/env.d.ts +11 -192
  168. package/package.json +10 -10
  169. package/src/builder.ts +6 -7
  170. package/src/codegen/run-codegen.ts +10 -7
  171. package/src/commands/build.ts +0 -5
  172. package/src/commands/codegen.ts +1 -3
  173. package/src/deploy/depends.ts +47 -18
  174. package/src/deploy/nfpm/config.ts +1 -1
  175. package/src/deploy/node-runtime/elf.ts +3 -3
  176. package/src/deploy/node-runtime/index.ts +2 -2
  177. package/src/deploy/notices/collect.ts +4 -2
  178. package/src/deploy/notices/packages.ts +5 -43
  179. package/src/deploy/payload/icons.ts +64 -34
  180. package/src/deploy/payload/stage.ts +5 -2
  181. package/src/deploy/run-deploy.ts +87 -12
  182. package/src/deploy/settings/build-manifest.ts +122 -0
  183. package/src/deploy/settings/index.ts +9 -4
  184. package/src/deploy/settings/libraries.ts +57 -0
  185. package/src/deploy/settings/paths.ts +10 -47
  186. package/src/deploy/targets/appimage.ts +32 -15
  187. package/src/deploy/targets/flatpak-manifest.ts +7 -1
  188. package/src/deploy/targets/flatpak-source.ts +9 -10
  189. package/src/deploy/types.ts +4 -4
  190. package/src/dev/icon-dir.ts +31 -7
  191. package/src/dev/runner-deps.ts +5 -1
  192. package/src/dev/runner-main.ts +4 -4
  193. package/src/dev/runner.ts +2 -2
  194. package/src/dev/schema-dir.ts +3 -1
  195. package/src/internal/build-manifest.ts +32 -0
  196. package/src/internal/icon-path.ts +127 -0
  197. package/src/internal/source-imports.ts +200 -0
  198. package/src/mcp/widget-properties.ts +3 -14
  199. package/src/settings/render.ts +99 -21
  200. package/src/settings/schema.ts +190 -17
  201. package/src/vite-plugins/asset-emitter.ts +1 -1
  202. package/src/vite-plugins/asset-extensions.ts +1 -0
  203. package/src/vite-plugins/asset-imports.ts +60 -19
  204. package/src/vite-plugins/asset-specifier.ts +73 -1
  205. package/src/vite-plugins/{bundled-packages.ts → build-manifest.ts} +30 -19
  206. package/src/vite-plugins/built-url.ts +49 -28
  207. package/src/vite-plugins/icons.ts +33 -14
  208. package/src/vite-plugins/index.ts +12 -5
  209. package/src/vite-plugins/native.ts +31 -4
  210. package/src/vite-plugins/resource-init-module.ts +44 -15
  211. package/src/vite-plugins/resource-shared.ts +4 -0
  212. package/src/vite-plugins/resources.ts +1459 -100
  213. package/src/vite-plugins/settings-worker-env.ts +7 -3
  214. package/src/vite-plugins/settings.ts +89 -21
  215. package/dist/vite-plugins/bundled-packages.d.ts +0 -5
  216. package/dist/vite-plugins/bundled-packages.d.ts.map +0 -1
  217. package/dist/vite-plugins/bundled-packages.js.map +0 -1
@@ -6,13 +6,14 @@ import {
6
6
  getHandle,
7
7
  getInstanceType,
8
8
  type Handle,
9
+ isReadableProperty,
9
10
  TYPE_ENUM,
10
11
  TYPE_FLAGS,
11
12
  typeIsA,
12
13
  typeName,
13
14
  } from "@gtkx/runtime";
14
15
  import { fromValue, getValueType } from "@gtkx/runtime/internal";
15
- import { camelCase, errorMessage, kebabCase } from "@gtkx/utils";
16
+ import { errorMessage, kebabCase } from "@gtkx/utils";
16
17
  import type { WidgetRegistry } from "./widget-registry.js";
17
18
 
18
19
  type PropertyRepresentation = Omit<SerializedProperty, "type">;
@@ -39,18 +40,6 @@ function prototypesFor(widget: Gtk.Widget): object[] {
39
40
  return prototypes;
40
41
  }
41
42
 
42
- function getAccessor(widget: Gtk.Widget, member: string): PropertyDescriptor | undefined {
43
- for (const prototype of prototypesFor(widget)) {
44
- const descriptor = Object.getOwnPropertyDescriptor(prototype, member);
45
-
46
- if (descriptor !== undefined) {
47
- return descriptor;
48
- }
49
- }
50
-
51
- return undefined;
52
- }
53
-
54
43
  function getReadableNames(prototype: object): string[] {
55
44
  const descriptors = Object.entries(Object.getOwnPropertyDescriptors(prototype));
56
45
 
@@ -64,7 +53,7 @@ function readablePropertyNames(widget: Gtk.Widget): string[] {
64
53
  }
65
54
 
66
55
  const requireReadableProperty = (widget: Gtk.Widget, property: string): void => {
67
- if (getAccessor(widget, camelCase(property))?.get !== undefined) {
56
+ if (isReadableProperty(getInstanceType(widget), property)) {
68
57
  return;
69
58
  }
70
59
 
@@ -1,19 +1,56 @@
1
- import { upperFirst } from "@gtkx/utils";
1
+ import { camelCase, sanitizeIdentifier, upperFirst } from "@gtkx/utils";
2
2
  import type { ParsedKey, ParsedSchema, ParsedSchemaFile } from "./parser.js";
3
3
 
4
+ type NamedSchema = {
5
+ schema: ParsedSchema;
6
+ exportName: string;
7
+ };
8
+
9
+ type EnvAssetDeclarations = {
10
+ blocked: string[];
11
+ icons: string[];
12
+ legacy: string[];
13
+ resources: string[];
14
+ };
15
+
4
16
  const GTKX_ENV_MODULE_HEADER = `/**
5
- * GSettings schema modules generated by GTKX from the project's
6
- * \`.gschema.xml\` files. Regenerated by \`gtkx codegen\`, \`gtkx dev\`, and
7
- * \`gtkx build\`; do not edit.
17
+ * Project import declarations generated by GTKX. Regenerated by
18
+ * \`gtkx codegen\`, \`gtkx dev\`, and \`gtkx build\`; do not edit.
8
19
  */`;
9
20
 
10
21
  const ENUM_KIND = "enum";
11
22
  const FLAGS_KIND = "flags";
23
+ const ID_SEPARATOR = /[._]/;
24
+ const NAME_SEPARATOR = "_";
12
25
 
13
26
  const toJsStringLiteral = (value: string): string =>
14
27
  JSON.stringify(value).replaceAll("\u{2028}", String.raw`\u2028`).replaceAll("\u{2029}", String.raw`\u2029`);
15
28
 
16
- const getExportName = (schemaId: string): string => schemaId.replaceAll(".", "_");
29
+ const getExportName = (schemaId: string): string =>
30
+ sanitizeIdentifier(schemaId.split(ID_SEPARATOR).map((part) => camelCase(part)).join(NAME_SEPARATOR));
31
+
32
+ const collisionError = (file: ParsedSchemaFile, taken: string, schemaId: string, name: string): Error =>
33
+ new Error(
34
+ `${file.fileName} declares ${taken} and ${schemaId}, which both export \`${name}\`. ` +
35
+ "Rename one of the schema ids so that every schema in the file gets an export of its own.",
36
+ );
37
+
38
+ const getNamedSchemas = (file: ParsedSchemaFile): NamedSchema[] => {
39
+ const owners: Map<string, string> = new Map();
40
+
41
+ return file.schemas.map((schema) => {
42
+ const exportName = getExportName(schema.id);
43
+ const taken = owners.get(exportName);
44
+
45
+ if (taken !== undefined) {
46
+ throw collisionError(file, taken, schema.id, exportName);
47
+ }
48
+
49
+ owners.set(exportName, schema.id);
50
+
51
+ return { schema, exportName };
52
+ });
53
+ };
17
54
 
18
55
  const runtimeKindForKey = (key: ParsedKey): string => {
19
56
  if (key.enumId !== null) {
@@ -37,26 +74,27 @@ const getRuntimeKeys = (schema: ParsedSchema): string => {
37
74
 
38
75
  const renderRuntimeModule = (file: ParsedSchemaFile): string => {
39
76
  const lines: string[] = [];
77
+ const named = getNamedSchemas(file);
40
78
 
41
- for (const [index, schema] of file.schemas.entries()) {
79
+ for (const [index, { schema, exportName }] of named.entries()) {
42
80
  const keysName = `keys_${String(index)}`;
43
81
  const id = toJsStringLiteral(schema.id);
44
82
  lines.push(`const ${keysName} = ${getRuntimeKeys(schema)};`);
45
83
 
46
84
  if (schema.path === null) {
47
85
  lines.push(
48
- `export const ${getExportName(schema.id)} = { id: ${id}, keys: ${keysName}, ` +
86
+ `export const ${exportName} = { id: ${id}, keys: ${keysName}, ` +
49
87
  `at: (path) => ({ id: ${id}, path, keys: ${keysName} }) };`,
50
88
  );
51
89
  } else {
52
- lines.push(`export const ${getExportName(schema.id)} = { id: ${id}, path: null, keys: ${keysName} };`);
90
+ lines.push(`export const ${exportName} = { id: ${id}, path: null, keys: ${keysName} };`);
53
91
  }
54
92
  }
55
93
 
56
- const first = file.schemas[0];
94
+ const first = named[0];
57
95
 
58
96
  if (first !== undefined) {
59
- lines.push(`export default ${getExportName(first.id)};`);
97
+ lines.push(`export default ${first.exportName};`);
60
98
  }
61
99
 
62
100
  return lines.join("\n");
@@ -106,9 +144,7 @@ const boundRefTypeLines = (interfaceName: string, indent: string): string[] => [
106
144
  `${indent}keys: ${interfaceName};`,
107
145
  ];
108
146
 
109
- const renderSchemaConst = (schema: ParsedSchema, interfaceName: string): string[] => {
110
- const exportName = getExportName(schema.id);
111
-
147
+ const renderSchemaConst = ({ schema, exportName }: NamedSchema, interfaceName: string): string[] => {
112
148
  if (schema.path !== null) {
113
149
  return [
114
150
  ` const ${exportName}: {`,
@@ -130,17 +166,17 @@ const renderSchemaConst = (schema: ParsedSchema, interfaceName: string): string[
130
166
  };
131
167
 
132
168
  const renderFileModule = (file: ParsedSchemaFile, usedNames: Set<string>): string[] => {
133
- const lines = [`declare module "${file.fileName}" {`];
169
+ const lines = [`declare module ${toJsStringLiteral(file.fileName)} {`];
134
170
  const exportNames: string[] = [];
135
171
 
136
- for (const [index, schema] of file.schemas.entries()) {
172
+ for (const [index, named] of getNamedSchemas(file).entries()) {
137
173
  if (index > 0) {
138
174
  lines.push("");
139
175
  }
140
176
 
141
- const interfaceName = allocateInterfaceName(schema.id, usedNames);
142
- lines.push(...renderKeysType(interfaceName, schema), "", ...renderSchemaConst(schema, interfaceName));
143
- exportNames.push(getExportName(schema.id));
177
+ const interfaceName = allocateInterfaceName(named.schema.id, usedNames);
178
+ lines.push(...renderKeysType(interfaceName, named.schema), "", ...renderSchemaConst(named, interfaceName));
179
+ exportNames.push(named.exportName);
144
180
  }
145
181
 
146
182
  const [firstExport] = exportNames;
@@ -154,14 +190,56 @@ const renderFileModule = (file: ParsedSchemaFile, usedNames: Set<string>): strin
154
190
  return lines;
155
191
  };
156
192
 
157
- const renderEnvModule = (files: ParsedSchemaFile[]): string => {
193
+ const renderResourceModule = (specifier: string): string => [
194
+ `declare module ${toJsStringLiteral(specifier)} {`,
195
+ " const path: string;",
196
+ " export { path };",
197
+ " export default path;",
198
+ "}",
199
+ ].join("\n");
200
+
201
+ const renderIconModule = (specifier: string): string => [
202
+ `declare module ${toJsStringLiteral(specifier)} {`,
203
+ " const iconName: string;",
204
+ " export default iconName;",
205
+ "}",
206
+ ].join("\n");
207
+
208
+ const renderLegacyAssetModule = (specifier: string): string => [
209
+ `declare module ${toJsStringLiteral(specifier)} {`,
210
+ " const resourceUri: string;",
211
+ " const path: string;",
212
+ " export { path };",
213
+ " export default resourceUri;",
214
+ "}",
215
+ ].join("\n");
216
+
217
+ const renderBlockedAssetModule = (specifier: string): string => [
218
+ `declare module ${toJsStringLiteral(specifier)} {`,
219
+ " const assetImportRequiresResourceOrUrlQuery: {",
220
+ " readonly __gtkxAssetImportRequiresResourceOrUrlQuery: unique symbol;",
221
+ " };",
222
+ " export default assetImportRequiresResourceOrUrlQuery;",
223
+ "}",
224
+ ].join("\n");
225
+
226
+ const renderEnvModule = (
227
+ files: ParsedSchemaFile[],
228
+ assetDeclarations?: EnvAssetDeclarations,
229
+ ): string => {
230
+ const assets = assetDeclarations ?? { blocked: [], icons: [], legacy: [], resources: [] };
158
231
  const usedNames: Set<string> = new Set();
159
232
 
160
- const blocks = files
233
+ const schemas = files
161
234
  .filter((file) => file.schemas.length > 0)
162
235
  .map((file) => renderFileModule(file, usedNames).join("\n"));
163
236
 
164
- return `${[GTKX_ENV_MODULE_HEADER, ...blocks].join("\n\n")}\n`;
237
+ const blocked = assets.blocked.map((specifier) => renderBlockedAssetModule(specifier));
238
+ const icons = assets.icons.map((specifier) => renderIconModule(specifier));
239
+ const legacy = assets.legacy.map((specifier) => renderLegacyAssetModule(specifier));
240
+ const resources = assets.resources.map((specifier) => renderResourceModule(specifier));
241
+
242
+ return `${[GTKX_ENV_MODULE_HEADER, ...blocked, ...icons, ...legacy, ...resources, ...schemas].join("\n\n")}\n`;
165
243
  };
166
244
 
167
245
  export { renderRuntimeModule, renderEnvModule };
@@ -1,10 +1,27 @@
1
1
  import { sortStrings, warn } from "@gtkx/utils";
2
2
  import { createHash } from "node:crypto";
3
- import { copyFileSync, type Dirent, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import {
4
+ copyFileSync,
5
+ type Dirent,
6
+ existsSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ realpathSync,
12
+ writeFileSync,
13
+ } from "node:fs";
4
14
  import { tmpdir } from "node:os";
5
- import { dirname, join, relative } from "node:path";
15
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
16
  import { DATA_IMPORT_PREFIX } from "../internal/data-dir.js";
17
+ import { discoverSourceImports, type SourceImport } from "../internal/source-imports.js";
7
18
  import { removeTempDir } from "../internal/staging-dir.js";
19
+ import {
20
+ isBareRelativeAsset,
21
+ isDataAsset,
22
+ parseIconSpecifier,
23
+ parseResourceSpecifier,
24
+ } from "../vite-plugins/asset-specifier.js";
8
25
  import { compileSchemas } from "./compile.js";
9
26
  import { type ParsedSchemaFile, parseSchemaXml, SchemaParseError } from "./parser.js";
10
27
  import { renderEnvModule } from "./render.js";
@@ -16,6 +33,9 @@ type SchemaEnvResult = {
16
33
 
17
34
  const SCHEMA_SUFFIX = ".gschema.xml";
18
35
  const STAGED_NAME_LENGTH = 16;
36
+ const SOURCE_DIR = "src";
37
+ const RESOURCE_QUERY = "?resource=";
38
+ const ICON_QUERY = "?icon=";
19
39
 
20
40
  const prependSchemaDir = (dir: string, existing: string | undefined): string => {
21
41
  if (existing === undefined || existing.length === 0) {
@@ -38,9 +58,150 @@ const stageSchema = (dir: string, filePath: string): void => {
38
58
 
39
59
  const toForwardSlashes = (value: string): string => value.replaceAll(/[/\\]/g, "/");
40
60
 
61
+ const projectRelativeSchemaPath = (root: string, filePath: string): string | null => {
62
+ let rel: string;
63
+
64
+ try {
65
+ rel = relative(realpathSync(root), realpathSync(filePath));
66
+ } catch {
67
+ return null;
68
+ }
69
+
70
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
71
+ return null;
72
+ }
73
+
74
+ return toForwardSlashes(rel);
75
+ };
76
+
41
77
  const getModuleSpecifier = (dataDirAbs: string, filePath: string): string =>
42
78
  `${DATA_IMPORT_PREFIX}/${toForwardSlashes(relative(dataDirAbs, filePath))}`;
43
79
 
80
+ const getRelativeModuleSpecifier = (filePath: string): string => `*/${basename(filePath)}`;
81
+
82
+ const sourceDirFor = (root: string): string => {
83
+ const sourceDir = join(root, SOURCE_DIR);
84
+
85
+ return existsSync(sourceDir) ? sourceDir : root;
86
+ };
87
+
88
+ const isRelativeImport = (source: string): boolean => source.startsWith("./") || source.startsWith("../");
89
+
90
+ const schemaFileFor = ({ importer, source }: SourceImport): string | null =>
91
+ isRelativeImport(source) && source.endsWith(SCHEMA_SUFFIX) ? resolve(dirname(importer), source) : null;
92
+
93
+ const findImportedSchemaFiles = (imports: SourceImport[]): string[] => {
94
+ const files = imports
95
+ .map((entry) => schemaFileFor(entry))
96
+ .filter((path): path is string => path !== null);
97
+
98
+ return sortStrings(new Set(files));
99
+ };
100
+
101
+ const legacyAssetSpecifier = (source: string): string | null =>
102
+ isDataAsset(source) ? source : null;
103
+
104
+ const blockedAssetSpecifier = (source: string): string | null => {
105
+ if (!isBareRelativeAsset(source)) {
106
+ return null;
107
+ }
108
+
109
+ const separator = source.lastIndexOf("/");
110
+ const name = source.slice(separator + 1);
111
+
112
+ if (name.includes("*")) {
113
+ throw new Error(
114
+ `Cannot generate an asset declaration for ${JSON.stringify(source)}: filenames cannot contain *`,
115
+ );
116
+ }
117
+
118
+ return `*/${name}`;
119
+ };
120
+
121
+ const resourceModuleSpecifier = (source: string): string | null => {
122
+ const parsed = parseResourceSpecifier(source);
123
+ const queryIndex = source.indexOf(RESOURCE_QUERY);
124
+
125
+ if (queryIndex === -1 || typeof parsed?.resourcePath !== "string") {
126
+ return null;
127
+ }
128
+
129
+ const query = source.slice(queryIndex);
130
+
131
+ if (query.includes("*")) {
132
+ throw new Error(
133
+ `Cannot generate an asset declaration for ${JSON.stringify(source)}: resource paths cannot contain *`,
134
+ );
135
+ }
136
+
137
+ return `*${query}`;
138
+ };
139
+
140
+ const iconModuleSpecifier = (source: string): string | null => {
141
+ const parsed = parseIconSpecifier(source);
142
+ const queryIndex = source.indexOf(ICON_QUERY);
143
+
144
+ if (queryIndex === -1 || typeof parsed?.iconName !== "string") {
145
+ return null;
146
+ }
147
+
148
+ const query = source.slice(queryIndex);
149
+
150
+ if (query.includes("*")) {
151
+ throw new Error(
152
+ `Cannot generate an asset declaration for ${JSON.stringify(source)}: icon names cannot contain *`,
153
+ );
154
+ }
155
+
156
+ return `*${query}`;
157
+ };
158
+
159
+ const findAssetModuleSpecifiers = (
160
+ imports: SourceImport[],
161
+ isV2ResourceImports: boolean,
162
+ ): { blocked: string[]; icons: string[]; legacy: string[]; resources: string[] } => {
163
+ const collect = (getSpecifier: (source: string) => string | null): string[] =>
164
+ sortStrings(new Set(imports.map((entry) => getSpecifier(entry.source)).filter((value) => value !== null)));
165
+
166
+ if (isV2ResourceImports) {
167
+ return {
168
+ blocked: collect(blockedAssetSpecifier),
169
+ icons: collect(iconModuleSpecifier),
170
+ legacy: [],
171
+ resources: collect(resourceModuleSpecifier),
172
+ };
173
+ }
174
+
175
+ return { blocked: [], icons: [], legacy: collect(legacyAssetSpecifier), resources: [] };
176
+ };
177
+
178
+ const canonicalPath = (path: string): string => {
179
+ try {
180
+ return realpathSync(path);
181
+ } catch {
182
+ return resolve(path);
183
+ }
184
+ };
185
+
186
+ const assertUniqueSchemaBasenames = (schemaFiles: string[]): void => {
187
+ const owners: Map<string, string> = new Map();
188
+ const canonicalFiles = sortStrings(new Set(schemaFiles.map((path) => canonicalPath(path))));
189
+
190
+ for (const filePath of canonicalFiles) {
191
+ const name = basename(filePath);
192
+ const owner = owners.get(name);
193
+
194
+ if (owner !== undefined && owner !== filePath) {
195
+ throw new Error(
196
+ `Cannot generate types for both ${owner} and ${filePath}: relative GSettings schema imports are ` +
197
+ `typed by basename, and both files are named ${name}. Rename one of them.`,
198
+ );
199
+ }
200
+
201
+ owners.set(name, filePath);
202
+ }
203
+ };
204
+
44
205
  const readVisibleEntries = (dir: string): Dirent[] => {
45
206
  try {
46
207
  return readdirSync(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith("."));
@@ -71,11 +232,11 @@ const findSchemaFiles = (dataDir: string): string[] => {
71
232
  };
72
233
 
73
234
  const stageAndCompileProjectSchemas = (root: string, dataDir: string | null): string | null => {
74
- if (dataDir === null) {
75
- return null;
76
- }
77
-
78
- const schemaFiles = findSchemaFiles(join(root, dataDir));
235
+ const imports = discoverSourceImports(sourceDirFor(root));
236
+ const imported = findImportedSchemaFiles(imports);
237
+ assertUniqueSchemaBasenames(imported);
238
+ const legacy = dataDir === null ? [] : findSchemaFiles(join(root, dataDir));
239
+ const schemaFiles = sortStrings(new Set([...legacy, ...imported]));
79
240
 
80
241
  if (schemaFiles.length === 0) {
81
242
  return null;
@@ -98,9 +259,7 @@ const stageAndCompileProjectSchemas = (root: string, dataDir: string | null): st
98
259
 
99
260
  const schemaEnvPath = (rootDir: string): string => join(rootDir, "node_modules", ".gtkx", "env.d.ts");
100
261
 
101
- const parseSchemaFileOrWarn = (filePath: string, dataDirAbs: string): ParsedSchemaFile | null => {
102
- const specifier = getModuleSpecifier(dataDirAbs, filePath);
103
-
262
+ const parseSchemaFileOrWarn = (filePath: string, specifier: string): ParsedSchemaFile | null => {
104
263
  try {
105
264
  return parseSchemaXml(readFileSync(filePath, "utf8"), specifier);
106
265
  } catch (error) {
@@ -114,11 +273,11 @@ const parseSchemaFileOrWarn = (filePath: string, dataDirAbs: string): ParsedSche
114
273
  }
115
274
  };
116
275
 
117
- const parseProjectSchemas = (schemaFiles: string[], dataDirAbs: string): ParsedSchemaFile[] => {
276
+ const parseProjectSchemas = (schemaFiles: string[], specifierFor: (filePath: string) => string): ParsedSchemaFile[] => {
118
277
  const parsed: ParsedSchemaFile[] = [];
119
278
 
120
279
  for (const filePath of schemaFiles) {
121
- const result = parseSchemaFileOrWarn(filePath, dataDirAbs);
280
+ const result = parseSchemaFileOrWarn(filePath, specifierFor(filePath));
122
281
 
123
282
  if (result !== null) {
124
283
  parsed.push(result);
@@ -147,11 +306,24 @@ const didWriteChanges = (path: string, content: string): boolean => {
147
306
  return true;
148
307
  };
149
308
 
150
- const emitSchemaEnv = (rootDir: string, dataDir: string | null): SchemaEnvResult => {
309
+ const emitSchemaEnv = (
310
+ rootDir: string,
311
+ dataDir: string | null,
312
+ isV2ResourceImports = false,
313
+ ): SchemaEnvResult => {
151
314
  const dataDirAbs = dataDir === null ? null : join(rootDir, dataDir);
152
- const schemaFiles = dataDirAbs === null ? [] : findSchemaFiles(dataDirAbs);
153
- const parsed = dataDirAbs === null ? [] : parseProjectSchemas(schemaFiles, dataDirAbs);
154
- const content = renderEnvModule(parsed);
315
+ const legacyFiles = dataDirAbs === null ? [] : findSchemaFiles(dataDirAbs);
316
+ const imports = discoverSourceImports(sourceDirFor(rootDir));
317
+
318
+ const legacy = dataDirAbs === null
319
+ ? []
320
+ : parseProjectSchemas(legacyFiles, (filePath) => getModuleSpecifier(dataDirAbs, filePath));
321
+
322
+ const importedFiles = findImportedSchemaFiles(imports);
323
+ assertUniqueSchemaBasenames(importedFiles);
324
+ const imported = parseProjectSchemas(importedFiles, getRelativeModuleSpecifier);
325
+ const assets = findAssetModuleSpecifiers(imports, isV2ResourceImports);
326
+ const content = renderEnvModule([...legacy, ...imported], assets);
155
327
  const path = schemaEnvPath(rootDir);
156
328
  const isWritten = didWriteChanges(path, content);
157
329
 
@@ -162,7 +334,8 @@ export {
162
334
  SCHEMA_SUFFIX,
163
335
  prependSchemaDir,
164
336
  stageSchema,
165
- findSchemaFiles,
337
+ assertUniqueSchemaBasenames,
338
+ projectRelativeSchemaPath,
166
339
  stageAndCompileProjectSchemas,
167
340
  emitSchemaEnv,
168
341
  };
@@ -1,5 +1,5 @@
1
1
  type AssetEmitter = {
2
- emitFile: (file: { type: "asset"; fileName: string; source: Buffer }) => void;
2
+ emitFile: (file: { type: "asset"; fileName: string; source: Buffer | string }) => void;
3
3
  };
4
4
 
5
5
  export { type AssetEmitter };
@@ -4,6 +4,7 @@ const ASSET_EXTENSIONS = [
4
4
  "jpeg",
5
5
  "gif",
6
6
  "svg",
7
+ "xpm",
7
8
  "webp",
8
9
  "webm",
9
10
  "mp4",
@@ -1,8 +1,17 @@
1
- import type { ParseResult, ParserOptions, Plugin } from "vite";
1
+ import type { ConfigLoader } from "@gtkx/config";
2
+ import type { ParseResult, ParserOptions, Plugin, UserConfig } from "vite";
3
+ import { createConfigLoader } from "@gtkx/config/internal";
2
4
  import { extname } from "node:path";
3
5
  import { parseSync } from "vite";
4
6
  import { ASSET_MENTION_RE } from "./asset-extensions.js";
5
- import { DATA_PREFIX, isAssetSpecifier, isDataAsset } from "./asset-specifier.js";
7
+ import {
8
+ DATA_PREFIX,
9
+ isAssetSpecifier,
10
+ isDataAsset,
11
+ isUrlSpecifier,
12
+ parseIconSpecifier,
13
+ parseResourceSpecifier,
14
+ } from "./asset-specifier.js";
6
15
  import { RESOURCE_PATH_EXPORT } from "./resource-shared.js";
7
16
  import { stripQuery } from "./strip-query.js";
8
17
 
@@ -13,6 +22,7 @@ type ExportStatement = ParsedModule["staticExports"][number];
13
22
  type ExportEntry = ExportStatement["entries"][number];
14
23
  type BindingEntry = { importName: { name: string | null }; isType: boolean };
15
24
  type NamedBinding = { name: string; source: string };
25
+ type PluginState = { isV2: boolean };
16
26
 
17
27
  const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
18
28
  [".cjs", "jsx"],
@@ -37,8 +47,12 @@ const BUNDLE_ADVICE =
37
47
  "nothing else.";
38
48
 
39
49
  const QUERY_ADVICE =
40
- "A query suffix hands the import to the Vite asset pipeline instead of the GResource bundle; drop the " +
41
- `query to keep ${RESOURCE_BINDING}${URL_FALLBACK}`;
50
+ `A URL asset exports only its default filesystem path; import the default instead of ${RESOURCE_BINDING}.`;
51
+
52
+ const RESOURCE_ADVICE =
53
+ `A resource asset exports only its default GResource path and ${RESOURCE_BINDING}, and nothing else.`;
54
+
55
+ const ICON_ADVICE = "An icon asset exports only its default icon-theme name, and nothing else.";
42
56
 
43
57
  const RELATIVE_ADVICE =
44
58
  `Only assets imported through "${DATA_PREFIX}" are staged into the GResource bundle and export ` +
@@ -76,34 +90,54 @@ const bindings = (parsed: ParsedModule): NamedBinding[] => [
76
90
  ...parsed.staticExports.flatMap((statement) => exportBindings(statement)),
77
91
  ];
78
92
 
79
- const isBacked = ({ name, source }: NamedBinding): boolean =>
80
- isDataAsset(source) && name === RESOURCE_PATH_EXPORT;
93
+ const isBacked = ({ name, source }: NamedBinding, isV2: boolean): boolean => {
94
+ if (name !== RESOURCE_PATH_EXPORT) {
95
+ return false;
96
+ }
97
+
98
+ return isV2 ? parseResourceSpecifier(source) !== null : isDataAsset(source);
99
+ };
100
+
101
+ const isCheckedSpecifier = (source: string): boolean => isAssetSpecifier(source) || isUrlSpecifier(source);
102
+
103
+ const unbackedBinding = (parsed: ParsedModule, isV2: boolean): NamedBinding | undefined =>
104
+ bindings(parsed).find((binding) => isCheckedSpecifier(binding.source) && !isBacked(binding, isV2));
81
105
 
82
- const unbackedBinding = (parsed: ParsedModule): NamedBinding | undefined =>
83
- bindings(parsed).find((binding) => isAssetSpecifier(binding.source) && !isBacked(binding));
106
+ const isBundledResource = (source: string, isV2: boolean): boolean =>
107
+ isV2
108
+ ? parseResourceSpecifier(source) !== null || parseIconSpecifier(source) !== null
109
+ : isDataAsset(source);
84
110
 
85
- const bundleAdvice = (source: string): string => {
86
- if (isDataAsset(source)) {
87
- return BUNDLE_ADVICE;
111
+ const v2BundleAdvice = (source: string): string =>
112
+ parseIconSpecifier(source) === null ? RESOURCE_ADVICE : ICON_ADVICE;
113
+
114
+ const bundleAdvice = (source: string, isV2: boolean): string => {
115
+ if (isBundledResource(source, isV2)) {
116
+ return isV2 ? v2BundleAdvice(source) : BUNDLE_ADVICE;
117
+ }
118
+
119
+ if (isUrlSpecifier(source)) {
120
+ return QUERY_ADVICE;
88
121
  }
89
122
 
90
123
  return source.startsWith(DATA_PREFIX) ? QUERY_ADVICE : RELATIVE_ADVICE;
91
124
  };
92
125
 
93
- const unbackedBindingError = (path: string, binding: NamedBinding): Error =>
126
+ const unbackedBindingError = (path: string, binding: NamedBinding, isV2: boolean): Error =>
94
127
  new Error(
95
128
  `${path}: ${JSON.stringify(binding.source)} does not export ${JSON.stringify(binding.name)}, which ` +
96
129
  "gtkx build rejects and gtkx dev would bind as undefined. " +
97
- bundleAdvice(binding.source),
130
+ bundleAdvice(binding.source, isV2),
98
131
  );
99
132
 
100
133
  const isCheckedSource = (path: string): boolean => !path.startsWith(VIRTUAL_PREFIX) && !NODE_MODULES.test(path);
134
+ const hasCheckedAssetMention = (code: string): boolean => ASSET_MENTION_RE.test(code) || code.includes("?url");
101
135
 
102
- const checkAssetImports = (code: string, id: string): void => {
136
+ const checkAssetImports = (code: string, id: string, isV2: boolean): void => {
103
137
  const path = stripQuery(id);
104
138
  const lang = SOURCE_LANGUAGES.get(extname(path).toLowerCase());
105
139
 
106
- if (lang === undefined || !isCheckedSource(path) || !ASSET_MENTION_RE.test(code)) {
140
+ if (lang === undefined || !isCheckedSource(path) || !hasCheckedAssetMention(code)) {
107
141
  return;
108
142
  }
109
143
 
@@ -113,20 +147,27 @@ const checkAssetImports = (code: string, id: string): void => {
113
147
  return;
114
148
  }
115
149
 
116
- const binding = unbackedBinding(parsed.module);
150
+ const binding = unbackedBinding(parsed.module, isV2);
117
151
 
118
152
  if (binding !== undefined) {
119
- throw unbackedBindingError(path, binding);
153
+ throw unbackedBindingError(path, binding, isV2);
120
154
  }
121
155
  };
122
156
 
123
- function gtkxAssetImports(): Plugin {
157
+ function gtkxAssetImports(loadConfig: ConfigLoader = createConfigLoader()): Plugin {
158
+ const state: PluginState = { isV2: false };
159
+
124
160
  return {
125
161
  name: "gtkx:asset-imports",
126
162
  enforce: "pre",
127
163
 
164
+ async config(config: UserConfig) {
165
+ const loaded = await loadConfig.load(config.root ?? process.cwd());
166
+ state.isV2 = loaded.config.future?.v2ResourceImports === true;
167
+ },
168
+
128
169
  transform(code, id) {
129
- checkAssetImports(code, id);
170
+ checkAssetImports(code, id, state.isV2);
130
171
  },
131
172
  };
132
173
  }