@gjsify/rolldown-plugin-gjsify 0.16.5 → 0.16.7

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.
@@ -12,12 +12,15 @@
12
12
  //
13
13
  // — the canonical pattern for `Gtk.CssProvider` under GJS.
14
14
  //
15
- // `@import` resolution + nesting/modern-syntax lowering are handled via
16
- // lightningcss `bundleAsync`. The defaults work for the common case
17
- // (resolve `@import`s, no targeting); the `--app gjs` orchestrator passes
18
- // `targets: { firefox: 60 << 16 }` so nesting + modern selectors get
19
- // flattened to GTK4-CSS-engine-compatible output. Targeting is opt-in —
20
- // a missing `targets` keeps the source pristine.
15
+ // `@import` resolution + nesting/modern-syntax lowering. On the npm backend
16
+ // both happen in one lightningcss `bundleAsync` call. On the native backend
17
+ // `@import`s are resolved + inlined in JS first (`flattenCssImports`, so bare
18
+ // node_modules specifiers work — the native `bundle()` FileProvider can't walk
19
+ // node_modules), then the flattened CSS goes through the native `transform()`
20
+ // for lowering. Both use the same `cssBundleResolver`. The `--app gjs`
21
+ // orchestrator passes `targets: { firefox: 60 << 16 }` so nesting + modern
22
+ // selectors get flattened to GTK4-CSS-engine-compatible output. Targeting is
23
+ // opt-in — a missing `targets` keeps the source pristine.
21
24
  //
22
25
  // Backend selection (Phase D-2 decision matrix in
23
26
  // `docs/poc/lightningcss-decision.md`):
@@ -77,18 +80,39 @@ async function tryLoadNativeBundler() {
77
80
  if (!mod.hasNativeLightningcss())
78
81
  return null;
79
82
  return async (filename, targets) => {
80
- // The native shim accepts a browserslist string; the npm
81
- // `lightningcss` Targets struct is bitfield-encoded
82
- // (`firefox: 60 << 16` etc). Convert by extracting major
83
- // version per browser key and re-emitting as the equivalent
84
- // browserslist query.
83
+ // The native `@gjsify/lightningcss-native` `bundle()` resolves
84
+ // `@import` chains through lightningcss's filesystem-backed
85
+ // FileProvider, which walks relative + absolute paths but NOT
86
+ // bare node_modules specifiers (`@import "@scope/pkg/x.css"`) —
87
+ // a JS resolver callback can't cross the GI/Rust boundary. So we
88
+ // do `@import` resolution in JS first, using the SAME
89
+ // `cssBundleResolver` the npm `bundleAsync` path uses (npm-package
90
+ // + `exports`-map aware), and hand the fully-flattened CSS to the
91
+ // native `transform()` for the GTK4 nesting/modern-syntax lowering
92
+ // (`targets`). The native shim accepts a browserslist string; the
93
+ // npm `lightningcss` Targets struct is bitfield-encoded
94
+ // (`firefox: 60 << 16` etc), so convert per browser key.
95
+ let flattened;
96
+ try {
97
+ flattened = await flattenCssImports(filename);
98
+ }
99
+ catch (err) {
100
+ // The native rolldown engine flattens a thrown plugin error to
101
+ // a generic "plugin `gjsify-css-as-string` threw an error"
102
+ // diagnostic — the JS message is lost across the GI boundary.
103
+ // Surface the actionable reason (which file, which @import) on
104
+ // stderr before re-throwing so a bad/missing @import is not an
105
+ // opaque build failure.
106
+ console.error(`[gjsify-css-as-string] ${err.message}`);
107
+ throw err;
108
+ }
85
109
  const query = targetsToBrowserslist(targets);
86
- return mod.bundle({
110
+ return mod.transform({
87
111
  filename,
112
+ code: flattened,
88
113
  targets: query,
89
114
  minify: false,
90
115
  sourceMap: false,
91
- errorRecovery: true,
92
116
  });
93
117
  };
94
118
  }
@@ -155,13 +179,86 @@ const cssBundleResolver = {
155
179
  // leave it verbatim so lightningcss keeps the `@font-face` /
156
180
  // `url()` rule intact (the consumer serves the asset) instead of
157
181
  // crashing the build. A genuine unresolvable CSS `@import`
158
- // re-throws so the missing dependency still surfaces clearly.
182
+ // re-throws with actionable context so the missing dependency
183
+ // surfaces clearly on both backends (npm and native).
159
184
  if (isAssetReference(specifier))
160
185
  return specifier;
161
- throw err;
186
+ throw new Error(`cannot resolve @import "${specifier}" from ${from} (${err.message}). ` +
187
+ 'If it is a workspace/npm package, ensure it is installed and exposes the CSS ' +
188
+ 'file via its package.json "exports".');
162
189
  }
163
190
  },
164
191
  };
