@gjsify/rolldown-plugin-gjsify 0.41.0 → 0.42.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.
package/lib/app/gjs.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
2
2
  import type { PluginOptions } from '../types/plugin-options.js';
3
+ import type { GiSystemProbe } from '../plugins/gi-runtime-paths.js';
3
4
  /** Resolved Rolldown configuration template + plugins for `--app gjs`. */
4
5
  export interface GjsBuildConfig {
5
6
  options: RolldownOptions;
@@ -26,6 +27,13 @@ export interface GjsFactoryInput {
26
27
  * `"…"` → custom line, supports `${env:NAME[:-default]}` placeholders
27
28
  */
28
29
  shebang?: boolean | string;
30
+ /**
31
+ * System GI library dirs the byte-1 prologue tries at RUNTIME — see `GiSystemProbe`.
32
+ * The only half a build can honestly fill: the build machine is not the host that
33
+ * runs the bundle, so what travels is a candidate plus the markers that decide it
34
+ * there. See `GjsifyPluginInput.giSystemProbes` for why the relative half is absent.
35
+ */
36
+ giSystemProbes?: readonly GiSystemProbe[];
29
37
  /** Plugin options forwarded to sub-plugins (deepkit, css, …). */
30
38
  pluginOptions: PluginOptions;
31
39
  }
package/lib/app/gjs.js CHANGED
@@ -191,7 +191,11 @@ export const setupForGjs = async (input) => {
191
191
  // into the subset GTK4 understands.
192
192
  cssAsStringPlugin({ targets: { firefox: 60 << 16 } }),
193
193
  nodeModulesPathRewritePlugin({ bundleDir, runtimeResolve: format === 'esm' }),
194
- processStubPlugin({ userBanner: input.userBanner, captureBundleUrl: format === 'esm' }),
194
+ processStubPlugin({
195
+ userBanner: input.userBanner,
196
+ captureBundleUrl: format === 'esm',
197
+ giSystemProbes: input.giSystemProbes,
198
+ }),
195
199
  // resolveShebangLine returns null when disabled, else the resolved line
196
200
  // with `${env:…}` expanded.
197
201
  (() => {
@@ -205,7 +209,14 @@ export const setupForGjs = async (input) => {
205
209
  // Unsupported URI scheme for importing: node`) — the same failure
206
210
  // `utils/gjs-bundle-guard.ts` catches at emit time, caught here at its
207
211
  // source with the importer and cause named.
208
- unresolvedWorkspaceImportPlugin({ target: 'gjs', aliases: aliasEntries, isExternal: external }),
212
+ unresolvedWorkspaceImportPlugin({
213
+ target: 'gjs',
214
+ aliases: aliasEntries,
215
+ isExternal: external,
216
+ ...(input.pluginOptions.toolchainAnchor !== undefined
217
+ ? { toolchainAnchor: input.pluginOptions.toolchainAnchor }
218
+ : {}),
219
+ }),
209
220
  ];
210
221
  return { options, plugins };
211
222
  };
package/lib/index.d.ts CHANGED
@@ -2,10 +2,11 @@ export * from './types/index.js';
2
2
  export * from './utils/index.js';
3
3
  export * from './app/index.js';
4
4
  export * from './library/index.js';
5
- export { REWRITE_FILTER, extractPackageSpec, getBundleDirFromOutput, rewriteContents, shouldRewrite, nodeModulesPathRewritePlugin, } from './plugins/rewrite-node-modules-paths.js';
5
+ export { REWRITE_FILTER, extractPackageSpec, getBundleDirFromOutput, rewriteContents, shouldRewrite, shouldInline, nodeModulesPathRewritePlugin, } from './plugins/rewrite-node-modules-paths.js';
6
6
  export type { NodeModulesPathRewriteOptions, RewriteResult } from './plugins/rewrite-node-modules-paths.js';
7
7
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
8
8
  export { giRuntimePathsStub } from './plugins/gi-runtime-paths.js';
9
+ export type { GiSystemProbe } from './plugins/gi-runtime-paths.js';
9
10
  export type { ProcessStubPluginOptions } from './plugins/process-stub.js';
10
11
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
11
12
  export { textLoaderPlugin } from './plugins/text-loader.js';
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ export * from './types/index.js';
3
3
  export * from './utils/index.js';
4
4
  export * from './app/index.js';
5
5
  export * from './library/index.js';
6
- export { REWRITE_FILTER, extractPackageSpec, getBundleDirFromOutput, rewriteContents, shouldRewrite, nodeModulesPathRewritePlugin, } from './plugins/rewrite-node-modules-paths.js';
6
+ export { REWRITE_FILTER, extractPackageSpec, getBundleDirFromOutput, rewriteContents, shouldRewrite, shouldInline, nodeModulesPathRewritePlugin, } from './plugins/rewrite-node-modules-paths.js';
7
7
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
8
8
  export { giRuntimePathsStub } from './plugins/gi-runtime-paths.js';
9
9
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
package/lib/plugin.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
2
2
  import type { PluginOptions } from './types/plugin-options.js';
3
+ import type { GiSystemProbe } from './plugins/gi-runtime-paths.js';
3
4
  export interface GjsifyConfig {
4
5
  options: RolldownOptions;
5
6
  plugins: RolldownPluginOption[];
@@ -20,6 +21,15 @@ export interface GjsifyPluginInput {
20
21
  * `"…"` → custom line, supports `${env:NAME[:-default]}` placeholders
21
22
  */
22
23
  shebang?: boolean | string;
24
+ /**
25
+ * `--app gjs` only: system GI library dirs the byte-1 prologue probes on the RUNNING
26
+ * host. The bundle-relative half of that prologue is deliberately NOT plumbed here —
27
+ * `processStubPlugin`'s `giRuntimePaths` still takes it, but nothing in this repo can
28
+ * fill it with a path that is true on the machine a shipped bundle runs on
29
+ * (`packages/infra/cli/src/utils/gi-runtime-paths.ts` records what was measured), and
30
+ * an option no caller fills is the defect this wiring was added to remove.
31
+ */
32
+ giSystemProbes?: readonly GiSystemProbe[];
23
33
  }
24
34
  /**
25
35
  * Build the Rolldown configuration template + plugin array for the given
package/lib/plugin.js CHANGED
@@ -42,6 +42,7 @@ export const gjsifyPlugin = async (input, pluginOptions = {}) => {
42
42
  userBanner: input.userBanner,
43
43
  userAliases: input.userAliases,
44
44
  shebang: input.shebang,
45
+ giSystemProbes: input.giSystemProbes,
45
46
  pluginOptions,
46
47
  });
47
48
  case 'node':
@@ -1,10 +1,32 @@
1
1
  /**
2
- * The byte-1 prologue that prepends `dirs` to the default repository's typelib
3
- * search path and library path. Empty list → `''`: a no-op prologue in every
4
- * bundle is a guard that can never fire, and a banner that is not there cannot
5
- * drift.
2
+ * An absolute candidate library dir, the path that proves it IS one, and the path
3
+ * that proves the running host is the PLATFORM the candidate belongs to.
4
+ *
5
+ * Both gates, because they answer different questions and only one of them travels
6
+ * well. `/usr/local/lib` holding a `girepository-1.0/` says "a GI stack is installed
7
+ * here"; it does NOT say "this host's loader needs help finding it" — on Linux
8
+ * `ld.so`'s system-wide cache already resolves those leaves, and a `meson setup
9
+ * --prefix=/usr/local` or jhbuild tree is a normal shape there. Prepending it anyway
10
+ * puts a second GI stack ahead of the distro's for every bundle on that host, which
11
+ * is the two-registries failure ADR 0023 § 4 / #910 describe. The bundle cannot ask
12
+ * `process.platform` (it is platform-neutral JavaScript built on a machine that is
13
+ * not the host), so the platform scope travels as a marker path too.
14
+ */
15
+ export type GiSystemProbe = readonly [libDir: string, marker: string, hostMarker: string];
16
+ /**
17
+ * The byte-1 prologue that prepends directories to the default repository's typelib
18
+ * search path and library path. Nothing to prepend → `''`: a no-op prologue in every
19
+ * bundle is a guard that can never fire, and a banner that is not there cannot drift.
20
+ *
21
+ * Prepending REVERSES a list, so both loops count down and the emitted order is the
22
+ * order given. `systemProbes` are applied first and therefore end up BEHIND `dirs`:
23
+ * ADR 0023's bundle-before-system precedence, which #910 paid for.
6
24
  *
7
25
  * @param dirs directories RELATIVE to the program's own directory, `/`-separated —
8
26
  * an absolute path baked at build time is the BUILD machine's
27
+ * @param systemProbes absolute directories prepended only where BOTH of a probe's
28
+ * marker paths exist on the RUNNING host; the markers are passed in so the
29
+ * `girepository-1.0` layout rule and the platform scope around it keep their single
30
+ * definition in the caller
9
31
  */
10
- export declare function giRuntimePathsStub(dirs: readonly string[]): string;
32
+ export declare function giRuntimePathsStub(dirs: readonly string[], systemProbes?: readonly GiSystemProbe[]): string;
@@ -1,37 +1,84 @@
1
- // Tell GI where a bundle's own typelibs and their backing libraries are — from
2
- // INSIDE the process, so a `--app gjs` bundle needs no launcher and no environment.
3
- // Why it exists, what it does NOT cover (a dylib reached through another dylib's
4
- // link closure never passes through GI, so only `@rpath`/`$ORIGIN` reaches it) and
5
- // the macOS measurements behind it: `status/open-todos.md` § "A globally installed
6
- // GJS launcher still cannot load a system GTK on macOS", ADR 0023 § 4.
1
+ // SPDX-License-Identifier: MIT
2
+ // The GI runtime search-path prologue for `--app gjs` bundles: the only repair shape
3
+ // that survives macOS (a typelib names its library by bare leaf, and the link closure
4
+ // never passes through GI, so only `@rpath`/`$ORIGIN` reaches it). Measurements:
5
+ // `status/open-todos.md` § "A globally installed GJS launcher still cannot load a
6
+ // system GTK on macOS", ADR 0023 § 4.
7
+ //
8
+ // WHAT IT REACHES, MEASURED (gjs 1.88.1, linux; pinned by the `gi-runtime-prologue`
9
+ // e2e): a banner is the entry chunk's BODY and ESM evaluates imports first, so a
10
+ // STATIC `import … from 'gi://Ns'` has already loaded its typelib before this runs.
11
+ // What is left is what loads LATER — `await import('gi://Soup')` and the other
12
+ // optional namespaces. Reaching the static ones changes how a bundle acquires GI
13
+ // namespaces at all; that is an ADR, filed in the same entry.
14
+ //
15
+ // TWO KINDS OF DIRECTORY, split by WHOSE FACT each is. `dirs` describe the SHIPPED
16
+ // TREE, so they are baked, relative to the program. `systemProbes` describe the host
17
+ // that will RUN it, which the build machine cannot know — so the candidate travels
18
+ // and is dropped on the host when it names nothing.
7
19
  /**
8
- * The byte-1 prologue that prepends `dirs` to the default repository's typelib
9
- * search path and library path. Empty list → `''`: a no-op prologue in every
10
- * bundle is a guard that can never fire, and a banner that is not there cannot
11
- * drift.
20
+ * The byte-1 prologue that prepends directories to the default repository's typelib
21
+ * search path and library path. Nothing to prepend → `''`: a no-op prologue in every
22
+ * bundle is a guard that can never fire, and a banner that is not there cannot drift.
23
+ *
24
+ * Prepending REVERSES a list, so both loops count down and the emitted order is the
25
+ * order given. `systemProbes` are applied first and therefore end up BEHIND `dirs`:
26
+ * ADR 0023's bundle-before-system precedence, which #910 paid for.
12
27
  *
13
28
  * @param dirs directories RELATIVE to the program's own directory, `/`-separated —
14
29
  * an absolute path baked at build time is the BUILD machine's
30
+ * @param systemProbes absolute directories prepended only where BOTH of a probe's
31
+ * marker paths exist on the RUNNING host; the markers are passed in so the
32
+ * `girepository-1.0` layout rule and the platform scope around it keep their single
33
+ * definition in the caller
15
34
  */
16
- export function giRuntimePathsStub(dirs) {
17
- if (dirs.length === 0)
35
+ export function giRuntimePathsStub(dirs, systemProbes = []) {
36
+ if (dirs.length === 0 && systemProbes.length === 0)
18
37
  return '';
19
38
  const list = dirs.map((d) => JSON.stringify(d)).join(',');
39
+ const probes = systemProbes.map((probe) => `[${probe.map((v) => JSON.stringify(v)).join(',')}]`).join(',');
20
40
  // Four rules, each pinned with its incident by `gi-runtime-paths-banner.spec.ts`:
21
- // every ambient global through `globalThis.` (a bare `imports` at byte 1 binds to
22
- // a bundled module's own top-level `const imports`, still in its TDZ); guarded
23
- // probes, not try/catch (no call here has a throw path, so a catch would hide a
24
- // real absence); one line (the banner precedes source-map-aware machinery); both
25
- // prepends (search path alone finds the typelib, then fails the dlopen).
26
- return (`(function(){var i=globalThis.imports;if(!i||!i.gi||!i.system)return;` +
27
- `var R=i.gi.GIRepository;if(!R||!R.Repository||!R.Repository.dup_default)return;` +
41
+ // every ambient global through `globalThis.` (a bare `imports` at byte 1 binds to a
42
+ // bundled module's own top-level `const imports`, still in its TDZ); capability
43
+ // guards rather than try/catch for every CALL, since none has a throw path in the
44
+ // GIR; one line (the banner precedes source-map-aware machinery); both prepends
45
+ // (search path alone finds the typelib, then fails the dlopen).
46
+ //
47
+ // The ONE try wraps a namespace LOAD, the only operation here that does throw:
48
+ // distributions ship the GIRepository typelib separately from the girepository
49
+ // library GJS links against (Debian's `gir1.2-girepository-3.0` vs
50
+ // `libgirepository-2.0-0`), so a host with only the second raises "Typelib file
51
+ // for namespace 'GIRepository' (any version) not found" — measured, and why
52
+ // `activateNativePrebuilds()` wraps the same access. At byte 1 that throw takes
53
+ // down a program that ran fine while this prologue was the empty string. Not
54
+ // logged, for that function's reason: every outcome leaves the caller where it
55
+ // was before the prologue existed, so no host is made worse.
56
+ const head = `(function(){var i=globalThis.imports;if(!i||!i.gi)return;` +
57
+ `var R;try{R=i.gi.GIRepository}catch(e){return}` +
58
+ `if(!R||!R.Repository||!R.Repository.dup_default)return;` +
28
59
  `var r=R.Repository.dup_default();if(!r||!r.prepend_search_path||!r.prepend_library_path)return;` +
29
- `var p=i.system.programPath||i.system.programInvocationName||'';` +
30
- // Both separators: the program path is the HOST's, and on win32 a `/`-only
31
- // cut matches nothing (same rule as `lastPathSeparatorIndex`, #1143).
32
- `var m=/^(.*)[\\/\\\\][^\\/\\\\]*$/.exec(p);if(!m)return;` +
33
- // Absolute stays absolute: joining a system libdir under the program dir names nothing.
34
- `var b=m[1];for(var d of [${list}]){var f=/^([A-Za-z]:[\\/\\\\]|[\\/\\\\])/.test(d)?d:b+'/'+d;` +
35
- `r.prepend_search_path(f);r.prepend_library_path(f)}` +
36
- `})();`);
60
+ `var a=function(f){r.prepend_search_path(f);r.prepend_library_path(f)};`;
61
+ // GLib gets its own try for the same reason, separate so a host that cannot
62
+ // introspect it still gets the relative half.
63
+ //
64
+ // The HOST marker is tested first and short-circuits: on a host the candidate does
65
+ // not belong to, the whole table costs one `stat` per entry and changes nothing.
66
+ const probeHalf = probes.length === 0
67
+ ? ''
68
+ : `var G;try{G=i.gi.GLib}catch(e){}` +
69
+ `if(G&&G.file_test&&G.FileTest){var S=[${probes}];` +
70
+ `for(var j=S.length;j--;)` +
71
+ `if(G.file_test(S[j][2],G.FileTest.EXISTS)&&G.file_test(S[j][1],G.FileTest.IS_DIR))a(S[j][0])}`;
72
+ // Guards rather than returns, so a program path with no separator (`gjs -c`) still
73
+ // leaves the system half applied.
74
+ const relativeHalf = list.length === 0
75
+ ? ''
76
+ : `var p=(i.system&&(i.system.programPath||i.system.programInvocationName))||'';` +
77
+ // Both separators: the program path is the HOST's, and on win32 a `/`-only
78
+ // cut matches nothing (same rule as `lastPathSeparatorIndex`, #1143).
79
+ `var m=/^(.*)[\\/\\\\][^\\/\\\\]*$/.exec(p);if(m){var b=m[1];var D=[${list}];` +
80
+ `for(var k=D.length;k--;){var d=D[k];` +
81
+ // Absolute stays absolute: joining a system libdir under the program dir names nothing.
82
+ `var q=/^([A-Za-z]:[\\/\\\\]|[\\/\\\\])/.test(d)?d:b+'/'+d;a(q)}}`;
83
+ return `${head}${probeHalf}${relativeHalf}})();`;
37
84
  }
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from 'rolldown';
2
+ import { type GiSystemProbe } from './gi-runtime-paths.js';
2
3
  export declare const GJS_PROCESS_STUB: string;
3
4
  /**
4
5
  * Compose the GJS process stub with the user-supplied banner so the result is
@@ -23,6 +24,12 @@ export interface ProcessStubPluginOptions {
23
24
  * Empty or unset emits nothing.
24
25
  */
25
26
  giRuntimePaths?: readonly string[];
27
+ /**
28
+ * Absolute system GI library directories, each paired with the path that proves the
29
+ * host really has one. Prepended BEHIND {@link giRuntimePaths} and only where the
30
+ * marker exists at RUNTIME. Empty or unset emits nothing.
31
+ */
32
+ giSystemProbes?: readonly GiSystemProbe[];
26
33
  }
27
34
  /** Inject the byte-1 banner as a chunk banner, after any user `output.banner`. */
28
35
  export declare function processStubPlugin(options?: ProcessStubPluginOptions): Plugin;
@@ -85,7 +85,7 @@ export function processStubPlugin(options = {}) {
85
85
  const stub = (options.captureBundleUrl ? BUNDLE_URL_BANNER : '') +
86
86
  GJS_WELLKNOWN_SYMBOLS_STUB +
87
87
  GJS_PROCESS_STUB +
88
- giRuntimePathsStub(options.giRuntimePaths ?? []);
88
+ giRuntimePathsStub(options.giRuntimePaths ?? [], options.giSystemProbes ?? []);
89
89
  const banner = composeBanner(stub, options.userBanner ?? '');
90
90
  return {
91
91
  name: 'gjsify-process-stub',
@@ -2,6 +2,36 @@ import type { Plugin } from 'rolldown';
2
2
  export declare const REWRITE_FILTER: RegExp;
3
3
  /** True when the rewriter wants to look at this path — node_modules + supported ext. */
4
4
  export declare function shouldRewrite(path: string): boolean;
5
+ /**
6
+ * True when the static-read INLINER wants to look at this path — any supported
7
+ * source, first-party included.
8
+ *
9
+ * Deliberately wider than {@link shouldRewrite}, because the two answer different
10
+ * questions. The inliner asks *is this read resolvable at build time*, which is a
11
+ * property of the expression and can be true in any file. The rewriter asks *does
12
+ * this file's `import.meta.url` still mean anything once bundled*, which is only
13
+ * worth asking of a file whose original location was `node_modules` — the whole
14
+ * subject of this plugin.
15
+ *
16
+ * They were ONE gate until 2026-08-22, and the cost was silent. Each of
17
+ * `@gjsify/cli`'s own six `readFileSync(new URL(<literal>, import.meta.url))`
18
+ * template loaders shipped into `dist/cli.gjs.mjs` as a LIVE read pointing at a
19
+ * `templates/` directory that exists neither in the repo (the sources are in
20
+ * `src/templates/`; the build copies them to `lib/templates/`) nor in the
21
+ * published tarball (`files` ships `lib`, not a root `templates`). Measured on a
22
+ * fresh build: `gjs -m dist/cli.gjs.mjs ship --stage` died with
23
+ * `ENOENT … packages/infra/cli/templates/app/desktop.tmpl` while the identical
24
+ * command through `lib/index.js` staged the desktop entry. Two of those six files
25
+ * carry a comment asserting the inliner handles them, so reading the code
26
+ * confirmed the belief rather than the behaviour.
27
+ *
28
+ * Consequence for a CONSUMER's build, stated because it is a behaviour change: a
29
+ * statically resolvable read in their own source is baked into their bundle too.
30
+ * That is the promise the inliner exists to make — a bundle that survives leaving
31
+ * its build site — and a read meant to stay dynamic keeps working by not being
32
+ * statically resolvable, since anything the evaluator cannot fold is left alone.
33
+ */
34
+ export declare function shouldInline(path: string): boolean;
5
35
  /** The directory the bundle's outfile lives in, from `output.file` or `output.dir`. */
6
36
  export declare function getBundleDirFromOutput(opts: {
7
37
  file?: string;
@@ -48,5 +78,10 @@ export interface NodeModulesPathRewriteOptions {
48
78
  /**
49
79
  * Build a Rolldown plugin that runs the path rewriter as a `transform(code, id)`
50
80
  * hook with `order: 'post'`.
81
+ *
82
+ * Two scopes in one transform, not two plugins: a second transform would parse
83
+ * every module a second time, and the inliner's own fast path (skip unless the
84
+ * source mentions `readFileSync`/`readdirSync`/`existsSync`) already makes the
85
+ * wider scope nearly free. See {@link shouldInline} for why the scopes differ.
51
86
  */
52
87
  export declare function nodeModulesPathRewritePlugin(options: NodeModulesPathRewriteOptions): Plugin;
@@ -18,6 +18,12 @@
18
18
  //
19
19
  // Hosted as a `transform(code, id)` hook with `order: 'post'`: after the
20
20
  // deepkit/blueprint/css pre-transforms, still during module loading, before chunking.
21
+ //
22
+ // TWO SCOPES, one transform. The four cases above are node_modules-only, because
23
+ // they are about a file whose recorded location stopped being true. The static-read
24
+ // INLINER that runs alongside them is not: it folds build-time-resolvable reads in
25
+ // ANY source, first-party included, and it was trapped inside the node_modules gate
26
+ // until 2026-08-22 — see `shouldInline` for the incident that measured the cost.
21
27
  import { createRequire } from 'node:module';
22
28
  import { dirname, join, relative, resolve } from 'node:path';
23
29
  import { inlineStaticReads } from '../utils/inline-static-reads.js';
@@ -39,6 +45,40 @@ export function shouldRewrite(path) {
39
45
  return false;
40
46
  return true;
41
47
  }
48
+ /**
49
+ * True when the static-read INLINER wants to look at this path — any supported
50
+ * source, first-party included.
51
+ *
52
+ * Deliberately wider than {@link shouldRewrite}, because the two answer different
53
+ * questions. The inliner asks *is this read resolvable at build time*, which is a
54
+ * property of the expression and can be true in any file. The rewriter asks *does
55
+ * this file's `import.meta.url` still mean anything once bundled*, which is only
56
+ * worth asking of a file whose original location was `node_modules` — the whole
57
+ * subject of this plugin.
58
+ *
59
+ * They were ONE gate until 2026-08-22, and the cost was silent. Each of
60
+ * `@gjsify/cli`'s own six `readFileSync(new URL(<literal>, import.meta.url))`
61
+ * template loaders shipped into `dist/cli.gjs.mjs` as a LIVE read pointing at a
62
+ * `templates/` directory that exists neither in the repo (the sources are in
63
+ * `src/templates/`; the build copies them to `lib/templates/`) nor in the
64
+ * published tarball (`files` ships `lib`, not a root `templates`). Measured on a
65
+ * fresh build: `gjs -m dist/cli.gjs.mjs ship --stage` died with
66
+ * `ENOENT … packages/infra/cli/templates/app/desktop.tmpl` while the identical
67
+ * command through `lib/index.js` staged the desktop entry. Two of those six files
68
+ * carry a comment asserting the inliner handles them, so reading the code
69
+ * confirmed the belief rather than the behaviour.
70
+ *
71
+ * Consequence for a CONSUMER's build, stated because it is a behaviour change: a
72
+ * statically resolvable read in their own source is baked into their bundle too.
73
+ * That is the promise the inliner exists to make — a bundle that survives leaving
74
+ * its build site — and a read meant to stay dynamic keeps working by not being
75
+ * statically resolvable, since anything the evaluator cannot fold is left alone.
76
+ */
77
+ export function shouldInline(path) {
78
+ if (!REWRITE_FILTER.test(path))
79
+ return false;
80
+ return !GJSIFY_SHIM_RE.test(path);
81
+ }
42
82
  /** The directory the bundle's outfile lives in, from `output.file` or `output.dir`. */
43
83
  export function getBundleDirFromOutput(opts) {
44
84
  const outFile = opts.file ?? join(opts.dir ?? '.', 'bundle.mjs');
@@ -265,6 +305,11 @@ export function rewriteContents(args, srcInput, bundleDir, runtimeResolve) {
265
305
  /**
266
306
  * Build a Rolldown plugin that runs the path rewriter as a `transform(code, id)`
267
307
  * hook with `order: 'post'`.
308
+ *
309
+ * Two scopes in one transform, not two plugins: a second transform would parse
310
+ * every module a second time, and the inliner's own fast path (skip unless the
311
+ * source mentions `readFileSync`/`readdirSync`/`existsSync`) already makes the
312
+ * wider scope nearly free. See {@link shouldInline} for why the scopes differ.
268
313
  */
269
314
  export function nodeModulesPathRewritePlugin(options) {
270
315
  const runtimeResolve = options.runtimeResolve ?? false;
@@ -274,12 +319,19 @@ export function nodeModulesPathRewritePlugin(options) {
274
319
  order: 'post',
275
320
  filter: { id: REWRITE_FILTER },
276
321
  handler(code, id) {
277
- if (!id.includes('node_modules'))
278
- return null;
279
- const result = rewriteContents({ path: id }, code, options.bundleDir, runtimeResolve);
280
- if (!result)
322
+ if (id.includes('node_modules')) {
323
+ const result = rewriteContents({ path: id }, code, options.bundleDir, runtimeResolve);
324
+ if (!result)
325
+ return null;
326
+ return { code: result.code, map: null };
327
+ }
328
+ // First-party source: inline statically resolvable reads, and
329
+ // nothing else. `import.meta.url` here is not rewritten — that
330
+ // is the node_modules question this plugin was written for.
331
+ if (!shouldInline(id))
281
332
  return null;
282
- return { code: result.code, map: null };
333
+ const inlined = inlineStaticReads(code, id);
334
+ return inlined.inlined > 0 ? { code: inlined.contents, map: null } : null;
283
335
  },
284
336
  },
285
337
  };
@@ -16,6 +16,43 @@ export interface WorkspaceImportGuardOptions {
16
16
  * (plus exact-name membership) so the two cannot drift.
17
17
  */
18
18
  isExternal?: (id: string) => boolean;
19
+ /**
20
+ * Last-resort anchor for a `@gjsify/*` the PROJECT cannot resolve — set only
21
+ * when the thing being bundled is TOOLCHAIN rather than user code, and unset
22
+ * for every ordinary `gjsify build`.
23
+ *
24
+ * A repo build script (`node scripts/x.mjs`, re-entered as `gjsify run
25
+ * --node-script` on a Node-less host) does not need the WORKSPACE's copy of
26
+ * `@gjsify/fs` — it needs a working one. Anchored at the script, resolution
27
+ * finds the workspace symlink, whose `lib/esm` a cold clone has not built
28
+ * yet; anchored additionally at the running CLI, it finds the copy installed
29
+ * beside it, which is built by construction.
30
+ *
31
+ * Measured on postmarketOS/aarch64 (gjs 1.88.1, no node): without this the
32
+ * ADR 0002 bootstrap fails at its third step, because `--globals auto` pulls
33
+ * the whole `@gjsify/<pkg>/register` closure into a one-line script (the
34
+ * count and the incident are in this package's AGENTS.md § toolchainAnchor).
35
+ * The alternative is building most of the polyfill tree before the first
36
+ * script runs, on every host including the ones that never bundle one.
37
+ *
38
+ * The value is a FILE PATH inside the running CLI's own install, used as the
39
+ * IMPORTER of the fallback resolve — not a second resolver. That matters:
40
+ * `--app gjs` deliberately omits both `import` and `require` from
41
+ * `conditionNames` (`app/gjs.ts`), and a `createRequire().resolve()` here
42
+ * would resolve under `require` and hand a `cjs-compat.cjs` shim to a GJS
43
+ * bundle for the ~3 `@gjsify/*` packages that declare a `require` branch.
44
+ * Going back through `this.resolve` uses the target's own condition set by
45
+ * construction, so the rescue can only pick the entry the build would have
46
+ * picked. The precedent is `commands/tsc.ts`, which resolves
47
+ * `@gjsify/tsc/bundle` from the project first and the running CLI second for
48
+ * exactly this reason; `tests/e2e/node-free-bootstrap` states that invariant.
49
+ *
50
+ * PROJECT FIRST, ALWAYS: consulted only after normal resolution returns
51
+ * null, so a workspace that HAS the package keeps using it and nothing about
52
+ * a warm tree changes. A project resolve that ERRORED is not a null and is
53
+ * never rescued — see the handler.
54
+ */
55
+ toolchainAnchor?: string;
19
56
  }
20
57
  /**
21
58
  * Is `id` a bare `@gjsify/*` package specifier (root or subpath)? Excludes disk
@@ -62,11 +99,16 @@ export interface UnresolvedWorkspaceImportDetails {
62
99
  */
63
100
  aliasedFrom?: readonly string[];
64
101
  importer: string;
102
+ /**
103
+ * Set when `this.resolve` THREW instead of answering — the message then names
104
+ * it rather than listing three causes that cannot apply.
105
+ */
106
+ resolverFailure?: string;
65
107
  }
66
108
  /** A `@gjsify/*` edge the build promised to substitute could not be resolved. */
67
109
  export declare class UnresolvedWorkspaceImportError extends Error {
68
110
  readonly details: UnresolvedWorkspaceImportDetails;
69
- constructor(details: UnresolvedWorkspaceImportDetails);
111
+ constructor(details: UnresolvedWorkspaceImportDetails, cause?: unknown);
70
112
  }
71
113
  /**
72
114
  * The actionable message: names the specifier, the importer and the likely cause —
@@ -82,6 +124,10 @@ export declare function buildReverseAliasIndex(aliases: Record<string, string> |
82
124
  * imports-empty/externals at normal) gets first refusal — only ids NOTHING claimed
83
125
  * reach this hook. It then re-runs the resolution Rolldown would have run next
84
126
  * (`this.resolve(…, { skipSelf })`) and returns that, so a healthy build resolves
85
- * each specifier exactly once and behaves identically. Only a genuine `null` throws.
127
+ * each specifier exactly once and behaves identically.
128
+ *
129
+ * A genuine `null` throws — unless `toolchainAnchor` is set and the CLI's own install
130
+ * has it. A THROW is not a `null`: it is rethrown with this plugin's context and the
131
+ * original as `cause`, never rescued and never reported as a missing dependency.
86
132
  */
87
133
  export declare function unresolvedWorkspaceImportPlugin(options: WorkspaceImportGuardOptions): Plugin;
@@ -72,8 +72,8 @@ export function classifyImport(input) {
72
72
  /** A `@gjsify/*` edge the build promised to substitute could not be resolved. */
73
73
  export class UnresolvedWorkspaceImportError extends Error {
74
74
  details;
75
- constructor(details) {
76
- super(formatUnresolvedWorkspaceImport(details));
75
+ constructor(details, cause) {
76
+ super(formatUnresolvedWorkspaceImport(details), cause === undefined ? undefined : { cause });
77
77
  this.name = 'UnresolvedWorkspaceImportError';
78
78
  this.details = details;
79
79
  }
@@ -83,7 +83,7 @@ export class UnresolvedWorkspaceImportError extends Error {
83
83
  * the three things `UNRESOLVED_IMPORT` and the post-hoc `node:` guard cannot.
84
84
  */
85
85
  export function formatUnresolvedWorkspaceImport(details) {
86
- const { target, source, candidate, aliasTarget, aliasedFrom, importer } = details;
86
+ const { target, source, candidate, aliasTarget, aliasedFrom, importer, resolverFailure } = details;
87
87
  // The specifiers `candidate` stood in for: `source` when IT carried the table
88
88
  // entry, else the reverse lookup — but only when its fan-in is SMALL. A shared
89
89
  // sink inverts to a wall of text (`@gjsify/empty` is the browser target for 49
@@ -114,11 +114,18 @@ export function formatUnresolvedWorkspaceImport(details) {
114
114
  `\`--app ${target}\` runtime cannot resolve.`
115
115
  : `Leaving it unresolved would silently externalise the ORIGINAL ${substitutedFor.length === 1 ? 'specifier' : 'specifiers'} ${quoted} into the ` +
116
116
  `bundle, defeating the substitution the \`--app ${target}\` target exists to perform.`) + symptom;
117
+ // A resolver that FAILED is not a resolver that found nothing, and the three
118
+ // likely causes below are all wrong for it: an EACCES on `node_modules`, a
119
+ // corrupt `package.json`, or a deliberate throw from another `resolveId` hook
120
+ // (`napi-node-addon`'s `resolveAddonPath`) would otherwise be reported as the
121
+ // user's missing dependency, with the real error gone from the process.
122
+ const failure = resolverFailure === undefined ? [] : ['', `The project resolver failed with: ${resolverFailure}`];
117
123
  return [
118
124
  headline,
119
125
  ` imported by: ${importer}`,
120
126
  '',
121
127
  consequence,
128
+ ...failure,
122
129
  '',
123
130
  'Likely causes:',
124
131
  ' - `node_modules` is missing or incomplete — run `gjsify install` (`--immutable` in CI).',
@@ -143,6 +150,10 @@ export function buildReverseAliasIndex(aliases) {
143
150
  list.sort();
144
151
  return index;
145
152
  }
153
+ /** One line for the message; the untouched error rides along as `cause`. */
154
+ function describeResolverFailure(err) {
155
+ return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
156
+ }
146
157
  /**
147
158
  * Refuse a build whose `@gjsify/*` substitution could not be resolved.
148
159
  *
@@ -150,10 +161,14 @@ export function buildReverseAliasIndex(aliases) {
150
161
  * imports-empty/externals at normal) gets first refusal — only ids NOTHING claimed
151
162
  * reach this hook. It then re-runs the resolution Rolldown would have run next
152
163
  * (`this.resolve(…, { skipSelf })`) and returns that, so a healthy build resolves
153
- * each specifier exactly once and behaves identically. Only a genuine `null` throws.
164
+ * each specifier exactly once and behaves identically.
165
+ *
166
+ * A genuine `null` throws — unless `toolchainAnchor` is set and the CLI's own install
167
+ * has it. A THROW is not a `null`: it is rethrown with this plugin's context and the
168
+ * original as `cause`, never rescued and never reported as a missing dependency.
154
169
  */
155
170
  export function unresolvedWorkspaceImportPlugin(options) {
156
- const { target, aliases, isExternal } = options;
171
+ const { target, aliases, isExternal, toolchainAnchor } = options;
157
172
  // Re-entrancy belt: `skipSelf: true` should keep our own `this.resolve` out of
158
173
  // this hook, but a hang is a far worse failure than a missed check. Keyed by
159
174
  // `candidate\0importer` — the same package legitimately resolves from many.
@@ -185,6 +200,12 @@ export function unresolvedWorkspaceImportPlugin(options) {
185
200
  const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
186
201
  if (extraOptions?.isEntry)
187
202
  return null;
203
+ // The toolchain fallback below re-enters this hook with the anchor as
204
+ // the importer. Declining there keeps the probe from rescuing itself:
205
+ // the anchor cannot answer for the anchor, and the recursion has no
206
+ // bound of its own.
207
+ if (toolchainAnchor !== undefined && importer === toolchainAnchor)
208
+ return null;
188
209
  const verdict = classifyImport({ source, importer, aliases, isExternal });
189
210
  if (verdict.verdict === 'ignore')
190
211
  return null;
@@ -198,13 +219,29 @@ export function unresolvedWorkspaceImportPlugin(options) {
198
219
  if (inFlight.has(key))
199
220
  return null;
200
221
  inFlight.add(key);
201
- let resolved;
222
+ let resolved = null;
223
+ // KEPT DELIBERATELY, and NOT as a miss. `this.resolve` re-runs the whole
224
+ // `pre`-order chain, so it can fail two very different ways. "Nothing is
225
+ // there" is `null` on both engines — npm `rolldown` always answered that,
226
+ // and `@gjsify/rolldown-native` now does too (`isResolveMiss`, which is
227
+ // where the engine mismatch that hid this plugin's diagnostic under GJS
228
+ // is fixed). What reaches this catch is the OTHER kind: a hook in that
229
+ // chain throwing on purpose (`napi-node-addon`'s `resolveAddonPath`), an
230
+ // EACCES, a corrupt `package.json`, a bridge fault. Letting it escape
231
+ // surfaces it as a bare "plugin `gjsify-alias` threw an error" naming
232
+ // neither specifier nor importer; swallowing it would report a real fault
233
+ // as the user's missing dependency. So it is caught, kept, and thrown
234
+ // BELOW with this plugin's context around it and the original as `cause`.
235
+ let resolverFailure;
202
236
  try {
203
237
  resolved = await this.resolve(verdict.candidate, importer, {
204
238
  skipSelf: true,
205
239
  kind: extraOptions?.kind,
206
240
  });
207
241
  }
242
+ catch (err) {
243
+ resolverFailure = err;
244
+ }
208
245
  finally {
209
246
  inFlight.delete(key);
210
247
  }
@@ -212,6 +249,30 @@ export function unresolvedWorkspaceImportPlugin(options) {
212
249
  resolvedCache.set(key, resolved);
213
250
  return resolved;
214
251
  }
252
+ // Toolchain fallback — see `toolchainAnchor`. Only after a real `null`:
253
+ // a resolver that ERRORED has not established that the project lacks the
254
+ // package, and rescuing there would turn "project first, always" into
255
+ // "project first unless the project errors".
256
+ if (resolverFailure === undefined && toolchainAnchor !== undefined) {
257
+ const fromToolchain = await this.resolve(verdict.candidate, toolchainAnchor, {
258
+ skipSelf: true,
259
+ kind: extraOptions?.kind,
260
+ });
261
+ if (fromToolchain) {
262
+ // The one visible trace that this artifact is MIXED — some of its
263
+ // `@gjsify/*` from the project, this one from the CLI's own
264
+ // install, possibly a different major. Without it a rescued build
265
+ // and a healthy one have byte-identical logs, and a wrong answer
266
+ // that leaves no trace is the expensive kind. Deduplicated by
267
+ // `resolvedCache`: one line per (candidate, importer dir, kind).
268
+ this.warn(`gjsify: \`${verdict.candidate}\` did not resolve from the project ` +
269
+ `(imported by ${importer ?? '<entry>'}) — using the copy installed beside the ` +
270
+ `running CLI (${toolchainAnchor}). Build the workspace package to use the ` +
271
+ `project's own copy.`);
272
+ resolvedCache.set(key, fromToolchain);
273
+ return fromToolchain;
274
+ }
275
+ }
215
276
  throw new UnresolvedWorkspaceImportError({
216
277
  target,
217
278
  source,
@@ -219,7 +280,10 @@ export function unresolvedWorkspaceImportPlugin(options) {
219
280
  aliasTarget: verdict.aliasTarget,
220
281
  aliasedFrom: reverseAliases.get(verdict.candidate)?.filter((s) => s !== source),
221
282
  importer: importer ?? '<entry>',
222
- });
283
+ ...(resolverFailure === undefined
284
+ ? {}
285
+ : { resolverFailure: describeResolverFailure(resolverFailure) }),
286
+ }, resolverFailure);
223
287
  },
224
288
  },
225
289
  };
@@ -67,4 +67,16 @@ export interface PluginOptions {
67
67
  * the wrapper. Defaults to `false`.
68
68
  */
69
69
  preserveDefaultExport?: boolean;
70
+ /**
71
+ * Where to look for a `@gjsify/*` the PROJECT cannot resolve, when the input is
72
+ * TOOLCHAIN rather than user code: a file path inside the running CLI's own
73
+ * install, used as the importer of a second resolve.
74
+ *
75
+ * OPT-IN PER CALL SITE, never inferred from the code path. `gjsify build` never
76
+ * sets it, and neither does a `node <file>` in an ordinary project's package
77
+ * script — a user's application must still fail loudly on a missing dependency
78
+ * rather than silently pick up whatever version the CLI ships. Rationale and the
79
+ * measured incident: `WorkspaceImportGuardOptions.toolchainAnchor`.
80
+ */
81
+ toolchainAnchor?: string;
70
82
  }
@@ -1,10 +1,3 @@
1
- /**
2
- * Run the inliner on a source string. Returns the rewritten source (or the
3
- * original string when no inlining applied) and the count of edits applied.
4
- *
5
- * Safe to call on any JS source. Files that don't reference `readFileSync` /
6
- * `readdirSync` / `existsSync` skip the AST parse entirely (cheap fast path).
7
- */
8
1
  export declare function inlineStaticReads(src: string, sourceFilePath: string): {
9
2
  contents: string;
10
3
  inlined: number;
@@ -41,7 +41,7 @@
41
41
  // Anything not statically resolvable is left untouched — the legacy
42
42
  // `import.meta.url` rewriter still applies as a fallback.
43
43
  import * as acorn from 'acorn';
44
- import * as walk from 'acorn-walk';
44
+ import { tsPlugin } from 'acorn-typescript';
45
45
  import { dirname, join, resolve, basename, relative, extname, posix, win32 } from 'node:path';
46
46
  import { fileURLToPath, pathToFileURL } from 'node:url';
47
47
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
@@ -52,35 +52,98 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
52
52
  * Safe to call on any JS source. Files that don't reference `readFileSync` /
53
53
  * `readdirSync` / `existsSync` skip the AST parse entirely (cheap fast path).
54
54
  */
55
+ const TS_EXT_RE = /\.[cm]?tsx?$/i;
56
+ /**
57
+ * The TypeScript-capable parser, built once.
58
+ *
59
+ * `acorn-typescript` and not the bundler's own parser, and the reason is the
60
+ * runtime rather than a preference: under GJS the engine is
61
+ * `@gjsify/rolldown-native`, and npm `rolldown` is "a Rust napi crate that cannot
62
+ * run under GJS" (`bundler-pick.ts`), loaded only behind a dynamic import on Node.
63
+ * A top-level `import { parseAst } from 'rolldown/parseAst'` therefore links a
64
+ * Node-only N-API package into a module that has to load under GJS — measured:
65
+ * the CLI's own bundle then died at startup with `createRequire: Cannot require
66
+ * builtin module "fs" synchronously in GJS`. `acorn-typescript` is pure JS, emits
67
+ * the same ESTree shapes the evaluator below already speaks, and needs
68
+ * `locations: true` (it refuses to parse without them).
69
+ */
70
+ const TS_PARSER = acorn.Parser.extend(tsPlugin());
71
+ /**
72
+ * Parse a source with the parser its extension calls for.
73
+ *
74
+ * The TypeScript half is why first-party sources were invisible to this inliner
75
+ * for as long as it was scoped to `node_modules`: an installed package ships JS,
76
+ * so plain acorn could always parse it, and nothing ever asked what happens to a
77
+ * `.ts`. The answer was that `acorn.parse` threw and the `catch` returned
78
+ * "nothing to inline" — a result indistinguishable from a file that genuinely has
79
+ * no static reads. Measured on `packages/infra/cli/src/utils/app-metadata.ts`:
80
+ * `inlined: 0`, while the identical expression in a `.js` file returned
81
+ * `inlined: 1`.
82
+ */
83
+ function parseSource(src, sourceFilePath) {
84
+ const shared = {
85
+ ecmaVersion: 'latest',
86
+ sourceType: 'module',
87
+ allowAwaitOutsideFunction: true,
88
+ allowReturnOutsideFunction: true,
89
+ allowImportExportEverywhere: true,
90
+ };
91
+ if (!TS_EXT_RE.test(sourceFilePath))
92
+ return acorn.parse(src, shared);
93
+ return TS_PARSER.parse(src, { ...shared, locations: true });
94
+ }
95
+ /**
96
+ * Visit every `CallExpression` in the tree.
97
+ *
98
+ * A generic descent rather than `acorn-walk`, and that is not a preference:
99
+ * `walk.simple` looks its visitor up in a table keyed by node type and throws
100
+ * `No walker function defined for node type TSInterfaceDeclaration` on the first
101
+ * interface in a first-party source. A walker with no node table cannot fall
102
+ * behind the parser — which matters precisely because the parser is now allowed
103
+ * to emit node types this file has never heard of.
104
+ */
105
+ function forEachCallExpression(root, visit) {
106
+ const stack = [root];
107
+ while (stack.length > 0) {
108
+ const node = stack.pop();
109
+ if (node === null || typeof node !== 'object')
110
+ continue;
111
+ if (Array.isArray(node)) {
112
+ for (const child of node)
113
+ stack.push(child);
114
+ continue;
115
+ }
116
+ if (node.type === 'CallExpression')
117
+ visit(node);
118
+ for (const value of Object.values(node)) {
119
+ if (value !== null && typeof value === 'object')
120
+ stack.push(value);
121
+ }
122
+ }
123
+ }
55
124
  export function inlineStaticReads(src, sourceFilePath) {
56
125
  if (!src.includes('readFileSync') && !src.includes('readdirSync') && !src.includes('existsSync')) {
57
126
  return { contents: src, inlined: 0 };
58
127
  }
59
128
  let ast;
60
129
  try {
61
- ast = acorn.parse(src, {
62
- ecmaVersion: 'latest',
63
- sourceType: 'module',
64
- allowAwaitOutsideFunction: true,
65
- allowReturnOutsideFunction: true,
66
- allowImportExportEverywhere: true,
67
- });
130
+ ast = parseSource(src, sourceFilePath);
68
131
  }
69
132
  catch {
70
- // Source isn't valid JS (CJS source with shebangs, mixed module
71
- // syntax, ...). Skip; the rest of the rewriter still runs.
133
+ // Not parseable as the language its extension claims (a CJS source with
134
+ // a shebang, mixed module syntax, ...). Skip; the rest of the rewriter
135
+ // still runs. This catch used to swallow every TypeScript file in the
136
+ // tree as well — see `parseSource`.
72
137
  return { contents: src, inlined: 0 };
73
138
  }
74
139
  const ctx = {
75
140
  sourceUrl: pathToFileURL(sourceFilePath).href,
76
141
  };
77
142
  const edits = [];
78
- walk.simple(ast, {
79
- CallExpression(node) {
80
- const edit = tryInlineCall(node, ctx, src);
81
- if (edit)
82
- edits.push(edit);
83
- },
143
+ forEachCallExpression(ast, (node) => {
144
+ const edit = tryInlineCall(node, ctx, src);
145
+ if (edit)
146
+ edits.push(edit);
84
147
  });
85
148
  if (edits.length === 0)
86
149
  return { contents: src, inlined: 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
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,20 +55,21 @@
55
55
  ],
56
56
  "license": "MIT",
57
57
  "dependencies": {
58
- "@gjsify/console": "^0.41.0",
59
- "@gjsify/resolve-npm": "^0.41.0",
60
- "@gjsify/rolldown-plugin-deepkit": "^0.41.0",
61
- "@gjsify/rolldown-plugin-pnp": "^0.41.0",
62
- "@gjsify/vite-plugin-blueprint": "^0.41.0",
58
+ "@gjsify/console": "^0.42.0",
59
+ "@gjsify/resolve-npm": "^0.42.0",
60
+ "@gjsify/rolldown-plugin-deepkit": "^0.42.0",
61
+ "@gjsify/rolldown-plugin-pnp": "^0.42.0",
62
+ "@gjsify/vite-plugin-blueprint": "^0.42.0",
63
63
  "@rollup/pluginutils": "^5.4.0",
64
64
  "acorn": "^8.17.0",
65
+ "acorn-typescript": "^1.4.13",
65
66
  "acorn-walk": "^8.3.5",
66
67
  "fast-glob": "^3.3.3",
67
68
  "lightningcss": "^1.32.0",
68
69
  "sass": "^1.101.0"
69
70
  },
70
71
  "peerDependencies": {
71
- "@gjsify/lightningcss-native": "^0.41.0",
72
+ "@gjsify/lightningcss-native": "^0.42.0",
72
73
  "rolldown": "^1.1.4"
73
74
  },
74
75
  "peerDependenciesMeta": {