@gtkx/cli 1.2.2 → 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 (234) hide show
  1. package/README.md +6 -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/freshness.js +1 -1
  7. package/dist/codegen/freshness.js.map +1 -1
  8. package/dist/codegen/run-codegen.d.ts +2 -2
  9. package/dist/codegen/run-codegen.d.ts.map +1 -1
  10. package/dist/codegen/run-codegen.js +28 -9
  11. package/dist/codegen/run-codegen.js.map +1 -1
  12. package/dist/commands/build.d.ts +0 -4
  13. package/dist/commands/build.d.ts.map +1 -1
  14. package/dist/commands/build.js +0 -5
  15. package/dist/commands/build.js.map +1 -1
  16. package/dist/commands/codegen.d.ts.map +1 -1
  17. package/dist/commands/codegen.js +1 -3
  18. package/dist/commands/codegen.js.map +1 -1
  19. package/dist/commands/docs.d.ts.map +1 -1
  20. package/dist/commands/docs.js +3 -0
  21. package/dist/commands/docs.js.map +1 -1
  22. package/dist/deploy/depends.d.ts +2 -2
  23. package/dist/deploy/depends.d.ts.map +1 -1
  24. package/dist/deploy/depends.js +30 -15
  25. package/dist/deploy/depends.js.map +1 -1
  26. package/dist/deploy/nfpm/config.js +1 -1
  27. package/dist/deploy/nfpm/config.js.map +1 -1
  28. package/dist/deploy/node-runtime/elf.d.ts +1 -1
  29. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  30. package/dist/deploy/node-runtime/elf.js +2 -2
  31. package/dist/deploy/node-runtime/elf.js.map +1 -1
  32. package/dist/deploy/node-runtime/index.js +2 -2
  33. package/dist/deploy/node-runtime/index.js.map +1 -1
  34. package/dist/deploy/notices/collect.d.ts +3 -1
  35. package/dist/deploy/notices/collect.d.ts.map +1 -1
  36. package/dist/deploy/notices/collect.js +2 -2
  37. package/dist/deploy/notices/collect.js.map +1 -1
  38. package/dist/deploy/notices/packages.d.ts +2 -1
  39. package/dist/deploy/notices/packages.d.ts.map +1 -1
  40. package/dist/deploy/notices/packages.js +4 -27
  41. package/dist/deploy/notices/packages.js.map +1 -1
  42. package/dist/deploy/payload/icons.d.ts +7 -3
  43. package/dist/deploy/payload/icons.d.ts.map +1 -1
  44. package/dist/deploy/payload/icons.js +46 -28
  45. package/dist/deploy/payload/icons.js.map +1 -1
  46. package/dist/deploy/payload/stage.d.ts.map +1 -1
  47. package/dist/deploy/payload/stage.js +3 -2
  48. package/dist/deploy/payload/stage.js.map +1 -1
  49. package/dist/deploy/run-deploy.d.ts.map +1 -1
  50. package/dist/deploy/run-deploy.js +63 -11
  51. package/dist/deploy/run-deploy.js.map +1 -1
  52. package/dist/deploy/settings/build-manifest.d.ts +9 -0
  53. package/dist/deploy/settings/build-manifest.d.ts.map +1 -0
  54. package/dist/deploy/settings/build-manifest.js +80 -0
  55. package/dist/deploy/settings/build-manifest.js.map +1 -0
  56. package/dist/deploy/settings/index.d.ts.map +1 -1
  57. package/dist/deploy/settings/index.js +9 -3
  58. package/dist/deploy/settings/index.js.map +1 -1
  59. package/dist/deploy/settings/libraries.d.ts +8 -0
  60. package/dist/deploy/settings/libraries.d.ts.map +1 -0
  61. package/dist/deploy/settings/libraries.js +39 -0
  62. package/dist/deploy/settings/libraries.js.map +1 -0
  63. package/dist/deploy/settings/paths.d.ts +3 -0
  64. package/dist/deploy/settings/paths.d.ts.map +1 -1
  65. package/dist/deploy/settings/paths.js +6 -32
  66. package/dist/deploy/settings/paths.js.map +1 -1
  67. package/dist/deploy/targets/appimage.d.ts.map +1 -1
  68. package/dist/deploy/targets/appimage.js +19 -13
  69. package/dist/deploy/targets/appimage.js.map +1 -1
  70. package/dist/deploy/targets/flatpak-manifest.d.ts +2 -1
  71. package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
  72. package/dist/deploy/targets/flatpak-manifest.js +5 -1
  73. package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
  74. package/dist/deploy/targets/flatpak-pnpm.js +4 -4
  75. package/dist/deploy/targets/flatpak-pnpm.js.map +1 -1
  76. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  77. package/dist/deploy/targets/flatpak-source.js +9 -6
  78. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  79. package/dist/deploy/types.d.ts +4 -4
  80. package/dist/deploy/types.d.ts.map +1 -1
  81. package/dist/deploy/types.js.map +1 -1
  82. package/dist/dev/icon-dir.d.ts +1 -1
  83. package/dist/dev/icon-dir.d.ts.map +1 -1
  84. package/dist/dev/icon-dir.js +20 -7
  85. package/dist/dev/icon-dir.js.map +1 -1
  86. package/dist/dev/runner-deps.d.ts.map +1 -1
  87. package/dist/dev/runner-deps.js +5 -1
  88. package/dist/dev/runner-deps.js.map +1 -1
  89. package/dist/dev/runner-main.js +4 -4
  90. package/dist/dev/runner-main.js.map +1 -1
  91. package/dist/dev/runner.d.ts +1 -1
  92. package/dist/dev/runner.d.ts.map +1 -1
  93. package/dist/dev/runner.js +1 -1
  94. package/dist/dev/runner.js.map +1 -1
  95. package/dist/dev/schema-dir.d.ts +1 -1
  96. package/dist/dev/schema-dir.d.ts.map +1 -1
  97. package/dist/dev/schema-dir.js +3 -1
  98. package/dist/dev/schema-dir.js.map +1 -1
  99. package/dist/internal/build-manifest.d.ts +20 -0
  100. package/dist/internal/build-manifest.d.ts.map +1 -0
  101. package/dist/internal/build-manifest.js +6 -0
  102. package/dist/internal/build-manifest.js.map +1 -0
  103. package/dist/internal/icon-path.d.ts +13 -0
  104. package/dist/internal/icon-path.d.ts.map +1 -0
  105. package/dist/internal/icon-path.js +86 -0
  106. package/dist/internal/icon-path.js.map +1 -0
  107. package/dist/internal/source-imports.d.ts +8 -0
  108. package/dist/internal/source-imports.d.ts.map +1 -0
  109. package/dist/internal/source-imports.js +144 -0
  110. package/dist/internal/source-imports.js.map +1 -0
  111. package/dist/mcp/widget-properties.d.ts.map +1 -1
  112. package/dist/mcp/widget-properties.js +3 -12
  113. package/dist/mcp/widget-properties.js.map +1 -1
  114. package/dist/settings/render.d.ts +7 -1
  115. package/dist/settings/render.d.ts.map +1 -1
  116. package/dist/settings/render.js +69 -20
  117. package/dist/settings/render.js.map +1 -1
  118. package/dist/settings/schema.d.ts +4 -3
  119. package/dist/settings/schema.d.ts.map +1 -1
  120. package/dist/settings/schema.js +122 -15
  121. package/dist/settings/schema.js.map +1 -1
  122. package/dist/vite-plugins/asset-emitter.d.ts +1 -1
  123. package/dist/vite-plugins/asset-emitter.d.ts.map +1 -1
  124. package/dist/vite-plugins/asset-emitter.js.map +1 -1
  125. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  126. package/dist/vite-plugins/asset-extensions.js +1 -0
  127. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  128. package/dist/vite-plugins/asset-imports.d.ts +2 -1
  129. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  130. package/dist/vite-plugins/asset-imports.js +37 -16
  131. package/dist/vite-plugins/asset-imports.js.map +1 -1
  132. package/dist/vite-plugins/asset-specifier.d.ts +13 -1
  133. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  134. package/dist/vite-plugins/asset-specifier.js +39 -1
  135. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  136. package/dist/vite-plugins/build-manifest.d.ts +5 -0
  137. package/dist/vite-plugins/build-manifest.d.ts.map +1 -0
  138. package/dist/vite-plugins/{bundled-packages.js → build-manifest.js} +20 -14
  139. package/dist/vite-plugins/build-manifest.js.map +1 -0
  140. package/dist/vite-plugins/built-url.d.ts +1 -1
  141. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  142. package/dist/vite-plugins/built-url.js +37 -24
  143. package/dist/vite-plugins/built-url.js.map +1 -1
  144. package/dist/vite-plugins/icons.d.ts +2 -1
  145. package/dist/vite-plugins/icons.d.ts.map +1 -1
  146. package/dist/vite-plugins/icons.js +25 -12
  147. package/dist/vite-plugins/icons.js.map +1 -1
  148. package/dist/vite-plugins/index.d.ts +2 -1
  149. package/dist/vite-plugins/index.d.ts.map +1 -1
  150. package/dist/vite-plugins/index.js +7 -5
  151. package/dist/vite-plugins/index.js.map +1 -1
  152. package/dist/vite-plugins/native.d.ts.map +1 -1
  153. package/dist/vite-plugins/native.js +23 -4
  154. package/dist/vite-plugins/native.js.map +1 -1
  155. package/dist/vite-plugins/resource-init-module.d.ts +7 -3
  156. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  157. package/dist/vite-plugins/resource-init-module.js +39 -14
  158. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  159. package/dist/vite-plugins/resource-shared.d.ts +3 -1
  160. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  161. package/dist/vite-plugins/resource-shared.js +3 -1
  162. package/dist/vite-plugins/resource-shared.js.map +1 -1
  163. package/dist/vite-plugins/resources.d.ts +1 -1
  164. package/dist/vite-plugins/resources.d.ts.map +1 -1
  165. package/dist/vite-plugins/resources.js +922 -86
  166. package/dist/vite-plugins/resources.js.map +1 -1
  167. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  168. package/dist/vite-plugins/settings-worker-env.js +6 -3
  169. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  170. package/dist/vite-plugins/settings.d.ts +3 -1
  171. package/dist/vite-plugins/settings.d.ts.map +1 -1
  172. package/dist/vite-plugins/settings.js +63 -23
  173. package/dist/vite-plugins/settings.js.map +1 -1
  174. package/dist/vite-plugins/store-links.js +1 -1
  175. package/dist/vite-plugins/store-links.js.map +1 -1
  176. package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
  177. package/dist/vite-plugins/undeclared-library.js +25 -13
  178. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  179. package/env.d.ts +11 -192
  180. package/package.json +14 -14
  181. package/src/builder.ts +6 -7
  182. package/src/codegen/freshness.ts +1 -1
  183. package/src/codegen/run-codegen.ts +33 -10
  184. package/src/commands/build.ts +0 -5
  185. package/src/commands/codegen.ts +1 -3
  186. package/src/commands/docs.ts +3 -0
  187. package/src/deploy/depends.ts +47 -18
  188. package/src/deploy/nfpm/config.ts +1 -1
  189. package/src/deploy/node-runtime/elf.ts +3 -3
  190. package/src/deploy/node-runtime/index.ts +2 -2
  191. package/src/deploy/notices/collect.ts +4 -2
  192. package/src/deploy/notices/packages.ts +5 -43
  193. package/src/deploy/payload/icons.ts +64 -34
  194. package/src/deploy/payload/stage.ts +5 -2
  195. package/src/deploy/run-deploy.ts +87 -12
  196. package/src/deploy/settings/build-manifest.ts +122 -0
  197. package/src/deploy/settings/index.ts +9 -4
  198. package/src/deploy/settings/libraries.ts +57 -0
  199. package/src/deploy/settings/paths.ts +10 -47
  200. package/src/deploy/targets/appimage.ts +32 -15
  201. package/src/deploy/targets/flatpak-manifest.ts +7 -1
  202. package/src/deploy/targets/flatpak-pnpm.ts +4 -4
  203. package/src/deploy/targets/flatpak-source.ts +9 -10
  204. package/src/deploy/types.ts +4 -4
  205. package/src/dev/icon-dir.ts +31 -7
  206. package/src/dev/runner-deps.ts +5 -1
  207. package/src/dev/runner-main.ts +4 -4
  208. package/src/dev/runner.ts +2 -2
  209. package/src/dev/schema-dir.ts +3 -1
  210. package/src/internal/build-manifest.ts +32 -0
  211. package/src/internal/icon-path.ts +127 -0
  212. package/src/internal/source-imports.ts +200 -0
  213. package/src/mcp/widget-properties.ts +3 -14
  214. package/src/settings/render.ts +99 -21
  215. package/src/settings/schema.ts +190 -17
  216. package/src/vite-plugins/asset-emitter.ts +1 -1
  217. package/src/vite-plugins/asset-extensions.ts +1 -0
  218. package/src/vite-plugins/asset-imports.ts +60 -19
  219. package/src/vite-plugins/asset-specifier.ts +73 -1
  220. package/src/vite-plugins/{bundled-packages.ts → build-manifest.ts} +30 -19
  221. package/src/vite-plugins/built-url.ts +49 -28
  222. package/src/vite-plugins/icons.ts +33 -14
  223. package/src/vite-plugins/index.ts +12 -5
  224. package/src/vite-plugins/native.ts +31 -4
  225. package/src/vite-plugins/resource-init-module.ts +44 -15
  226. package/src/vite-plugins/resource-shared.ts +4 -0
  227. package/src/vite-plugins/resources.ts +1459 -100
  228. package/src/vite-plugins/settings-worker-env.ts +7 -3
  229. package/src/vite-plugins/settings.ts +89 -21
  230. package/src/vite-plugins/store-links.ts +1 -1
  231. package/src/vite-plugins/undeclared-library.ts +31 -13
  232. package/dist/vite-plugins/bundled-packages.d.ts +0 -5
  233. package/dist/vite-plugins/bundled-packages.d.ts.map +0 -1
  234. package/dist/vite-plugins/bundled-packages.js.map +0 -1