192
+ // Matches a CSS `@import` at-rule and captures the specifier + any trailing
193
+ // condition tokens (media query / `layer()` / `supports()`) before the `;`.
194
+ // Covers `@import "x";`, `@import 'x';`, and `@import url("x")` / `url(x)`.
195
+ const IMPORT_RE = /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)["']?\s*\)?\s*([^;]*);/g;
196
+ /** `String.prototype.replace` with an async replacer (sequential, order-preserving). */
197
+ async function replaceAllAsync(source, re, fn) {
198
+ const matches = [...source.matchAll(re)];
199
+ if (matches.length === 0)
200
+ return source;
201
+ let out = '';
202
+ let last = 0;
203
+ for (const m of matches) {
204
+ const index = m.index ?? 0;
205
+ out += source.slice(last, index);
206
+ out += await fn(m[0], ...m.slice(1));
207
+ last = index + m[0].length;
208
+ }
209
+ out += source.slice(last);
210
+ return out;
211
+ }
212
+ /**
213
+ * Recursively resolve + inline every bundleable `@import` in `entry`, using the
214
+ * same {@link cssBundleResolver} the npm `bundleAsync` path uses. Returns a
215
+ * single flattened CSS string with no bundleable `@import` statements left — the
216
+ * native `transform()` then applies GTK4 nesting/target lowering.
217
+ *
218
+ * This is the GJS-native counterpart of the npm `lightningcss` `bundleAsync`
219
+ * `@import` resolution: the native `bundle()` can only follow relative/absolute
220
+ * paths (lightningcss's filesystem FileProvider), so this closes the bare
221
+ * node_modules specifier gap (`@import "@scope/pkg/x.css"`) — the shared-CSS
222
+ * package pattern every consumer monorepo uses — without a Rust-side resolver
223
+ * hook. Asset-reference `@import`s (a bare `.woff2`/`.png`, a `data:`/`http:`
224
+ * URL) are kept verbatim (the resolver leaves them alone), matching the npm
225
+ * path. A cycle inlines each file at most once.
226
+ */
227
+ async function flattenCssImports(entry) {
228
+ const seen = new Set();
229
+ const inline = async (file, isRoot) => {
230
+ const abs = isAbsolute(file) ? file : resolvePath(file);
231
+ if (seen.has(abs))
232
+ return ''; // @import cycle — inline once
233
+ seen.add(abs);
234
+ let source = await readFile(abs, 'utf8');
235
+ // `@charset` is only valid as the very first token of a stylesheet;
236
+ // an inlined sub-file's `@charset` mid-stream would be invalid CSS.
237
+ if (!isRoot)
238
+ source = source.replace(/@charset[^;]*;/gi, '');
239
+ return replaceAllAsync(source, IMPORT_RE, async (match, spec, condition) => {
240
+ const resolved = cssBundleResolver.resolve(spec, abs);
241
+ // Asset-reference `@import` (rare): resolver returns it verbatim —
242
+ // keep the at-rule intact.
243
+ if (resolved === spec)
244
+ return match;
245
+ const cond = condition?.trim();
246
+ if (cond) {
247
+ // Conditional `@import`s (`@import "x" screen`) wrap the inlined
248
+ // content per lightningcss semantics. `layer()`/`supports()`
249
+ // conditions have richer nesting rules we don't model — fail
250
+ // loudly rather than emit subtly-wrong CSS.
251
+ if (/\b(layer|supports)\s*\(/i.test(cond)) {
252
+ throw new Error(`@import "${spec}" in ${abs} uses a layer()/supports() condition, which the ` +
253
+ 'GJS-native CSS backend does not support. Inline the import manually or drop the condition.');
254
+ }
255
+ return `@media ${cond} {\n${await inline(resolved, false)}\n}`;
256
+ }
257
+ return inline(resolved, false);
258
+ });
259
+ };
260
+ return inline(entry, true);
261
+ }
165
262
  function targetsToBrowserslist(targets) {
166
263
  if (!targets)
167
264
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.16.5",
3
+ "version": "0.16.7",
4
4
  "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -55,11 +55,11 @@
55
55
  ],
56
56
  "license": "MIT",
57
57
  "dependencies": {
58
- "@gjsify/console": "^0.16.5",
59
- "@gjsify/resolve-npm": "^0.16.5",
60
- "@gjsify/rolldown-plugin-deepkit": "^0.16.5",
61
- "@gjsify/rolldown-plugin-pnp": "^0.16.5",
62
- "@gjsify/vite-plugin-blueprint": "^0.16.5",
58
+ "@gjsify/console": "^0.16.7",
59
+ "@gjsify/resolve-npm": "^0.16.7",
60
+ "@gjsify/rolldown-plugin-deepkit": "^0.16.7",
61
+ "@gjsify/rolldown-plugin-pnp": "^0.16.7",
62
+ "@gjsify/vite-plugin-blueprint": "^0.16.7",
63
63
  "@rollup/pluginutils": "^5.4.0",
64
64
  "acorn": "^8.17.0",
65
65
  "acorn-walk": "^8.3.5",
@@ -68,7 +68,7 @@
68
68
  "sass": "^1.101.0"
69
69
  },
70
70
  "peerDependencies": {
71
- "@gjsify/lightningcss-native": "^0.16.5",
71
+ "@gjsify/lightningcss-native": "^0.16.7",
72
72
  "rolldown": "^1.1.4"
73
73
  },
74
74
  "peerDependenciesMeta": {