@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
@@ -1,29 +1,38 @@
1
1
  import type { ConfigLoader } from "@gtkx/config";
2
- import type { Plugin, ResolvedConfig, UserConfig, ViteDevServer } from "vite";
3
- import { createConfigLoader } from "@gtkx/config/internal";
4
- import { error, info, sortStrings } from "@gtkx/utils";
5
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import { dirname, join } from "node:path";
7
- import type { AssetEmitter } from "./asset-emitter.js";
2
+ import type { ModuleNode, ParserOptions, Plugin, ResolvedConfig, Rolldown, UserConfig, ViteDevServer } from "vite";
3
+ import { createConfigLoader, resourceBasePath } from "@gtkx/config/internal";
4
+ import { error, info, isRecord, sortStrings } from "@gtkx/utils";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, sep } from "node:path";
7
+ import { parseSync } from "vite";
8
8
  import { resolveDataDir } from "../internal/data-dir.js";
9
9
  import { type ListedFile, listFilesRecursive } from "../internal/list-files.js";
10
- import { loadModuleExclusively } from "../internal/module-loads.js";
11
10
  import { runCliTool } from "../internal/run-cli-tool.js";
11
+ import { parseRuntimeImportsIn, type SourceImport } from "../internal/source-imports.js";
12
12
  import { createRetainedStagingDir, type RetainedStagingDir, withStagingDir } from "../internal/staging-dir.js";
13
13
  import { ASSET_RE } from "./asset-extensions.js";
14
- import { DATA_PREFIX, isDataAsset } from "./asset-specifier.js";
14
+ import {
15
+ DATA_PREFIX,
16
+ isBareRelativeAsset,
17
+ isDataAsset,
18
+ parseIconSpecifier,
19
+ parseResourceSpecifier,
20
+ } from "./asset-specifier.js";
15
21
  import { renderInitModule } from "./resource-init-module.js";
16
22
  import {
17
23
  BUNDLE_FILENAME,
18
24
  escapeXml,
19
25
  fromVirtualId,
26
+ ICON_NAME_SEPARATOR,
20
27
  isVirtual,
21
28
  REFRESH_EXPORT,
29
+ REGISTER_REFRESH_EXPORT,
22
30
  REL_SEPARATOR,
23
31
  RESOURCE_PATH_EXPORT,
24
32
  toVirtualId,
25
33
  VIRTUAL_INIT,
26
34
  } from "./resource-shared.js";
35
+ import { stripQuery } from "./strip-query.js";
27
36
 
