@gjsify/rolldown-plugin-gjsify 0.40.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 +8 -0
- package/lib/app/gjs.js +13 -2
- package/lib/app/nativescript.js +12 -9
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -1
- package/lib/plugin.d.ts +10 -0
- package/lib/plugin.js +1 -0
- package/lib/plugins/gi-runtime-paths.d.ts +27 -5
- package/lib/plugins/gi-runtime-paths.js +74 -27
- package/lib/plugins/process-stub.d.ts +7 -0
- package/lib/plugins/process-stub.js +1 -1
- package/lib/plugins/rewrite-node-modules-paths.d.ts +35 -0
- package/lib/plugins/rewrite-node-modules-paths.js +57 -5
- package/lib/plugins/unresolved-workspace-import.d.ts +48 -2
- package/lib/plugins/unresolved-workspace-import.js +71 -7
- package/lib/types/plugin-options.d.ts +12 -0
- package/lib/utils/inline-static-reads.d.ts +0 -7
- package/lib/utils/inline-static-reads.js +79 -16
- package/package.json +8 -7
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({
|
|
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({
|
|
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/app/nativescript.js
CHANGED
|
@@ -21,13 +21,11 @@
|
|
|
21
21
|
// slot routing would only change 4 of its 122 entries and all four are
|
|
22
22
|
// regressions — `fs`/`fs/promises` (+ their `node:` forms) deliberately point
|
|
23
23
|
// at `@gjsify/native-fs-bridge`, which declares `nativescript: "native"` in the
|
|
24
|
-
// "this package IS the native implementation" sense and therefore
|
|
25
|
-
// `globals.mjs` it does not ship
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// before this table is composed.
|
|
29
|
-
// '@gjsify/native-fs-bridge'` already hits the derived map (plain key hit) and
|
|
30
|
-
// is emptied TODAY — same root cause, listed in the same TODO.
|
|
24
|
+
// "this package IS the native implementation" sense and therefore promises a
|
|
25
|
+
// `globals.mjs` it does not ship. The slot vocabulary's `native` means "the
|
|
26
|
+
// RUNTIME provides this, use its value", so the bridge packages are
|
|
27
|
+
// mis-declared; that is tracked in status/open-todos.md and must be settled
|
|
28
|
+
// before this table is composed.
|
|
31
29
|
//
|
|
32
30
|
// Derived `@gjsify/<X>` entries are merged into the map below, so an import by
|
|
33
31
|
// PACKAGE NAME still routes per its declared slot in a single hop.
|
|
@@ -41,9 +39,14 @@ import { globToEntryPoints } from '../utils/entry-points.js';
|
|
|
41
39
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
42
40
|
import { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
|
|
43
41
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
42
|
+
// Never bundled: an app works only with the ONE core instance the runtime boots, and what
|
|
43
|
+
// this target emits is an ENTRY the NS bundler resolves it for. An optional peer absent from
|
|
44
|
+
// the workspace install, it was not resolvable either — every UI-widget bridge failed to build.
|
|
45
|
+
const NATIVESCRIPT_CORE = /^@nativescript\/core(?:\/|$)/;
|
|
44
46
|
export const setupForNativescript = async (input) => {
|
|
45
47
|
const userExternal = input.userExternal ?? [];
|
|
46
|
-
const external = [...userExternal];
|
|
48
|
+
const external = [...userExternal, NATIVESCRIPT_CORE];
|
|
49
|
+
const isExternal = (id) => external.some((entry) => (typeof entry === 'string' ? entry === id : entry.test(id)));
|
|
47
50
|
const exclude = input.pluginOptions.exclude ?? [];
|
|
48
51
|
const entryPoints = await globToEntryPoints(input.input, exclude);
|
|
49
52
|
// Target platform — discovered from the env NS' CLI sets when it spawns a
|
|
@@ -141,7 +144,7 @@ export const setupForNativescript = async (input) => {
|
|
|
141
144
|
unresolvedWorkspaceImportPlugin({
|
|
142
145
|
target: 'nativescript',
|
|
143
146
|
aliases: aliasEntries,
|
|
144
|
-
isExternal
|
|
147
|
+
isExternal,
|
|
145
148
|
}),
|
|
146
149
|
];
|
|
147
150
|
return { options, plugins };
|
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
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
9
|
-
* search path and library path.
|
|
10
|
-
* bundle is a guard that can never fire, and a banner that is not there cannot
|
|
11
|
-
*
|
|
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
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
71
|
-
// syntax, ...). Skip; the rest of the rewriter
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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.
|
|
59
|
-
"@gjsify/resolve-npm": "^0.
|
|
60
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
61
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
62
|
-
"@gjsify/vite-plugin-blueprint": "^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.
|
|
72
|
+
"@gjsify/lightningcss-native": "^0.42.0",
|
|
72
73
|
"rolldown": "^1.1.4"
|
|
73
74
|
},
|
|
74
75
|
"peerDependenciesMeta": {
|