@@ -0,0 +1,127 @@
1
+ import { type Stats, statSync } from "node:fs";
2
+ import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
3
+
4
+ type ResolvedApplicationIcon = { kind: "file"; path: string } |
5
+ { kind: "none" } |
6
+ { kind: "theme"; path: string };
7
+
8
+ const ICON_EXTENSIONS: Set<string> = new Set([".svg", ".png", ".xpm"]);
9
+ const SCALABLE_DIR = "hicolor/scalable/apps";
10
+ const SIZE_PATTERN = /^(?<width>[1-9]\d{0,3})x(?<height>[1-9]\d{0,3})$/;
11
+ const SIZE_SEPARATORS = /[-_.]/;
12
+
13
+ const isInside = (parent: string, candidate: string): boolean => {
14
+ const rel = relative(parent, candidate);
15
+
16
+ return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
17
+ };
18
+
19
+ const getStats = (path: string): Stats | undefined => {
20
+ try {
21
+ return statSync(path, { throwIfNoEntry: false });
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ };
26
+
27
+ const iconExtension = (file: string): string => {
28
+ const extension = extname(file).toLowerCase();
29
+
30
+ if (!ICON_EXTENSIONS.has(extension)) {
31
+ throw new Error(`Cannot use ${file} as an application icon: expected an SVG, PNG, or XPM file`);
32
+ }
33
+
34
+ return extension;
35
+ };
36
+
37
+ const defaultIconFiles = (root: string, applicationId: string): string[] =>
38
+ [...ICON_EXTENSIONS]
39
+ .map((extension) => join(root, `${applicationId}${extension}`))
40
+ .filter((path) => getStats(path)?.isFile() === true);
41
+
42
+ const resolveDefaultApplicationIcon = (root: string, applicationId: string): ResolvedApplicationIcon => {
43
+ const files = defaultIconFiles(root, applicationId);
44
+
45
+ if (files.length === 0) {
46
+ return { kind: "none" };
47
+ }
48
+
49
+ if (files.length > 1) {
50
+ throw new Error(
51
+ `Found multiple default application icons for ${applicationId}; set \`applicationIcon\` to choose one`,
52
+ );
53
+ }
54
+
55
+ const path = files[0];
56
+
57
+ if (path === undefined) {
58
+ return { kind: "none" };
59
+ }
60
+
61
+ return { kind: "file", path };
62
+ };
63
+
64
+ const resolveApplicationIcon = (
65
+ root: string,
66
+ applicationId: string,
67
+ configured: string | undefined,
68
+ ): ResolvedApplicationIcon => {
69
+ if (configured === undefined) {
70
+ return resolveDefaultApplicationIcon(root, applicationId);
71
+ }
72
+
73
+ if (isAbsolute(configured)) {
74
+ throw new Error(`Cannot use "${configured}" as the application icon: expected a project-relative path`);
75
+ }
76
+
77
+ const path = resolve(root, configured);
78
+
79
+ if (!isInside(root, path)) {
80
+ throw new Error(`Cannot use "${configured}" as the application icon: it is outside ${root}`);
81
+ }
82
+
83
+ const stats = getStats(path);
84
+
85
+ if (stats?.isDirectory() === true) {
86
+ return { kind: "theme", path };
87
+ }
88
+
89
+ if (stats?.isFile() !== true) {
90
+ throw new Error(`Cannot read the application icon "${configured}": no such file or directory under ${root}`);
91
+ }
92
+
93
+ iconExtension(path);
94
+
95
+ return { kind: "file", path };
96
+ };
97
+
98
+ const squareSize = (token: string): number | null => {
99
+ const groups = SIZE_PATTERN.exec(token)?.groups;
100
+
101
+ if (groups === undefined || groups.width !== groups.height) {
102
+ return null;
103
+ }
104
+
105
+ return Number(groups.width);
106
+ };
107
+
108
+ const iconThemeDir = (file: string, extension: string): string => {
109
+ if (extension === ".svg") {
110
+ return SCALABLE_DIR;
111
+ }
112
+
113
+ const size = basename(file)
114
+ .split(SIZE_SEPARATORS)
115
+ .map((token) => squareSize(token))
116
+ .find((value) => value !== null);
117
+
118
+ return size === undefined ? SCALABLE_DIR : `hicolor/${String(size)}x${String(size)}/apps`;
119
+ };
120
+
121
+ const relativeIconPath = (applicationId: string, file: string): string => {
122
+ const extension = iconExtension(file);
123
+
124
+ return `${iconThemeDir(file, extension)}/${applicationId}${extension}`;
125
+ };
126
+
127
+ export { relativeIconPath, resolveApplicationIcon, type ResolvedApplicationIcon };
@@ -0,0 +1,200 @@
1
+ import type { ParseResult, ParserOptions } from "vite";
2
+ import { isRecord, sortStringsBy } from "@gtkx/utils";
3
+ import { type Dirent, readdirSync, readFileSync } from "node:fs";
4
+ import { extname, join } from "node:path";
5
+ import { parseSync } from "vite";
6
+
7
+ type SourceImport = {
8
+ importer: string;
9
+ source: string;
10
+ };
11
+
12
+ type SourceLanguage = NonNullable<ParserOptions["lang"]>;
13
+ type ParsedModule = ParseResult["module"];
14
+
15
+ const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
16
+ [".cjs", "jsx"],
17
+ [".cts", "ts"],
18
+ [".js", "jsx"],
19
+ [".jsx", "jsx"],
20
+ [".mjs", "jsx"],
21
+ [".mts", "ts"],
22
+ [".ts", "ts"],
23
+ [".tsx", "tsx"],
24
+ ]);
25
+
26
+ const EXCLUDED_DIRECTORIES: Set<string> = new Set([
27
+ ".git",
28
+ ".gtkx",
29
+ "build",
30
+ "coverage",
31
+ "dist",
32
+ "node_modules",
33
+ "out-tsc",
34
+ ]);
35
+
36
+ const sourceLanguage = (path: string): SourceLanguage | undefined => {
37
+ if (path.endsWith(".d.ts") || path.endsWith(".d.cts") || path.endsWith(".d.mts")) {
38
+ return undefined;
39
+ }
40
+
41
+ return SOURCE_LANGUAGES.get(extname(path).toLowerCase());
42
+ };
43
+
44
+ const collectSourceEntry = (dir: string, entry: Dirent, found: string[]): void => {
45
+ const path = join(dir, entry.name);
46
+
47
+ if (entry.isDirectory()) {
48
+ if (!EXCLUDED_DIRECTORIES.has(entry.name)) {
49
+ sourceFilesIn(path, found);
50
+ }
51
+
52
+ return;
53
+ }
54
+
55
+ if (entry.isFile() && sourceLanguage(path) !== undefined) {
56
+ found.push(path);
57
+ }
58
+ };
59
+
60
+ const sourceFilesIn = (dir: string, found: string[]): void => {
61
+ let entries: Dirent[];
62
+
63
+ try {
64
+ entries = readdirSync(dir, { withFileTypes: true });
65
+ } catch {
66
+ return;
67
+ }
68
+
69
+ for (const entry of entries) {
70
+ collectSourceEntry(dir, entry, found);
71
+ }
72
+ };
73
+
74
+ const staticImportSources = (module: ParsedModule): string[] => [
75
+ ...module.staticImports.map((statement) => statement.moduleRequest.value),
76
+ ...module.staticExports.flatMap((statement) =>
77
+ statement.entries
78
+ .map((entry) => entry.moduleRequest?.value)
79
+ .filter((source): source is string => source !== undefined),
80
+ ),
81
+ ];
82
+
83
+ const staticRuntimeImportSources = (module: ParsedModule): string[] => [
84
+ ...module.staticImports
85
+ .filter((statement) => statement.entries.length === 0 || statement.entries.some((entry) => !entry.isType))
86
+ .map((statement) => statement.moduleRequest.value),
87
+ ...module.staticExports.flatMap((statement) =>
88
+ statement.entries
89
+ .filter((entry) => !entry.isType)
90
+ .map((entry) => entry.moduleRequest?.value)
91
+ .filter((source): source is string => source !== undefined),
92
+ ),
93
+ ];
94
+
95
+ const templateImportSource = (node: Record<string, unknown>): string | null => {
96
+ if (!Array.isArray(node.expressions) || node.expressions.length > 0 || !Array.isArray(node.quasis)) {
97
+ return null;
98
+ }
99
+
100
+ const quasis: unknown[] = node.quasis;
101
+ const first: unknown = quasis[0];
102
+
103
+ if (!isRecord(first) || !isRecord(first.value)) {
104
+ return null;
105
+ }
106
+
107
+ return typeof first.value.cooked === "string" ? first.value.cooked : null;
108
+ };
109
+
110
+ const literalImportSource = (node: unknown): string | null => {
111
+ if (!isRecord(node)) {
112
+ return null;
113
+ }
114
+
115
+ if (node.type === "Literal") {
116
+ return typeof node.value === "string" ? node.value : null;
117
+ }
118
+
119
+ return node.type === "TemplateLiteral" ? templateImportSource(node) : null;
120
+ };
121
+
122
+ const collectDynamicImportArray = (nodes: unknown[], found: string[]): void => {
123
+ for (const child of nodes) {
124
+ collectDynamicImportSources(child, found);
125
+ }
126
+ };
127
+
128
+ const collectDynamicImportRecord = (node: Record<string, unknown>, found: string[]): void => {
129
+ if (node.type === "ImportExpression") {
130
+ const source = literalImportSource(node.source);
131
+
132
+ if (source !== null) {
133
+ found.push(source);
134
+ }
135
+
136
+ return;
137
+ }
138
+
139
+ collectDynamicImportArray(Object.values(node), found);
140
+ };
141
+
142
+ const collectDynamicImportSources = (node: unknown, found: string[]): void => {
143
+ if (Array.isArray(node)) {
144
+ collectDynamicImportArray(node, found);
145
+
146
+ return;
147
+ }
148
+
149
+ if (!isRecord(node)) {
150
+ return;
151
+ }
152
+
153
+ collectDynamicImportRecord(node, found);
154
+ };
155
+
156
+ const parseImportsInWith = (
157
+ path: string,
158
+ staticSources: (module: ParsedModule) => string[],
159
+ ): SourceImport[] | null => {
160
+ const lang = sourceLanguage(path);
161
+
162
+ if (lang === undefined) {
163
+ return [];
164
+ }
165
+
166
+ let parsed: ParseResult;
167
+
168
+ try {
169
+ parsed = parseSync(path, readFileSync(path, "utf8"), { lang });
170
+ } catch {
171
+ return null;
172
+ }
173
+
174
+ if (parsed.errors.length > 0) {
175
+ return null;
176
+ }
177
+
178
+ const sources = staticSources(parsed.module);
179
+ collectDynamicImportSources(parsed.program, sources);
180
+
181
+ return [...new Set(sources)].map((source) => ({ importer: path, source }));
182
+ };
183
+
184
+ const parseImportsIn = (path: string): SourceImport[] | null => parseImportsInWith(path, staticImportSources);
185
+
186
+ const parseRuntimeImportsIn = (path: string): SourceImport[] | null =>
187
+ parseImportsInWith(path, staticRuntimeImportSources);
188
+
189
+ const importsIn = (path: string): SourceImport[] => parseImportsIn(path) ?? [];
190
+ const importKey = (entry: SourceImport): string => `${entry.importer}\0${entry.source}`;
191
+
192
+ const discoverSourceImports = (dir: string): SourceImport[] => {
193
+ const files: string[] = [];
194
+ sourceFilesIn(dir, files);
195
+ const imports = files.flatMap((path) => importsIn(path));
196
+
197
+ return sortStringsBy(imports, importKey);
198
+ };
199
+
200
+ export { discoverSourceImports, parseRuntimeImportsIn, type SourceImport };
@@ -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 };