28
37
  type ResourceEntry = {
29
38
  sourcePath: string;
@@ -33,21 +42,320 @@ type ResourceEntry = {
33
42
 
34
43
  type PluginState = {
35
44
  prefix: string;
45
+ root: string;
36
46
  isBuild: boolean;
47
+ isV2: boolean;
37
48
  entries: Map<string, ResourceEntry>;
49
+ entryImporters: Map<string, Set<string>>;
50
+ bundledSpecifiers: Map<string, Set<string>>;
51
+ moduleDependencies: Map<string, Set<string>>;
52
+ prunedDependencies: Map<string, Set<string>>;
53
+ declaredEntries: Map<string, Map<string, ResourceEntry>>;
54
+ declaredIcons: Map<string, Map<string, ResourceEntry>>;
55
+ entryModule: string | null;
38
56
  sourcePaths: Set<string>;
57
+ missingSourcePaths: Set<string>;
39
58
  stagingDir: RetainedStagingDir;
40
59
  devBundlePath: string;
41
60
  server: ViteDevServer | null;
42
61
  compiledSignature: string;
43
62
  dataDir: string | null;
63
+ bundleReferenceId: string | null;
64
+ iconOwners: Map<string, ResourceEntry>;
65
+ iconImporters: Map<string, Set<string>>;
66
+ };
67
+
68
+ type ResolveContext = {
69
+ resolve: (
70
+ source: string,
71
+ importer?: string,
72
+ options?: Record<string, unknown>,
73
+ ) => Promise<{ id: string; external?: boolean | string } | null>;
74
+ };
75
+
76
+ type PackageOwner = {
77
+ dir: string;
78
+ name: string;
79
+ };
80
+
81
+ type AssetResolveRequest = {
82
+ assetSource: string;
83
+ importer: string | undefined;
84
+ resourcePath: string | null;
85
+ options: Record<string, unknown> | undefined;
86
+ };
87
+
88
+ type IconResolveRequest = {
89
+ assetSource: string;
90
+ iconName: string | null;
91
+ importer: string | undefined;
92
+ options: Record<string, unknown> | undefined;
93
+ };
94
+
95
+ type ResolvedIconEntry = {
96
+ entry: ResourceEntry;
97
+ iconName: string;
98
+ };
99
+
100
+ type RetainedIconModuleId = {
101
+ id: string;
102
+ moduleSideEffects: true;
103
+ };
104
+
105
+ type ResourceModuleId = string | RetainedIconModuleId;
106
+ type SourceLanguage = NonNullable<ParserOptions["lang"]>;
107
+
108
+ type ImporterReconciliation = {
109
+ currentBundled: Set<string>;
110
+ currentDependencies: Set<string>;
111
+ file: string;
112
+ imports: SourceImport[];
113
+ key: string;
114
+ previousBundled: Set<string> | undefined;
115
+ previousDependencies: Set<string> | undefined;
116
+ };
117
+
118
+ type DerivedIconEntry = {
119
+ iconIdentity: string;
120
+ iconName: string;
121
+ resourcePath: string;
122
+ };
123
+
124
+ type LegacyResolveRequest = {
125
+ source: string;
126
+ importer: string | undefined;
127
+ options: Record<string, unknown> | undefined;
44
128
  };
45
129
 
46
130
  const RESOURCE_COMPILER = "glib-compile-resources";
47
131
  const MANIFEST_PREFIX = "/";
48
132
  const DEV_STAGING_PREFIX = "resources-dev";
133
+ const MANIFEST_FILENAME = "package.json";
134
+ const BUNDLED_QUERY_MENTION_RE = /[?&](?:icon|resource)(?:[=&]|$)/;
135
+ const ICON_EXTENSIONS: Set<string> = new Set([".png", ".svg", ".xpm"]);
136
+
137
+ const ICON_CONTEXTS: Set<string> = new Set([
138
+ "actions",
139
+ "animations",
140
+ "apps",
141
+ "categories",
142
+ "devices",
143
+ "emblems",
144
+ "emotes",
145
+ "filesystems",
146
+ "intl",
147
+ "mimetypes",
148
+ "places",
149
+ "status",
150
+ ]);
151
+
152
+ const STOCK_ICON_CONTEXTS: Set<string> = new Set([
153
+ "chart",
154
+ "code",
155
+ "data",
156
+ "form",
157
+ "image",
158
+ "io",
159
+ "media",
160
+ "navigation",
161
+ "net",
162
+ "object",
163
+ "table",
164
+ "text",
165
+ ]);
166
+
167
+ const ICON_SIZE_RE = /^([1-9]\d*)x\1(?:@[1-9]\d*)?$/;
168
+ const RESERVED_ICON_NAMES: Set<string> = new Set(["", ".", ".."]);
169
+
170
+ const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
171
+ [".cjs", "jsx"],
172
+ [".cts", "ts"],
173
+ [".js", "jsx"],
174
+ [".jsx", "jsx"],
175
+ [".mjs", "jsx"],
176
+ [".mts", "ts"],
177
+ [".ts", "ts"],
178
+ [".tsx", "tsx"],
179
+ ]);
180
+
181
+ const toForwardSlashes = (value: string): string => value.replaceAll(/[/\\]/g, "/");
182
+
183
+ const isWithin = (root: string, path: string): boolean => {
184
+ const rel = relative(root, path);
185
+
186
+ return rel.length > 0 && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
187
+ };
188
+
189
+ const isDependencyFile = (root: string, path: string): boolean =>
190
+ isWithin(root, path) && relative(root, path).split(sep).includes("node_modules");
191
+
192
+ const packageIdentityIn = (dir: string): string | null => {
193
+ try {
194
+ const manifest: unknown = JSON.parse(readFileSync(join(dir, MANIFEST_FILENAME), "utf8"));
195
+
196
+ return isRecord(manifest) && typeof manifest.name === "string" ? manifest.name : null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ };
201
+
202
+ const packageIn = (dir: string): PackageOwner | null => {
203
+ const name = packageIdentityIn(dir);
204
+
205
+ if (name !== null) {
206
+ return { dir, name };
207
+ }
208
+
209
+ const parent = dirname(dir);
210
+
211
+ return parent === dir ? null : packageIn(parent);
212
+ };
213
+
214
+ const packageForFile = (file: string): PackageOwner | null => packageIn(dirname(file));
215
+
216
+ const validateResourcePath = (resourcePath: string): string => {
217
+ if (
218
+ resourcePath === "/" ||
219
+ !resourcePath.startsWith("/") ||
220
+ resourcePath.endsWith("/") ||
221
+ resourcePath.includes("\0") ||
222
+ posix.normalize(resourcePath) !== resourcePath
223
+ ) {
224
+ throw new Error(
225
+ `${JSON.stringify(resourcePath)} is not a valid GResource path; use an absolute normalized path`,
226
+ );
227
+ }
228
+
229
+ return resourcePath;
230
+ };
231
+
232
+ const derivedResourcePath = (state: PluginState, sourcePath: string): string => {
233
+ if (isWithin(state.root, sourcePath) && !isDependencyFile(state.root, sourcePath)) {
234
+ return validateResourcePath(`${state.prefix}/${toForwardSlashes(relative(state.root, sourcePath))}`);
235
+ }
236
+
237
+ const owner = packageForFile(sourcePath);
238
+
239
+ if (owner === null || !isWithin(owner.dir, sourcePath)) {
240
+ throw new Error(`${sourcePath} is outside the application and does not belong to a named package`);
241
+ }
242
+
243
+ const packagePath = toForwardSlashes(relative(owner.dir, sourcePath));
244
+
245
+ return validateResourcePath(`${state.prefix}/${owner.name}/${packagePath}`);
246
+ };
247
+
248
+ const iconExtension = (sourcePath: string): string => {
249
+ const extension = extname(sourcePath).toLowerCase();
250
+
251
+ if (!ICON_EXTENSIONS.has(extension)) {
252
+ throw new Error(`${sourcePath} is not a supported themed icon; use an SVG, PNG, or XPM file`);
253
+ }
254
+
255
+ return extension;
256
+ };
257
+
258
+ const hasIconNamePathSeparator = (value: string): boolean =>
259
+ value.includes("\0") || value.includes("/") || value.includes("\\");
260
+
261
+ const validateIconName = (value: string): string => {
262
+ if (
263
+ RESERVED_ICON_NAMES.has(value) ||
264
+ hasIconNamePathSeparator(value) ||
265
+ ICON_EXTENSIONS.has(extname(value).toLowerCase())
266
+ ) {
267
+ throw new Error(
268
+ `${JSON.stringify(value)} is not a valid icon name; use an extensionless filename without slashes`,
269
+ );
270
+ }
271
+
272
+ return value;
273
+ };
274
+
275
+ const isIconContext = (parts: string[]): boolean => {
276
+ const [first, second] = parts;
277
+
278
+ if (parts.length === 1) {
279
+ return first !== undefined && ICON_CONTEXTS.has(first);
280
+ }
281
+
282
+ return parts.length === 2 && first === "stock" && second !== undefined && STOCK_ICON_CONTEXTS.has(second);
283
+ };
284
+
285
+ const isIconLayout = (parts: string[]): boolean => {
286
+ const [size, ...context] = parts;
287
+
288
+ if (size === "symbolic") {
289
+ return context.length === 1 && context[0] === "apps";
290
+ }
291
+
292
+ return size !== undefined && (size === "scalable" || ICON_SIZE_RE.test(size)) && isIconContext(context);
293
+ };
294
+
295
+ const iconLayoutInParts = (parts: string[], beforeIndex: number): string | null => {
296
+ const index = parts.lastIndexOf("icons", beforeIndex);
297
+
298
+ if (index === -1) {
299
+ return null;
300
+ }
301
+
302
+ const treePath = parts.slice(index + 1, -1);
303
+ const layout = treePath[0] === "hicolor" ? treePath.slice(1) : treePath;
304
+
305
+ return isIconLayout(layout) ? layout.join("/") : iconLayoutInParts(parts, index - 1);
306
+ };
307
+
308
+ const iconLayoutFromTree = (sourcePath: string): string | null => {
309
+ const parts = toForwardSlashes(sourcePath).split("/");
310
+
311
+ return iconLayoutInParts(parts, parts.length - 2);
312
+ };
49
313
 
50
- const deriveResourcePrefix = (applicationId: string): string => `/${applicationId.replaceAll(".", "/")}`;
314
+ const derivedIconEntry = (
315
+ state: PluginState,
316
+ sourcePath: string,
317
+ requestedName: string | null,
318
+ ): DerivedIconEntry => {
319
+ const extension = iconExtension(sourcePath);
320
+ const sourceName = basename(sourcePath, extname(sourcePath));
321
+ const iconName = validateIconName(requestedName ?? sourceName);
322
+ const layout = iconLayoutFromTree(sourcePath);
323
+
324
+ const iconIdentity = layout === null
325
+ ? `${state.prefix}/icons/${iconName}`
326
+ : `${state.prefix}/icons/${layout}/${iconName}`;
327
+
328
+ const resourcePath = validateResourcePath(`${iconIdentity}${extension}`);
329
+
330
+ return { iconIdentity, iconName, resourcePath };
331
+ };
332
+
333
+ const hasSideEffectIconImport = (code: string, id: string): boolean => {
334
+ const lang = SOURCE_LANGUAGES.get(extname(stripQuery(id)).toLowerCase());
335
+
336
+ if (lang === undefined || !code.includes("?icon")) {
337
+ return false;
338
+ }
339
+
340
+ const parsed = parseSync(id, code, { lang });
341
+
342
+ if (parsed.errors.length > 0) {
343
+ return false;
344
+ }
345
+
346
+ return parsed.module.staticImports.some(
347
+ (statement) => statement.entries.length === 0 && parseIconSpecifier(statement.moduleRequest.value) !== null,
348
+ );
349
+ };
350
+
351
+ const retainSideEffectIconImport = (
352
+ state: PluginState,
353
+ code: string,
354
+ id: string,
355
+ ): { code: string; moduleSideEffects: true } | null =>
356
+ state.isV2 && hasSideEffectIconImport(code, id)
357
+ ? { code, moduleSideEffects: true }
358
+ : null;
51
359
 
52
360
  const compileBundle = (state: PluginState, outputPath: string): Buffer =>
53
361
  withStagingDir("resources", (dir) => {
@@ -103,12 +411,16 @@ const releaseStagingDir = (state: PluginState): void => {
103
411
  state.compiledSignature = "";
104
412
  };
105
413
 
106
- const entriesSignature = (state: PluginState): string => sortStrings(state.entries.keys()).join("\0");
414
+ const entriesSignature = (state: PluginState): string =>
415
+ sortStrings([...state.entries].map(([resourcePath, entry]) => `${resourcePath}\0${entry.sourcePath}`)).join("\0");
107
416
 
108
417
  const compileDevBundle = (state: PluginState): void => {
109
418
  ensureStagingDir(state);
110
419
 
111
420
  if (state.entries.size === 0) {
421
+ rmSync(state.devBundlePath, { force: true });
422
+ state.compiledSignature = "";
423
+
112
424
  return;
113
425
  }
114
426
 
@@ -118,15 +430,14 @@ const compileDevBundle = (state: PluginState): void => {
118
430
 
119
431
  const isRefreshHook = (value: unknown): value is () => void => typeof value === "function";
120
432
 
121
- const reregisterDevBundle = async (state: PluginState): Promise<void> => {
433
+ const refreshRegisteredDevBundle = (state: PluginState): void => {
122
434
  const server = state.server;
123
435
 
124
436
  if (!server) {
125
437
  return;
126
438
  }
127
439
 
128
- const mod = await loadModuleExclusively(server, VIRTUAL_INIT);
129
- const refresh: unknown = mod[REFRESH_EXPORT];
440
+ const refresh: unknown = server.moduleGraph.getModuleById(VIRTUAL_INIT)?.ssrModule?.[REFRESH_EXPORT];
130
441
 
131
442
  if (isRefreshHook(refresh)) {
132
443
  refresh();
@@ -135,7 +446,7 @@ const reregisterDevBundle = async (state: PluginState): Promise<void> => {
135
446
 
136
447
  const scanDataAssets = (dataDir: string): ListedFile[] => listFilesRecursive(dataDir, (name) => ASSET_RE.test(name));
137
448
 
138
- const primeDevBundle = (state: PluginState): void => {
449
+ const primeLegacyDevBundle = (state: PluginState): void => {
139
450
  if (state.dataDir === null) {
140
451
  return;
141
452
  }
@@ -153,114 +464,1009 @@ const registerDevAsset = (state: PluginState): void => {
153
464
  }
154
465
  };
155
466
 
156
- const registerEntry = (state: PluginState, absPath: string, rel: string): ResourceEntry => {
157
- const existing = state.entries.get(absPath);
467
+ const importerKey = (importer: string): string => stripQuery(importer);
468
+
469
+ const claimEntry = (state: PluginState, resourcePath: string, importer: string | undefined): void => {
470
+ if (importer === undefined) {
471
+ return;
472
+ }
473
+
474
+ const importers = state.entryImporters.get(resourcePath) ?? new Set<string>();
475
+ importers.add(importerKey(importer));
476
+ state.entryImporters.set(resourcePath, importers);
477
+ };
478
+
479
+ const trackBundledSpecifier = (state: PluginState, importer: string | undefined, source: string): void => {
480
+ if (importer === undefined) {
481
+ return;
482
+ }
483
+
484
+ const key = importerKey(importer);
485
+ const specifiers = state.bundledSpecifiers.get(key) ?? new Set<string>();
486
+ specifiers.add(source);
487
+ state.bundledSpecifiers.set(key, specifiers);
488
+ };
489
+
490
+ const claimIcon = (state: PluginState, iconIdentity: string, importer: string | undefined): void => {
491
+ if (importer === undefined) {
492
+ return;
493
+ }
494
+
495
+ const importers = state.iconImporters.get(iconIdentity) ?? new Set<string>();
496
+ importers.add(importerKey(importer));
497
+ state.iconImporters.set(iconIdentity, importers);
498
+ };
499
+
500
+ const createResourceEntry = (sourcePath: string, resourcePath: string): ResourceEntry => ({
501
+ sourcePath,
502
+ stagedRelPath: resourcePath.replace(/^\/+/, ""),
503
+ resourcePath,
504
+ });
505
+
506
+ const activateEntry = (state: PluginState, entry: ResourceEntry, importer?: string): ResourceEntry => {
507
+ const existing = state.entries.get(entry.resourcePath);
508
+
509
+ if (existing !== undefined) {
510
+ if (existing.sourcePath !== entry.sourcePath) {
511
+ throw new Error(
512
+ `${existing.sourcePath} and ${entry.sourcePath} both resolve to the GResource path ` +
513
+ entry.resourcePath,
514
+ );
515
+ }
516
+
517
+ claimEntry(state, entry.resourcePath, importer);
158
518
 
159
- if (existing) {
160
519
  return existing;
161
520
  }
162
521
 
163
- const resourcePath = `${state.prefix}/${rel}`;
522
+ state.entries.set(entry.resourcePath, entry);
523
+ state.sourcePaths.add(entry.sourcePath);
524
+ claimEntry(state, entry.resourcePath, importer);
164
525
 
165
- const entry: ResourceEntry = {
166
- sourcePath: absPath,
167
- stagedRelPath: resourcePath.replace(/^\/+/, ""),
168
- resourcePath,
169
- };
526
+ return entry;
527
+ };
528
+
529
+ const isDeclarativeImporter = (state: PluginState, importer: string | undefined): importer is string =>
530
+ !state.isBuild && state.isV2 && importer !== undefined;
531
+
532
+ const setSourceAvailability = (state: PluginState, file: string, isAvailable: boolean): void => {
533
+ if (isAvailable) {
534
+ state.missingSourcePaths.delete(file);
535
+
536
+ return;
537
+ }
538
+
539
+ state.missingSourcePaths.add(file);
540
+ };
541
+
542
+ const isDeclaredSourceAvailable = (state: PluginState, sourcePath: string): boolean => {
543
+ const isAvailable = existsSync(sourcePath);
544
+ setSourceAvailability(state, sourcePath, isAvailable);
545
+
546
+ return isAvailable;
547
+ };
548
+
549
+ const declareEntry = (state: PluginState, importer: string, entry: ResourceEntry): ResourceEntry => {
550
+ const key = importerKey(importer);
551
+ const declarations = state.declaredEntries.get(key) ?? new Map<string, ResourceEntry>();
552
+ const existing = declarations.get(entry.resourcePath);
553
+
554
+ if (existing !== undefined && existing.sourcePath !== entry.sourcePath) {
555
+ throw new Error(
556
+ `${existing.sourcePath} and ${entry.sourcePath} both resolve to the GResource path ` +
557
+ entry.resourcePath,
558
+ );
559
+ }
170
560
 
171
- state.entries.set(absPath, entry);
172
- state.sourcePaths.add(absPath);
561
+ const declared = existing ?? entry;
562
+ declarations.set(entry.resourcePath, declared);
563
+ state.declaredEntries.set(key, declarations);
564
+ state.server?.watcher.add(dirname(declared.sourcePath));
565
+
566
+ return declared;
567
+ };
568
+
569
+ const declareIcon = (
570
+ state: PluginState,
571
+ importer: string,
572
+ iconIdentity: string,
573
+ entry: ResourceEntry,
574
+ ): void => {
575
+ const key = importerKey(importer);
576
+ const declarations = state.declaredIcons.get(key) ?? new Map<string, ResourceEntry>();
577
+ const existing = declarations.get(iconIdentity);
578
+
579
+ if (existing !== undefined && existing.sourcePath !== entry.sourcePath) {
580
+ throw new Error(
581
+ `${existing.sourcePath} and ${entry.sourcePath} both claim the themed icon ${iconIdentity}`,
582
+ );
583
+ }
584
+
585
+ declarations.set(iconIdentity, existing ?? entry);
586
+ state.declaredIcons.set(key, declarations);
587
+ };
588
+
589
+ const assertIconIdentityAvailable = (
590
+ state: PluginState,
591
+ iconIdentity: string,
592
+ sourcePath: string,
593
+ ): void => {
594
+ const existing = state.iconOwners.get(iconIdentity);
595
+
596
+ if (existing !== undefined && existing.sourcePath !== sourcePath) {
597
+ throw new Error(
598
+ `${existing.sourcePath} and ${sourcePath} both claim the themed icon ${iconIdentity}`,
599
+ );
600
+ }
601
+ };
602
+
603
+ const declareResolvedIcon = (
604
+ state: PluginState,
605
+ importer: string | undefined,
606
+ iconIdentity: string,
607
+ entry: ResourceEntry,
608
+ ): void => {
609
+ if (isDeclarativeImporter(state, importer)) {
610
+ declareIcon(state, importer, iconIdentity, entry);
611
+ }
612
+ };
613
+
614
+ const activateIcon = (
615
+ state: PluginState,
616
+ importer: string | undefined,
617
+ iconIdentity: string,
618
+ entry: ResourceEntry,
619
+ ): void => {
620
+ state.iconOwners.set(iconIdentity, entry);
621
+ claimIcon(state, iconIdentity, importer);
622
+ };
623
+
624
+ const registerEntry = (
625
+ state: PluginState,
626
+ absPath: string,
627
+ rel: string,
628
+ importer?: string,
629
+ ): ResourceEntry => {
630
+ const resourcePath = validateResourcePath(rel.startsWith("/") ? rel : `${state.prefix}/${rel}`);
631
+ let entry = createResourceEntry(absPath, resourcePath);
632
+
633
+ if (isDeclarativeImporter(state, importer)) {
634
+ entry = declareEntry(state, importer, entry);
635
+
636
+ if (!isImporterActive(state, importer) || !isDeclaredSourceAvailable(state, absPath)) {
637
+ return entry;
638
+ }
639
+ }
640
+
641
+ return activateEntry(state, entry, importer);
642
+ };
643
+
644
+ const registerIconEntry = (
645
+ state: PluginState,
646
+ sourcePath: string,
647
+ derived: DerivedIconEntry,
648
+ importer: string | undefined,
649
+ ): ResourceEntry => {
650
+ const shouldActivate = !isDeclarativeImporter(state, importer) ||
651
+ (isImporterActive(state, importer) && isDeclaredSourceAvailable(state, sourcePath));
652
+
653
+ if (shouldActivate) {
654
+ assertIconIdentityAvailable(state, derived.iconIdentity, sourcePath);
655
+ }
656
+
657
+ const entry = registerEntry(state, sourcePath, derived.resourcePath, importer);
658
+ declareResolvedIcon(state, importer, derived.iconIdentity, entry);
659
+
660
+ if (shouldActivate) {
661
+ activateIcon(state, importer, derived.iconIdentity, entry);
662
+ }
173
663
 
174
664
  return entry;
175
665
  };
176
666
 
177
667
  const isTrackedSource = (state: PluginState, file: string): boolean => state.sourcePaths.has(file);
178
668
 
179
- const resolvedAssetId = (
180
- resolved: { id: string; external: boolean | string } | null,
181
- source: string,
182
- ): string | undefined => {
669
+ const hasSourceEntry = (entries: Map<string, ResourceEntry>, file: string): boolean => {
670
+ for (const entry of entries.values()) {
671
+ if (entry.sourcePath === file) {
672
+ return true;
673
+ }
674
+ }
675
+
676
+ return false;
677
+ };
678
+
679
+ const hasDeclaredSource = (
680
+ declarations: Map<string, Map<string, ResourceEntry>>,
681
+ file: string,
682
+ ): boolean => {
683
+ for (const entries of declarations.values()) {
684
+ if (hasSourceEntry(entries, file)) {
685
+ return true;
686
+ }
687
+ }
688
+
689
+ return false;
690
+ };
691
+
692
+ const isDeclaredSource = (state: PluginState, file: string): boolean =>
693
+ hasDeclaredSource(state.declaredEntries, file) || hasDeclaredSource(state.declaredIcons, file);
694
+
695
+ const virtualAssetId = (entry: ResourceEntry): string =>
696
+ toVirtualId(entry.sourcePath) + REL_SEPARATOR + entry.resourcePath;
697
+
698
+ const virtualIconId = ({ entry, iconName }: ResolvedIconEntry): string =>
699
+ virtualAssetId(entry) + ICON_NAME_SEPARATOR + iconName;
700
+
701
+ const resolveAssetSource = async (
702
+ ctx: ResolveContext,
703
+ request: { assetSource: string; importer: string | undefined; options: Record<string, unknown> | undefined },
704
+ ): Promise<string | undefined> => {
705
+ const resolved = await ctx.resolve(request.assetSource, request.importer, {
706
+ ...request.options,
707
+ skipSelf: true,
708
+ });
709
+
710
+ if (!resolved || resolved.external) {
711
+ return undefined;
712
+ }
713
+
714
+ const sourcePath = stripQuery(resolved.id);
715
+
716
+ if (!isAbsolute(sourcePath)) {
717
+ throw new Error(`${JSON.stringify(request.assetSource)} did not resolve to a file-backed asset`);
718
+ }
719
+
720
+ return sourcePath;
721
+ };
722
+
723
+ const resolveAssetEntry = async (
724
+ ctx: ResolveContext,
725
+ state: PluginState,
726
+ request: AssetResolveRequest,
727
+ ): Promise<ResourceEntry | undefined> => {
728
+ const sourcePath = await resolveAssetSource(ctx, request);
729
+
730
+ if (sourcePath === undefined) {
731
+ return undefined;
732
+ }
733
+
734
+ return registerEntry(
735
+ state,
736
+ sourcePath,
737
+ request.resourcePath === null
738
+ ? derivedResourcePath(state, sourcePath)
739
+ : validateResourcePath(request.resourcePath),
740
+ request.importer,
741
+ );
742
+ };
743
+
744
+ const resolveIconEntry = async (
745
+ ctx: ResolveContext,
746
+ state: PluginState,
747
+ request: IconResolveRequest,
748
+ ): Promise<ResolvedIconEntry | undefined> => {
749
+ const sourcePath = await resolveAssetSource(ctx, request);
750
+
751
+ if (sourcePath === undefined) {
752
+ return undefined;
753
+ }
754
+
755
+ const derived = derivedIconEntry(state, sourcePath, request.iconName);
756
+
757
+ return {
758
+ entry: registerIconEntry(state, sourcePath, derived, request.importer),
759
+ iconName: derived.iconName,
760
+ };
761
+ };
762
+
763
+ const resolveLegacyAssetEntry = async (
764
+ ctx: ResolveContext,
765
+ state: PluginState,
766
+ request: LegacyResolveRequest,
767
+ ): Promise<ResourceEntry | undefined> => {
768
+ const resolved = await ctx.resolve(request.source, request.importer, { ...request.options, skipSelf: true });
769
+
183
770
  if (!resolved || resolved.external) {
184
771
  return undefined;
185
772
  }
186
773
 
187
- return toVirtualId(resolved.id) + REL_SEPARATOR + source.slice(DATA_PREFIX.length);
774
+ const sourcePath = stripQuery(resolved.id);
775
+
776
+ if (!isAbsolute(sourcePath)) {
777
+ return undefined;
778
+ }
779
+
780
+ return registerEntry(state, sourcePath, request.source.slice(DATA_PREFIX.length));
188
781
  };
189
782
 
190
783
  const loadInitModule = (state: PluginState): string => {
191
- if (!state.isBuild) {
192
- ensureStagingDir(state);
784
+ if (state.isBuild) {
785
+ if (state.bundleReferenceId === null) {
786
+ throw new Error("Cannot render the GResource loader before its bundle has been emitted");
787
+ }
788
+
789
+ return renderInitModule({ isBuild: true, bundleReferenceId: state.bundleReferenceId });
193
790
  }
194
791
 
195
- return renderInitModule({ isBuild: state.isBuild, devBundlePath: state.devBundlePath });
792
+ ensureStagingDir(state);
793
+
794
+ return renderInitModule({ isBuild: false, devBundlePath: state.devBundlePath });
196
795
  };
197
796
 
198
- const loadAssetModule = (state: PluginState, virtualId: string): string => {
797
+ const virtualAssetMetadata = (virtualId: string): { absPath: string; iconName: string | null; rel: string } => {
199
798
  const rest = fromVirtualId(virtualId);
200
799
  const separatorIndex = rest.indexOf(REL_SEPARATOR);
201
- const absPath = rest.slice(0, separatorIndex);
202
- const rel = rest.slice(separatorIndex + REL_SEPARATOR.length);
203
- const entry = registerEntry(state, absPath, rel);
204
- const uri = `resource://${entry.resourcePath}`;
800
+ const relAndMetadata = rest.slice(separatorIndex + REL_SEPARATOR.length);
801
+ const iconSeparatorIndex = relAndMetadata.indexOf(ICON_NAME_SEPARATOR);
802
+
803
+ return {
804
+ absPath: rest.slice(0, separatorIndex),
805
+ iconName: iconSeparatorIndex === -1
806
+ ? null
807
+ : relAndMetadata.slice(iconSeparatorIndex + ICON_NAME_SEPARATOR.length),
808
+ rel: iconSeparatorIndex === -1 ? relAndMetadata : relAndMetadata.slice(0, iconSeparatorIndex),
809
+ };
810
+ };
205
811
 
206
- if (!state.isBuild) {
812
+ const resourcePathExport = (entry: ResourceEntry, iconName: string | null): string[] =>
813
+ iconName === null ? [`export const ${RESOURCE_PATH_EXPORT} = ${JSON.stringify(entry.resourcePath)};`] : [];
814
+
815
+ const assetModuleImports = (iconName: string | null): string[] => {
816
+ const initBindings = iconName === null
817
+ ? "ensureRegistered"
818
+ : `ensureRegistered, ${REGISTER_REFRESH_EXPORT}`;
819
+
820
+ return [
821
+ `import { ${initBindings} } from ${JSON.stringify(VIRTUAL_INIT)};`,
822
+ ...(iconName === null
823
+ ? []
824
+ : [
825
+ "import { Display } from \"@gtkx/gi/gdk\";",
826
+ "import { IconTheme } from \"@gtkx/gi/gtk\";",
827
+ ]),
828
+ ];
829
+ };
830
+
831
+ const iconThemeRegistration = (state: PluginState, iconName: string | null): string[] => {
832
+ if (iconName === null) {
833
+ return [];
834
+ }
835
+
836
+ const resourceIconPath = `${state.prefix}/icons`;
837
+
838
+ return [
839
+ `const resourceIconPath = ${JSON.stringify(resourceIconPath)};`,
840
+ "function refreshIconTheme() {",
841
+ " const display = Display.getDefault();",
842
+ " if (!display) return;",
843
+ " const theme = IconTheme.getForDisplay(display);",
844
+ " const paths = theme.getResourcePath() ?? [];",
845
+ " theme.setResourcePath(paths.includes(resourceIconPath) ? [...paths] : [...paths, resourceIconPath]);",
846
+ "}",
847
+ `${REGISTER_REFRESH_EXPORT}(resourceIconPath, refreshIconTheme);`,
848
+ ];
849
+ };
850
+
851
+ const loadedResourceEntry = (
852
+ state: PluginState,
853
+ sourcePath: string,
854
+ resourcePath: string,
855
+ ): { entry: ResourceEntry; isRegistered: boolean } => {
856
+ const registered = state.entries.get(resourcePath);
857
+
858
+ if (registered === undefined) {
859
+ return { entry: createResourceEntry(sourcePath, resourcePath), isRegistered: false };
860
+ }
861
+
862
+ if (registered.sourcePath !== sourcePath) {
863
+ throw new Error(
864
+ `${registered.sourcePath} and ${sourcePath} both resolve to the GResource path ${resourcePath}`,
865
+ );
866
+ }
867
+
868
+ return { entry: registered, isRegistered: true };
869
+ };
870
+
871
+ const compileLoadedDevAsset = (state: PluginState, isRegistered: boolean): void => {
872
+ if (isRegistered && !state.isBuild) {
207
873
  registerDevAsset(state);
208
874
  }
875
+ };
876
+
877
+ const loadAssetModule = (state: PluginState, virtualId: string): string => {
878
+ const { absPath, iconName, rel } = virtualAssetMetadata(virtualId);
879
+ const resourcePath = validateResourcePath(rel);
880
+ const { entry, isRegistered } = loadedResourceEntry(state, absPath, resourcePath);
881
+ const uri = `resource://${entry.resourcePath}`;
882
+ const defaultValue = iconName ?? (state.isV2 ? entry.resourcePath : uri);
883
+ compileLoadedDevAsset(state, isRegistered);
209
884
 
210
885
  return [
211
- `import { ensureRegistered } from ${JSON.stringify(VIRTUAL_INIT)};`,
886
+ ...assetModuleImports(iconName),
212
887
  "ensureRegistered();",
213
- `export default ${JSON.stringify(uri)};`,
214
- `export const ${RESOURCE_PATH_EXPORT} = ${JSON.stringify(entry.resourcePath)};`,
888
+ ...iconThemeRegistration(state, iconName),
889
+ `export default ${JSON.stringify(defaultValue)};`,
890
+ ...resourcePathExport(entry, iconName),
215
891
  ].join("\n");
216
892
  };
217
893
 
218
- const emitBuildBundle = (ctx: AssetEmitter, state: PluginState): void => {
219
- if (!state.isBuild || state.entries.size === 0) {
894
+ const finalizeBuildBundle = (
895
+ ctx: Pick<Rolldown.PluginContext, "getFileName">,
896
+ bundle: Rolldown.OutputBundle,
897
+ state: PluginState,
898
+ ): void => {
899
+ if (!state.isBuild || state.bundleReferenceId === null) {
900
+ return;
901
+ }
902
+
903
+ const fileName = ctx.getFileName(state.bundleReferenceId);
904
+ const output = bundle[fileName];
905
+
906
+ if (output?.type !== "asset") {
907
+ throw new Error(`Cannot find the emitted ${BUNDLE_FILENAME} asset`);
908
+ }
909
+
910
+ if (state.entries.size === 0) {
911
+ Reflect.deleteProperty(bundle, fileName);
912
+
220
913
  return;
221
914
  }
222
915
 
223
916
  const compiled = withStagingDir("resources-out", (outDir) => compileBundle(state, join(outDir, BUNDLE_FILENAME)));
224
- ctx.emitFile({ type: "asset", fileName: BUNDLE_FILENAME, source: compiled });
917
+ output.source = compiled;
225
918
  info(`Compiled ${String(state.entries.size)} resource(s) into ${BUNDLE_FILENAME}`);
226
919
  };
227
920
 
228
- const refreshDevRegistration = async (state: PluginState): Promise<void> => {
921
+ const refreshDevRegistration = (state: PluginState): void => {
229
922
  compileDevBundle(state);
230
923
 
231
924
  try {
232
- await reregisterDevBundle(state);
925
+ refreshRegisteredDevBundle(state);
233
926
  } catch (error_) {
234
927
  error("Failed to refresh GResource bundle:", error_);
235
928
  }
236
929
  };
237
930
 
931
+ const isSameSpecifiers = (left: Set<string> | undefined, right: Set<string>): boolean =>
932
+ left === undefined
933
+ ? right.size === 0
934
+ : left.size === right.size && [...right].every((source) => left.has(source));
935
+
936
+ const bundledSpecifiersIn = (imports: SourceImport[]): Set<string> =>
937
+ new Set(
938
+ imports
939
+ .filter((entry) =>
940
+ parseResourceSpecifier(entry.source) !== null || parseIconSpecifier(entry.source) !== null)
941
+ .map((entry) => entry.source),
942
+ );
943
+
944
+ const isSourceModule = (file: string): boolean =>
945
+ !file.endsWith(".d.ts") && !file.endsWith(".d.cts") && !file.endsWith(".d.mts") &&
946
+ SOURCE_LANGUAGES.has(extname(file).toLowerCase());
947
+
948
+ const isBundledSource = (source: string): boolean =>
949
+ parseResourceSpecifier(source) !== null || parseIconSpecifier(source) !== null;
950
+
951
+ const validateUnbundledV2Source = (source: string): void => {
952
+ if (isLegacyDataSpecifier(source)) {
953
+ throw new Error(`${JSON.stringify(source)} uses the legacy #data asset form`);
954
+ }
955
+
956
+ rejectInvalidV2Specifier(source);
957
+ };
958
+
959
+ const resolvedModuleDependency = async (
960
+ ctx: ResolveContext,
961
+ source: string,
962
+ importer: string,
963
+ ): Promise<string | null> => {
964
+ if (isBundledSource(source)) {
965
+ return null;
966
+ }
967
+
968
+ validateUnbundledV2Source(source);
969
+ const resolved = await ctx.resolve(source, importer, { skipSelf: true });
970
+
971
+ if (resolved === null) {
972
+ throw new Error(`${JSON.stringify(source)} could not be resolved from ${importer}`);
973
+ }
974
+
975
+ return resolved.external ? null : importerKey(resolved.id);
976
+ };
977
+
978
+ const resolvedModuleDependencies = async (
979
+ ctx: ResolveContext,
980
+ imports: SourceImport[],
981
+ importer: string,
982
+ ): Promise<Set<string>> => {
983
+ const dependencies: Set<string> = new Set();
984
+
985
+ for (const entry of imports) {
986
+ const dependency = await resolvedModuleDependency(ctx, entry.source, importer);
987
+
988
+ if (dependency !== null) {
989
+ dependencies.add(dependency);
990
+ }
991
+ }
992
+
993
+ return dependencies;
994
+ };
995
+
996
+ const importerReconciliation = async (
997
+ ctx: ResolveContext,
998
+ state: PluginState,
999
+ id: string,
1000
+ ): Promise<ImporterReconciliation | null> => {
1001
+ const file = stripQuery(id);
1002
+ const key = importerKey(file);
1003
+
1004
+ if (!state.isV2 || state.isBuild || !isSourceModule(file)) {
1005
+ return null;
1006
+ }
1007
+
1008
+ const imports = parseRuntimeImportsIn(file);
1009
+
1010
+ if (imports === null) {
1011
+ return null;
1012
+ }
1013
+
1014
+ return {
1015
+ currentBundled: bundledSpecifiersIn(imports),
1016
+ currentDependencies: await resolvedModuleDependencies(ctx, imports, file),
1017
+ file,
1018
+ imports,
1019
+ key,
1020
+ previousBundled: state.bundledSpecifiers.get(key),
1021
+ previousDependencies: state.moduleDependencies.get(key),
1022
+ };
1023
+ };
1024
+
1025
+ const moduleNodeKey = (module: ModuleNode): string => importerKey(module.id ?? module.file ?? module.url);
1026
+
1027
+ const moduleNodeFor = (state: PluginState, id: string): ModuleNode | undefined => {
1028
+ const server = state.server;
1029
+
1030
+ if (server === null) {
1031
+ return undefined;
1032
+ }
1033
+
1034
+ return server.moduleGraph.getModuleById(id) ?? server.moduleGraph.getModuleById(importerKey(id));
1035
+ };
1036
+
1037
+ const restoreDependencies = (pruned: Set<string>, current: Set<string>): void => {
1038
+ for (const dependency of current) {
1039
+ pruned.delete(dependency);
1040
+ }
1041
+ };
1042
+
1043
+ const pruneRemovedDependencies = (
1044
+ pruned: Set<string>,
1045
+ previous: Set<string> | undefined,
1046
+ current: Set<string>,
1047
+ ): void => {
1048
+ if (previous === undefined) {
1049
+ return;
1050
+ }
1051
+
1052
+ for (const dependency of previous) {
1053
+ if (!current.has(dependency)) {
1054
+ pruned.add(dependency);
1055
+ }
1056
+ }
1057
+ };
1058
+
1059
+ const storePrunedDependencies = (state: PluginState, key: string, pruned: Set<string>): void => {
1060
+ if (pruned.size === 0) {
1061
+ state.prunedDependencies.delete(key);
1062
+
1063
+ return;
1064
+ }
1065
+
1066
+ state.prunedDependencies.set(key, pruned);
1067
+ };
1068
+
1069
+ const updatePrunedDependencies = (
1070
+ state: PluginState,
1071
+ key: string,
1072
+ previous: Set<string> | undefined,
1073
+ current: Set<string>,
1074
+ ): void => {
1075
+ const pruned = state.prunedDependencies.get(key) ?? new Set<string>();
1076
+ restoreDependencies(pruned, current);
1077
+ pruneRemovedDependencies(pruned, previous, current);
1078
+ storePrunedDependencies(state, key, pruned);
1079
+ };
1080
+
1081
+ const isPrunedDependency = (state: PluginState, importer: ModuleNode, imported: ModuleNode): boolean =>
1082
+ state.prunedDependencies.get(moduleNodeKey(importer))?.has(moduleNodeKey(imported)) === true;
1083
+
1084
+ const fallbackRootModules = (state: PluginState): string[] => {
1085
+ const roots: string[] = [];
1086
+
1087
+ for (const key of state.moduleDependencies.keys()) {
1088
+ if (moduleNodeFor(state, key)?.importers.size === 0) {
1089
+ roots.push(key);
1090
+ }
1091
+ }
1092
+
1093
+ return roots;
1094
+ };
1095
+
1096
+ const enqueueCurrentDependencies = (state: PluginState, key: string, pending: string[]): void => {
1097
+ const dependencies = state.moduleDependencies.get(key) ?? [];
1098
+
1099
+ for (const dependency of dependencies) {
1100
+ pending.push(dependency);
1101
+ }
1102
+ };
1103
+
1104
+ const enqueueGraphDependencies = (state: PluginState, key: string, pending: string[]): void => {
1105
+ const module = moduleNodeFor(state, key);
1106
+
1107
+ if (module === undefined) {
1108
+ return;
1109
+ }
1110
+
1111
+ for (const imported of module.importedModules) {
1112
+ if (!isPrunedDependency(state, module, imported)) {
1113
+ pending.push(moduleNodeKey(imported));
1114
+ }
1115
+ }
1116
+ };
1117
+
1118
+ const visitActiveImporter = (
1119
+ state: PluginState,
1120
+ active: Set<string>,
1121
+ pending: string[],
1122
+ key: string,
1123
+ ): void => {
1124
+ if (active.has(key)) {
1125
+ return;
1126
+ }
1127
+
1128
+ active.add(key);
1129
+ enqueueCurrentDependencies(state, key, pending);
1130
+ enqueueGraphDependencies(state, key, pending);
1131
+ };
1132
+
1133
+ const activeImporterKeys = (state: PluginState): Set<string> => {
1134
+ const active: Set<string> = new Set();
1135
+ const roots = state.entryModule === null ? fallbackRootModules(state) : [state.entryModule];
1136
+ const pending = [...roots];
1137
+
1138
+ while (pending.length > 0) {
1139
+ const key = pending.pop();
1140
+
1141
+ if (key === undefined || active.has(key)) {
1142
+ continue;
1143
+ }
1144
+
1145
+ visitActiveImporter(state, active, pending, key);
1146
+ }
1147
+
1148
+ return active;
1149
+ };
1150
+
1151
+ const isImporterActive = (state: PluginState, importer: string): boolean =>
1152
+ activeImporterKeys(state).has(importerKey(importer));
1153
+
1154
+ const activateDeclaredIcon = (
1155
+ state: PluginState,
1156
+ importer: string,
1157
+ iconIdentity: string,
1158
+ entry: ResourceEntry,
1159
+ ): void => {
1160
+ const existing = state.iconOwners.get(iconIdentity);
1161
+
1162
+ if (existing !== undefined && existing.sourcePath !== entry.sourcePath) {
1163
+ throw new Error(
1164
+ `${existing.sourcePath} and ${entry.sourcePath} both claim the themed icon ${iconIdentity}`,
1165
+ );
1166
+ }
1167
+
1168
+ const activeEntry = activateEntry(state, entry, importer);
1169
+ state.iconOwners.set(iconIdentity, activeEntry);
1170
+ claimIcon(state, iconIdentity, importer);
1171
+ };
1172
+
1173
+ const activateDeclaredEntries = (state: PluginState, importer: string): void => {
1174
+ const entries = state.declaredEntries.get(importer)?.values() ?? [];
1175
+
1176
+ for (const entry of entries) {
1177
+ if (isDeclaredSourceAvailable(state, entry.sourcePath)) {
1178
+ activateEntry(state, entry, importer);
1179
+ }
1180
+ }
1181
+ };
1182
+
1183
+ const activateDeclaredIcons = (state: PluginState, importer: string): void => {
1184
+ const icons = state.declaredIcons.get(importer) ?? [];
1185
+
1186
+ for (const [iconIdentity, entry] of icons) {
1187
+ if (isDeclaredSourceAvailable(state, entry.sourcePath)) {
1188
+ activateDeclaredIcon(state, importer, iconIdentity, entry);
1189
+ }
1190
+ }
1191
+ };
1192
+
1193
+ const activateDeclaredClaims = (state: PluginState, active: Set<string>): void => {
1194
+ for (const importer of sortStrings(active)) {
1195
+ activateDeclaredEntries(state, importer);
1196
+ activateDeclaredIcons(state, importer);
1197
+ }
1198
+ };
1199
+
1200
+ const rebuildActiveClaims = (state: PluginState): void => {
1201
+ const previous = {
1202
+ entries: state.entries,
1203
+ entryImporters: state.entryImporters,
1204
+ iconImporters: state.iconImporters,
1205
+ iconOwners: state.iconOwners,
1206
+ sourcePaths: state.sourcePaths,
1207
+ };
1208
+
1209
+ state.entries = new Map();
1210
+ state.entryImporters = new Map();
1211
+ state.iconImporters = new Map();
1212
+ state.iconOwners = new Map();
1213
+ state.sourcePaths = new Set();
1214
+
1215
+ try {
1216
+ activateDeclaredClaims(state, activeImporterKeys(state));
1217
+ } catch (error_) {
1218
+ state.entries = previous.entries;
1219
+ state.entryImporters = previous.entryImporters;
1220
+ state.iconImporters = previous.iconImporters;
1221
+ state.iconOwners = previous.iconOwners;
1222
+ state.sourcePaths = previous.sourcePaths;
1223
+ throw error_;
1224
+ }
1225
+ };
1226
+
1227
+ const forgetImporterDeclarations = (state: PluginState, importer: string): void => {
1228
+ state.declaredEntries.delete(importer);
1229
+ state.declaredIcons.delete(importer);
1230
+ };
1231
+
1232
+ const cloneStringSetMap = (source: Map<string, Set<string>>): Map<string, Set<string>> => {
1233
+ const clone: Map<string, Set<string>> = new Map();
1234
+
1235
+ for (const [key, values] of source) {
1236
+ clone.set(key, new Set(values));
1237
+ }
1238
+
1239
+ return clone;
1240
+ };
1241
+
1242
+ const cloneDeclarationMap = (
1243
+ source: Map<string, Map<string, ResourceEntry>>,
1244
+ ): Map<string, Map<string, ResourceEntry>> => {
1245
+ const clone: Map<string, Map<string, ResourceEntry>> = new Map();
1246
+
1247
+ for (const [key, declarations] of source) {
1248
+ clone.set(key, new Map(declarations));
1249
+ }
1250
+
1251
+ return clone;
1252
+ };
1253
+
1254
+ const reconciliationCandidate = (state: PluginState): PluginState => ({
1255
+ ...state,
1256
+ bundledSpecifiers: cloneStringSetMap(state.bundledSpecifiers),
1257
+ moduleDependencies: cloneStringSetMap(state.moduleDependencies),
1258
+ prunedDependencies: cloneStringSetMap(state.prunedDependencies),
1259
+ declaredEntries: cloneDeclarationMap(state.declaredEntries),
1260
+ declaredIcons: cloneDeclarationMap(state.declaredIcons),
1261
+ missingSourcePaths: new Set(state.missingSourcePaths),
1262
+ });
1263
+
1264
+ const storeBundledSpecifiers = (state: PluginState, importer: string, specifiers: Set<string>): void => {
1265
+ if (specifiers.size === 0) {
1266
+ state.bundledSpecifiers.delete(importer);
1267
+
1268
+ return;
1269
+ }
1270
+
1271
+ state.bundledSpecifiers.set(importer, specifiers);
1272
+ };
1273
+
1274
+ const applyImporterGraph = (state: PluginState, reconciliation: ImporterReconciliation): void => {
1275
+ updatePrunedDependencies(
1276
+ state,
1277
+ reconciliation.key,
1278
+ reconciliation.previousDependencies,
1279
+ reconciliation.currentDependencies,
1280
+ );
1281
+
1282
+ state.moduleDependencies.set(reconciliation.key, reconciliation.currentDependencies);
1283
+ storeBundledSpecifiers(state, reconciliation.key, reconciliation.currentBundled);
1284
+ };
1285
+
1286
+ const declareReconciledResource = async (
1287
+ ctx: ResolveContext,
1288
+ state: PluginState,
1289
+ importer: string,
1290
+ source: string,
1291
+ ): Promise<void> => {
1292
+ const resourceSpecifier = parseResourceSpecifier(source);
1293
+
1294
+ if (resourceSpecifier === null) {
1295
+ return;
1296
+ }
1297
+
1298
+ const entry = await resolveAssetEntry(ctx, state, {
1299
+ assetSource: resourceSpecifier.assetSource,
1300
+ importer,
1301
+ resourcePath: resourceSpecifier.resourcePath,
1302
+ options: undefined,
1303
+ });
1304
+
1305
+ if (entry === undefined || state.missingSourcePaths.has(entry.sourcePath)) {
1306
+ throw new Error(`${JSON.stringify(source)} did not resolve to a bundled resource`);
1307
+ }
1308
+ };
1309
+
1310
+ const declareReconciledIcon = async (
1311
+ ctx: ResolveContext,
1312
+ state: PluginState,
1313
+ importer: string,
1314
+ source: string,
1315
+ ): Promise<void> => {
1316
+ const iconSpecifier = parseIconSpecifier(source);
1317
+
1318
+ if (iconSpecifier === null) {
1319
+ return;
1320
+ }
1321
+
1322
+ const entry = await resolveIconEntry(ctx, state, {
1323
+ assetSource: iconSpecifier.assetSource,
1324
+ iconName: iconSpecifier.iconName,
1325
+ importer,
1326
+ options: undefined,
1327
+ });
1328
+
1329
+ if (entry === undefined || state.missingSourcePaths.has(entry.entry.sourcePath)) {
1330
+ throw new Error(`${JSON.stringify(source)} did not resolve to a bundled icon`);
1331
+ }
1332
+ };
1333
+
1334
+ const declareReconciledSource = async (
1335
+ ctx: ResolveContext,
1336
+ state: PluginState,
1337
+ importer: string,
1338
+ source: string,
1339
+ ): Promise<void> => {
1340
+ await declareReconciledResource(ctx, state, importer, source);
1341
+ await declareReconciledIcon(ctx, state, importer, source);
1342
+ };
1343
+
1344
+ const declareReconciledImports = async (
1345
+ ctx: ResolveContext,
1346
+ state: PluginState,
1347
+ reconciliation: ImporterReconciliation,
1348
+ ): Promise<void> => {
1349
+ for (const entry of reconciliation.imports) {
1350
+ if (isBundledSource(entry.source)) {
1351
+ await declareReconciledSource(ctx, state, reconciliation.file, entry.source);
1352
+ }
1353
+ }
1354
+ };
1355
+
1356
+ const prepareReconciliation = async (
1357
+ ctx: ResolveContext,
1358
+ state: PluginState,
1359
+ reconciliation: ImporterReconciliation,
1360
+ ): Promise<PluginState> => {
1361
+ const candidate = reconciliationCandidate(state);
1362
+
1363
+ const isBundledChanged = !isSameSpecifiers(
1364
+ reconciliation.previousBundled,
1365
+ reconciliation.currentBundled,
1366
+ );
1367
+
1368
+ if (isBundledChanged) {
1369
+ forgetImporterDeclarations(candidate, reconciliation.key);
1370
+ }
1371
+
1372
+ applyImporterGraph(candidate, reconciliation);
1373
+ rebuildActiveClaims(candidate);
1374
+
1375
+ if (isBundledChanged) {
1376
+ await declareReconciledImports(ctx, candidate, reconciliation);
1377
+ }
1378
+
1379
+ return candidate;
1380
+ };
1381
+
1382
+ const commitReconciliation = (state: PluginState, candidate: PluginState): void => {
1383
+ state.entries = candidate.entries;
1384
+ state.entryImporters = candidate.entryImporters;
1385
+ state.bundledSpecifiers = candidate.bundledSpecifiers;
1386
+ state.moduleDependencies = candidate.moduleDependencies;
1387
+ state.prunedDependencies = candidate.prunedDependencies;
1388
+ state.declaredEntries = candidate.declaredEntries;
1389
+ state.declaredIcons = candidate.declaredIcons;
1390
+ state.missingSourcePaths = candidate.missingSourcePaths;
1391
+ state.sourcePaths = candidate.sourcePaths;
1392
+ state.iconOwners = candidate.iconOwners;
1393
+ state.iconImporters = candidate.iconImporters;
1394
+ };
1395
+
1396
+ const reconcileV2Importer = async (ctx: ResolveContext, state: PluginState, id: string): Promise<void> => {
1397
+ const reconciliation = await importerReconciliation(ctx, state, id);
1398
+
1399
+ if (reconciliation === null) {
1400
+ return;
1401
+ }
1402
+
1403
+ const previousSignature = entriesSignature(state);
1404
+ const candidate = await prepareReconciliation(ctx, state, reconciliation);
1405
+ commitReconciliation(state, candidate);
1406
+
1407
+ if (entriesSignature(state) !== previousSignature) {
1408
+ refreshDevRegistration(state);
1409
+ }
1410
+ };
1411
+
238
1412
  const resolveResourceConfig = async (state: PluginState, config: UserConfig, loadConfig: ConfigLoader) => {
239
- const { applicationId } = await loadConfig.resolve(config.root ?? process.cwd());
240
- state.prefix = deriveResourcePrefix(applicationId);
1413
+ const loaded = await loadConfig.load(config.root ?? process.cwd());
1414
+ state.prefix = resourceBasePath(loaded.config.applicationId);
1415
+ state.root = loaded.root;
1416
+ state.isV2 = loaded.config.future?.v2ResourceImports === true;
241
1417
 
242
1418
  return {
243
1419
  assetsInclude: [ASSET_RE],
244
1420
  };
245
1421
  };
246
1422
 
247
- const refreshTrackedSource = async (state: PluginState, file: string): Promise<void> => {
1423
+ const refreshTrackedSource = (state: PluginState, file: string): void => {
248
1424
  if (!isTrackedSource(state, file)) {
249
1425
  return;
250
1426
  }
251
1427
 
252
1428
  try {
253
- await refreshDevRegistration(state);
1429
+ refreshDevRegistration(state);
254
1430
  } catch (error_) {
255
1431
  error("GResource refresh failed:", error_);
256
1432
  }
257
1433
  };
258
1434
 
1435
+ const rebuildForSourceAvailability = (state: PluginState, previousSignature: string): void => {
1436
+ try {
1437
+ rebuildActiveClaims(state);
1438
+
1439
+ if (entriesSignature(state) !== previousSignature) {
1440
+ refreshDevRegistration(state);
1441
+ }
1442
+ } catch (error_) {
1443
+ error("GResource source update failed:", error_);
1444
+ }
1445
+ };
1446
+
1447
+ const updateTrackedSourceAvailability = (state: PluginState, file: string, isAvailable: boolean): void => {
1448
+ if (!isTrackedSource(state, file) && !state.missingSourcePaths.has(file) && !isDeclaredSource(state, file)) {
1449
+ return;
1450
+ }
1451
+
1452
+ const previousSignature = entriesSignature(state);
1453
+ setSourceAvailability(state, file, isAvailable);
1454
+ rebuildForSourceAvailability(state, previousSignature);
1455
+ };
1456
+
259
1457
  const attachResourceWatcher = (state: PluginState, server: ViteDevServer): void => {
260
1458
  state.server = server;
261
1459
 
262
- const onFileEvent = (file: string): void => {
263
- void refreshTrackedSource(state, file);
1460
+ const onChange = (file: string): void => {
1461
+ refreshTrackedSource(state, file);
1462
+ };
1463
+
1464
+ const onAdd = (file: string): void => {
1465
+ updateTrackedSourceAvailability(state, file, true);
1466
+ };
1467
+
1468
+ const onUnlink = (file: string): void => {
1469
+ updateTrackedSourceAvailability(state, file, false);
264
1470
  };
265
1471
 
266
1472
  const onClose = (): void => {
@@ -269,14 +1475,17 @@ const attachResourceWatcher = (state: PluginState, server: ViteDevServer): void
269
1475
 
270
1476
  server.httpServer?.once("close", onClose);
271
1477
  server.watcher.once("close", onClose);
272
- server.watcher.on("change", onFileEvent);
273
- server.watcher.on("add", onFileEvent);
1478
+ server.watcher.on("change", onChange);
1479
+ server.watcher.on("add", onAdd);
1480
+ server.watcher.on("unlink", onUnlink);
274
1481
  };
275
1482
 
276
1483
  const applyResolvedConfig = (state: PluginState, config: ResolvedConfig): void => {
277
1484
  state.isBuild = config.command === "build";
1485
+ state.root = config.root;
1486
+ state.dataDir = null;
278
1487
 
279
- if (state.isBuild) {
1488
+ if (state.isBuild || state.isV2) {
280
1489
  return;
281
1490
  }
282
1491
 
@@ -296,62 +1505,212 @@ const loadResourceModule = (state: PluginState, id: string): string | undefined
296
1505
  return loadAssetModule(state, id);
297
1506
  };
298
1507
 
299
- function gtkxResources(loadConfig: ConfigLoader = createConfigLoader()): Plugin {
300
- const state: PluginState = {
301
- prefix: "",
302
- isBuild: false,
303
- entries: new Map(),
304
- sourcePaths: new Set(),
305
- stagingDir: createRetainedStagingDir(DEV_STAGING_PREFIX),
306
- devBundlePath: "",
307
- server: null,
308
- compiledSignature: "",
309
- dataDir: null,
310
- };
1508
+ const isLegacyDataSpecifier = (source: string): boolean =>
1509
+ source.startsWith(DATA_PREFIX) && ASSET_RE.test(stripQuery(source));
311
1510
 
312
- return {
313
- name: "gtkx:resources",
314
- enforce: "pre",
1511
+ const rejectInvalidV2Specifier = (source: string): void => {
1512
+ if (BUNDLED_QUERY_MENTION_RE.test(source)) {
1513
+ throw new Error(
1514
+ `${JSON.stringify(source)} is not a valid bundled asset import; use ?resource, ` +
1515
+ "?resource=/absolute/path, ?icon, or ?icon=name",
1516
+ );
1517
+ }
1518
+
1519
+ if (isBareRelativeAsset(source)) {
1520
+ throw new Error(
1521
+ `${JSON.stringify(source)} must choose ?resource for a GResource, ?icon for a themed icon, ` +
1522
+ "or ?url for an emitted file",
1523
+ );
1524
+ }
1525
+ };
315
1526
 
316
- config(config: UserConfig) {
317
- return resolveResourceConfig(state, config, loadConfig);
318
- },
1527
+ const resolveExplicitResourceId = async (
1528
+ ctx: ResolveContext,
1529
+ state: PluginState,
1530
+ request: LegacyResolveRequest,
1531
+ ): Promise<string | undefined> => {
1532
+ const specifier = parseResourceSpecifier(request.source);
1533
+
1534
+ if (specifier === null) {
1535
+ return undefined;
1536
+ }
319
1537
 
320
- configResolved(config: ResolvedConfig) {
321
- applyResolvedConfig(state, config);
322
- },
1538
+ const entry = await resolveAssetEntry(ctx, state, {
1539
+ assetSource: specifier.assetSource,
1540
+ importer: request.importer,
1541
+ resourcePath: specifier.resourcePath,
1542
+ options: request.options,
1543
+ });
323
1544
 
324
- configureServer(server) {
325
- attachResourceWatcher(state, server);
326
- primeDevBundle(state);
327
- },
1545
+ return entry === undefined ? undefined : virtualAssetId(entry);
1546
+ };
328
1547
 
329
- async resolveId(source, importer, opts) {
330
- if (source === VIRTUAL_INIT) {
331
- return VIRTUAL_INIT;
332
- }
1548
+ const resolveIconResourceId = async (
1549
+ ctx: ResolveContext,
1550
+ state: PluginState,
1551
+ request: LegacyResolveRequest,
1552
+ ): Promise<RetainedIconModuleId | undefined> => {
1553
+ const specifier = parseIconSpecifier(request.source);
333
1554
 
334
- if (!isDataAsset(source)) {
335
- return;
336
- }
1555
+ if (specifier === null) {
1556
+ return undefined;
1557
+ }
337
1558
 
338
- const resolved = await this.resolve(source, importer, { ...opts, skipSelf: true });
1559
+ const resolved = await resolveIconEntry(ctx, state, {
1560
+ assetSource: specifier.assetSource,
1561
+ iconName: specifier.iconName,
1562
+ importer: request.importer,
1563
+ options: request.options,
1564
+ });
339
1565
 
340
- return resolvedAssetId(resolved, source);
341
- },
1566
+ return resolved === undefined
1567
+ ? undefined
1568
+ : { id: virtualIconId(resolved), moduleSideEffects: true };
1569
+ };
342
1570
 
343
- load(id) {
344
- return loadResourceModule(state, id);
345
- },
1571
+ const resolveV2ResourceId = async (
1572
+ ctx: ResolveContext,
1573
+ state: PluginState,
1574
+ request: LegacyResolveRequest,
1575
+ ): Promise<ResourceModuleId | undefined> => {
1576
+ if (isLegacyDataSpecifier(request.source)) {
1577
+ throw new Error(
1578
+ `${JSON.stringify(request.source)} uses the legacy #data asset form; import it relatively with ` +
1579
+ "?resource or ?url",
1580
+ );
1581
+ }
346
1582
 
347
- buildEnd() {
348
- emitBuildBundle(this, state);
349
- },
1583
+ if (parseResourceSpecifier(request.source) !== null) {
1584
+ const resolved = await resolveExplicitResourceId(ctx, state, request);
1585
+ trackBundledSpecifier(state, request.importer, request.source);
350
1586
 
351
- closeBundle() {
352
- releaseStagingDir(state);
353
- },
1587
+ return resolved;
1588
+ }
1589
+
1590
+ if (parseIconSpecifier(request.source) !== null) {
1591
+ const resolved = await resolveIconResourceId(ctx, state, request);
1592
+ trackBundledSpecifier(state, request.importer, request.source);
1593
+
1594
+ return resolved;
1595
+ }
1596
+
1597
+ rejectInvalidV2Specifier(request.source);
1598
+
1599
+ return undefined;
1600
+ };
1601
+
1602
+ const resolveLegacyResourceId = async (
1603
+ ctx: ResolveContext,
1604
+ state: PluginState,
1605
+ request: LegacyResolveRequest,
1606
+ ): Promise<string | undefined> => {
1607
+ if (BUNDLED_QUERY_MENTION_RE.test(request.source)) {
1608
+ throw new Error(`${JSON.stringify(request.source)} requires future.v2ResourceImports`);
1609
+ }
1610
+
1611
+ if (!isDataAsset(request.source)) {
1612
+ return undefined;
1613
+ }
1614
+
1615
+ const entry = await resolveLegacyAssetEntry(ctx, state, request);
1616
+
1617
+ return entry === undefined ? undefined : virtualAssetId(entry);
1618
+ };
1619
+
1620
+ const resolveResourceId = (
1621
+ ctx: ResolveContext,
1622
+ state: PluginState,
1623
+ request: LegacyResolveRequest,
1624
+ ): Promise<ResourceModuleId | undefined> => {
1625
+ if (request.source === VIRTUAL_INIT) {
1626
+ return Promise.resolve(VIRTUAL_INIT);
1627
+ }
1628
+
1629
+ return state.isV2
1630
+ ? resolveV2ResourceId(ctx, state, request)
1631
+ : resolveLegacyResourceId(ctx, state, request);
1632
+ };
1633
+
1634
+ const configureResourceServer = (state: PluginState, server: ViteDevServer): void => {
1635
+ attachResourceWatcher(state, server);
1636
+ primeLegacyDevBundle(state);
1637
+ };
1638
+
1639
+ const createResourcesPlugin = (state: PluginState, loadConfig: ConfigLoader): Plugin => ({
1640
+ name: "gtkx:resources",
1641
+ enforce: "pre",
1642
+ perEnvironmentStartEndDuringDev: true,
1643
+
1644
+ config: (config: UserConfig) => resolveResourceConfig(state, config, loadConfig),
1645
+
1646
+ configResolved: (config: ResolvedConfig) => {
1647
+ applyResolvedConfig(state, config);
1648
+ },
1649
+
1650
+ configureServer: (server) => {
1651
+ configureResourceServer(state, server);
1652
+ },
1653
+
1654
+ buildStart() {
1655
+ if (state.isBuild && state.bundleReferenceId === null) {
1656
+ state.bundleReferenceId = this.emitFile({
1657
+ type: "asset",
1658
+ fileName: BUNDLE_FILENAME,
1659
+ source: "",
1660
+ });
1661
+ }
1662
+ },
1663
+
1664
+ resolveId(source, importer, options) {
1665
+ return resolveResourceId(this, state, { source, importer, options });
1666
+ },
1667
+
1668
+ load(id) {
1669
+ return loadResourceModule(state, id);
1670
+ },
1671
+
1672
+ async transform(code, id) {
1673
+ await reconcileV2Importer(this, state, id);
1674
+
1675
+ return retainSideEffectIconImport(state, code, id);
1676
+ },
1677
+
1678
+ generateBundle(_options, bundle) {
1679
+ finalizeBuildBundle(this, bundle, state);
1680
+ },
1681
+
1682
+ closeBundle() {
1683
+ releaseStagingDir(state);
1684
+ },
1685
+ });
1686
+
1687
+ function gtkxResources(loadConfig: ConfigLoader = createConfigLoader(), entryPath?: string): Plugin {
1688
+ const state: PluginState = {
1689
+ prefix: "",
1690
+ root: "",
1691
+ isBuild: false,
1692
+ isV2: false,
1693
+ entries: new Map(),
1694
+ entryImporters: new Map(),
1695
+ bundledSpecifiers: new Map(),
1696
+ moduleDependencies: new Map(),
1697
+ prunedDependencies: new Map(),
1698
+ declaredEntries: new Map(),
1699
+ declaredIcons: new Map(),
1700
+ entryModule: entryPath === undefined ? null : importerKey(entryPath),
1701
+ sourcePaths: new Set(),
1702
+ missingSourcePaths: new Set(),
1703
+ stagingDir: createRetainedStagingDir(DEV_STAGING_PREFIX),
1704
+ devBundlePath: "",
1705
+ server: null,
1706
+ compiledSignature: "",
1707
+ dataDir: null,
1708
+ bundleReferenceId: null,
1709
+ iconOwners: new Map(),
1710
+ iconImporters: new Map(),
354
1711
  };
1712
+
1713
+ return createResourcesPlugin(state, loadConfig);
355
1714
  }
356
1715
 
357
1716
  export { gtkxResources };