@peachy/plugin-resources 0.0.10

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.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @peachy/plugin-resources
2
+
3
+ Allow you to import files as resources.
4
+
5
+ ## Usage
6
+
7
+ You will need to have peachy installed.
8
+
9
+ Then, you need to create a `peachy.config.ts` file in the root of your project.
10
+
11
+ ```ts
12
+ // peachy.config.ts
13
+
14
+ import { defineConfig } from "@peachy/core";
15
+
16
+ export default defineConfig({
17
+ package: {
18
+ name: "dev.peachy.Example"
19
+ },
20
+ resources: {
21
+ icons: true,
22
+ }
23
+ });
24
+ ```
25
+
26
+ TODO: enable this by default, skip generation if there's no resources/icons
27
+
28
+ In your code, make sure to initialize your code to use the given name in your `peachy.config.ts` file.
29
+
30
+ ```tsx
31
+ import Gtk from "gi://Gtk?version=4.0";
32
+
33
+ const app = new Gtk.Application({
34
+ // IMPORTANT: match this with the one in `peachy.config.ts`
35
+ application_id: "dev.peachy.Example",
36
+ flags: Gtk.ApplicationFlags.FLAGS_NONE,
37
+ });
38
+
39
+ /// ...
40
+
41
+ app.run([]);
42
+ ```
43
+
44
+ Then you can start importing resources/icons.
45
+
46
+
47
+ ### Importing Files
48
+
49
+ When you need to import a resource, you can directly import them. Currently, only SVG files are supported.
50
+
51
+ ```tsx
52
+ import Gtk from "gi://Gtk?version=4.0";
53
+
54
+ import File from "./path/to/file.svg";
55
+
56
+ const image = new Gtk.Image({
57
+ resource: File,
58
+ });
59
+ ```
60
+
61
+ You can use the imported resource anywhere that accepts a `Gio.Resource` like `Gtk.Image` and `Gtk.Picture`.
62
+
63
+ ### Icons
64
+
65
+ Icons in `data/icons` folder will be automatically configured and registered as Themed Icons, so you can use them just by referencing their name.
66
+
67
+ #### 1. Put your icons in `data/icons` folder.
68
+
69
+ ```
70
+ data/icons/right-symbolic.svg
71
+ data/icons/left-symbolic.svg
72
+ ```
73
+
74
+ #### 2. Use them in your code.
75
+
76
+ ```tsx
77
+ import Gtk from "gi://Gtk?version=4.0";
78
+
79
+ const button = new Gtk.Button({
80
+ icon_name: "right-symbolic",
81
+ });
82
+ ```
@@ -0,0 +1,15 @@
1
+ import { Plugin } from "rolldown";
2
+
3
+ //#region src/types.d.ts
4
+ interface ResourcesPluginOptions {
5
+ setRunnerEnv(key: string, value: string): void;
6
+ applicationId?: string;
7
+ prod: boolean;
8
+ outdir: string;
9
+ iconsPath?: string | false;
10
+ }
11
+ //#endregion
12
+ //#region src/plugins/index.d.ts
13
+ declare function resourcesPlugin(config: ResourcesPluginOptions): Plugin[];
14
+ //#endregion
15
+ export { resourcesPlugin };
package/dist/index.mjs ADDED
@@ -0,0 +1,287 @@
1
+ import { basename, dirname, join, relative, resolve } from "node:path";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+ import { access, mkdir, mkdtempDisposable, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
+ import { exactRegex, prefixRegex } from "rolldown/filter";
6
+ import { tmpdir } from "node:os";
7
+ import { exec } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+
10
+ //#region src/constants.ts
11
+ const GRESOURCE_PREFIX = "\0gresource";
12
+ const GRESOURCE_LOADER = `${GRESOURCE_PREFIX}:loader`;
13
+ const GRESOURCE_ICONS_PATH = "icons/scalable/actions";
14
+
15
+ //#endregion
16
+ //#region src/utilities/index.ts
17
+ function parseImportType(assertions) {
18
+ if (!assertions?.type) return null;
19
+ const type = assertions.type;
20
+ if (![
21
+ "resource",
22
+ "string",
23
+ "bytes"
24
+ ].includes(type)) throw new SyntaxError(`Invalid gresource import type: ${type}`);
25
+ return type;
26
+ }
27
+ function getInternalResourceId(id) {
28
+ const normalized = id.replace(/\\/g, "/");
29
+ return relative(process.cwd(), normalized);
30
+ }
31
+ function getResourceId(prefix, id) {
32
+ return `${prefix}/${getInternalResourceId(id)}`;
33
+ }
34
+ function generateImportCode(type, resourcePath, addImportCode = false) {
35
+ let code;
36
+ switch (type) {
37
+ case "resource":
38
+ code = `export default "${resourcePath}";\n`;
39
+ break;
40
+ case "string":
41
+ code = [
42
+ `import Gio from 'gi://Gio';`,
43
+ `const bytes = Gio.resources_lookup_data("${resourcePath}", Gio.ResourceLookupFlags.NONE);`,
44
+ `export default new TextDecoder().decode(bytes.toArray());`,
45
+ ""
46
+ ].join("\n");
47
+ break;
48
+ case "bytes":
49
+ code = [
50
+ `import Gio from 'gi://Gio';`,
51
+ `export default Gio.resources_lookup_data("${resourcePath}", Gio.ResourceLookupFlags.NONE);`,
52
+ ""
53
+ ].join("\n");
54
+ break;
55
+ }
56
+ if (addImportCode) return `import ${JSON.stringify(GRESOURCE_LOADER)};${code}`;
57
+ return code;
58
+ }
59
+ async function loadIcons(prefix, path$1) {
60
+ if (!access(path$1).catch(() => false)) throw new Error(`Icon assets directory not found: ${path$1}`);
61
+ const files = await readdir(path$1, { recursive: true });
62
+ const collectedAssets = /* @__PURE__ */ new Map();
63
+ for (const file of files) {
64
+ const fullName = resolve(path$1, file);
65
+ const stats = await stat(fullName);
66
+ if (!fullName.endsWith(".svg") || !stats.isFile()) continue;
67
+ const id = relative(process.cwd(), fullName);
68
+ collectedAssets.set(fullName, {
69
+ internalResourceId: id,
70
+ resourceId: getResourceId(prefix, id),
71
+ path: fullName
72
+ });
73
+ }
74
+ return collectedAssets;
75
+ }
76
+ function getFilePath(path$1, alias) {
77
+ let attrs = "";
78
+ if (/\.(svg|xml)$/.test(path$1)) attrs = " compressed=\"true\" preprocess=\"xml-stripblanks\"";
79
+ else if (/\.(png|jpg|jpeg)$/.test(path$1)) attrs = " compressed=\"true\"";
80
+ else if (/\.(json)$/.test(path$1)) attrs = " preprocess=\"json-stripblanks\"";
81
+ if (alias) attrs += ` alias="${alias}"`;
82
+ return ` <file${attrs}>${path$1}</file>`;
83
+ }
84
+ function generateGResourceXML(prefix, assets, icons) {
85
+ const files = [...assets.values()].map(({ internalResourceId }) => getFilePath(internalResourceId)).join("\n");
86
+ const iconFiles = icons ? [...icons.values()].map(({ internalResourceId }) => getFilePath(internalResourceId, basename(internalResourceId))).join("\n") : "";
87
+ return `<?xml version="1.0" encoding="UTF-8"?>
88
+ <gresources>
89
+ <gresource prefix="${prefix}">
90
+ ${files}
91
+ </gresource>
92
+ ${iconFiles ? `<gresource prefix="${prefix}/${GRESOURCE_ICONS_PATH}">
93
+ ${iconFiles}
94
+ </gresource>` : ""}
95
+ </gresources>`;
96
+ }
97
+
98
+ //#endregion
99
+ //#region src/utilities/icons.ts
100
+ async function getIconsPath(iconsPath) {
101
+ if (!iconsPath) return null;
102
+ if (!await fs.stat(iconsPath).then(() => true).catch(() => false)) return null;
103
+ return path.resolve(process.cwd(), iconsPath);
104
+ }
105
+ async function collectIcons(baseIconsPath, prefix) {
106
+ const iconsPath = await getIconsPath(baseIconsPath);
107
+ let collectedIcons = /* @__PURE__ */ new Map();
108
+ if (iconsPath) collectedIcons = await loadIcons(prefix, iconsPath);
109
+ return collectedIcons;
110
+ }
111
+
112
+ //#endregion
113
+ //#region src/plugins/dev/index.ts
114
+ /**
115
+ * This plugin configures a GRESOURCES overlay for development purposes.
116
+ */
117
+ function resourcesPluginOverlay({ prefix, prod, outdir, setRunnerEnv, iconsPath: baseIconsPath }) {
118
+ if (prod) return null;
119
+ const collectedResources = /* @__PURE__ */ new Map();
120
+ const devResourcesOverlay = path.join(outdir, "_peachy_resources");
121
+ let iconsLinked = false;
122
+ setRunnerEnv("G_RESOURCE_OVERLAYS", `${prefix}=${devResourcesOverlay}`);
123
+ return {
124
+ name: "@peachy/plugin-resources#bundle-dev",
125
+ load: {
126
+ filter: { id: prefixRegex(`${GRESOURCE_PREFIX}:`) },
127
+ handler(id) {
128
+ const [, importType, path$1] = id.split(":", 3);
129
+ if (!path$1) return;
130
+ const resourceId = getResourceId(prefix, path$1);
131
+ collectedResources.set(path$1, {
132
+ resourceId,
133
+ path: path$1,
134
+ internalResourceId: getInternalResourceId(path$1)
135
+ });
136
+ return generateImportCode(importType, resourceId, false);
137
+ }
138
+ },
139
+ async buildStart() {
140
+ if (iconsLinked) return;
141
+ iconsLinked = true;
142
+ const iconsPath = await getIconsPath(baseIconsPath ?? false);
143
+ if (!iconsPath) return;
144
+ const linkPath = path.join(devResourcesOverlay, GRESOURCE_ICONS_PATH);
145
+ await fs.mkdir(path.dirname(linkPath), { recursive: true });
146
+ await fs.lstat(linkPath).then(() => fs.unlink(linkPath)).catch(() => {});
147
+ await fs.symlink(iconsPath, linkPath);
148
+ },
149
+ async writeBundle() {
150
+ await fs.mkdir(devResourcesOverlay, { recursive: true });
151
+ const currentPaths = new Set(Array.from(collectedResources.values()).map((r) => r.internalResourceId));
152
+ const existing = await fs.readdir(devResourcesOverlay, {
153
+ recursive: true,
154
+ withFileTypes: true
155
+ });
156
+ for (const entry of existing) {
157
+ if (!entry.isSymbolicLink()) continue;
158
+ const fullPath = path.resolve(entry.parentPath, entry.name);
159
+ const relativePath = path.relative(devResourcesOverlay, fullPath);
160
+ if (currentPaths.has(relativePath) || relativePath === GRESOURCE_ICONS_PATH) continue;
161
+ await fs.unlink(fullPath);
162
+ }
163
+ for (const [, { path: filePath, internalResourceId }] of collectedResources) {
164
+ const linkPath = path.join(devResourcesOverlay, internalResourceId);
165
+ try {
166
+ await fs.lstat(linkPath);
167
+ } catch {
168
+ await fs.mkdir(path.dirname(linkPath), { recursive: true });
169
+ await fs.symlink(filePath, linkPath);
170
+ }
171
+ }
172
+ collectedResources.clear();
173
+ }
174
+ };
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/plugins/prod/index.ts
179
+ const exec$1 = promisify(exec);
180
+ function resourcesPluginGenerateXML({ prefix, gresourceName, gresourcePath, prod, iconsPath }) {
181
+ if (!prod) return null;
182
+ const collectedResources = /* @__PURE__ */ new Map();
183
+ return {
184
+ name: "@peachy/plugin-resources#bundle-prod",
185
+ load: {
186
+ filter: { id: prefixRegex(`${GRESOURCE_PREFIX}:`) },
187
+ handler(id) {
188
+ const [, _importType, path$1] = id.split(":", 3);
189
+ if (!path$1) return;
190
+ const importType = _importType;
191
+ const resourceId = getResourceId(prefix, path$1);
192
+ collectedResources.set(path$1, {
193
+ resourceId,
194
+ path: path$1,
195
+ internalResourceId: getInternalResourceId(path$1)
196
+ });
197
+ return generateImportCode(importType, resourceId, true);
198
+ }
199
+ },
200
+ async generateBundle() {
201
+ const collectedIcons = await collectIcons(iconsPath ?? false, prefix);
202
+ if (collectedResources.size === 0 && collectedIcons.size === 0) return;
203
+ const xmlPath = join((await mkdtempDisposable(join(tmpdir(), "peachy-resource-build"))).path, "gresources.xml");
204
+ await writeFile(xmlPath, generateGResourceXML(prefix, collectedResources, collectedIcons));
205
+ await mkdir(dirname(gresourcePath), { recursive: true });
206
+ await exec$1(`glib-compile-resources --sourcedir=${process.cwd()} --target=${gresourcePath} ${xmlPath}`, { env: { ...process.env } });
207
+ this.emitFile({
208
+ type: "asset",
209
+ fileName: gresourceName,
210
+ source: await readFile(gresourcePath)
211
+ });
212
+ }
213
+ };
214
+ }
215
+
216
+ //#endregion
217
+ //#region src/plugins/resolve.ts
218
+ /**
219
+ * This plugin monitors all imports, and decides if the import is a resource.
220
+ *
221
+ * Then it replaces the import with some code that registers the resource.
222
+ */
223
+ function resourcesPluginResolve() {
224
+ return {
225
+ name: "@peachy/plugin-resources#resolve",
226
+ resolveId: {
227
+ filter: { id: /\.svg$/ },
228
+ handler(id, importer) {
229
+ const importType = parseImportType({ type: "resource" });
230
+ if (!importType) return null;
231
+ return `${GRESOURCE_PREFIX}:${importType}:${importer ? path.resolve(path.dirname(importer), id) : path.resolve(id)}`;
232
+ }
233
+ }
234
+ };
235
+ }
236
+
237
+ //#endregion
238
+ //#region src/plugins/loader.ts
239
+ /**
240
+ * This plugin injects some code that loads and registers our application's
241
+ * GResource
242
+ */
243
+ function resourcesPluginLoader({ gresourcePath }) {
244
+ return {
245
+ name: "@peachy/plugin-resources#loader",
246
+ resolveId: {
247
+ filter: { id: exactRegex(GRESOURCE_LOADER) },
248
+ handler(id) {
249
+ return id;
250
+ }
251
+ },
252
+ load: {
253
+ filter: { id: exactRegex(GRESOURCE_LOADER) },
254
+ handler() {
255
+ return [
256
+ `import Gio from "gi://Gio";`,
257
+ `const resource = Gio.Resource.load("${gresourcePath}");`,
258
+ `resource._register();`
259
+ ].join("\n");
260
+ }
261
+ }
262
+ };
263
+ }
264
+
265
+ //#endregion
266
+ //#region src/plugins/index.ts
267
+ function resourcesPlugin(config) {
268
+ const pkgName = config.applicationId || "dev.peachy.application";
269
+ const prefix = "/" + pkgName.replaceAll(".", "/");
270
+ const gresourceName = `${pkgName}.gresource`;
271
+ const gresourcePath = join(config.outdir, gresourceName);
272
+ const resourcesOptions = {
273
+ ...config,
274
+ gresourceName,
275
+ gresourcePath,
276
+ prefix
277
+ };
278
+ return [
279
+ resourcesPluginResolve(),
280
+ resourcesPluginLoader(resourcesOptions),
281
+ resourcesPluginOverlay(resourcesOptions),
282
+ resourcesPluginGenerateXML(resourcesOptions)
283
+ ].filter(Boolean);
284
+ }
285
+
286
+ //#endregion
287
+ export { resourcesPlugin };
@@ -0,0 +1,5 @@
1
+ //#region src/types/global.d.ts
2
+ declare module "*.svg" {
3
+ const content: string;
4
+ export default content;
5
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@peachy/plugin-resources",
3
+ "version": "0.0.10",
4
+ "type": "module",
5
+ "description": "Import resources in your app",
6
+ "main": "./src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "types": "./dist/index.d.mts"
11
+ },
12
+ "./types": {
13
+ "types": "./dist/types/global.d.mts"
14
+ }
15
+ },
16
+ "author": "",
17
+ "license": "MIT",
18
+ "devDependencies": {
19
+ "@types/node": "^25.0.9",
20
+ "rolldown": "1.0.0-beta.60",
21
+ "tsdown": "0.20.0-beta.4",
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "peerDependencies": {
25
+ "rolldown": "1.0.0-beta.58"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsdown src/index.ts src/types/global.d.ts --dts"
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./plugins";