@gjsify/rolldown-plugin-gjsify 0.51.1 → 0.53.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/README.md +11 -0
- package/lib/app/browser.js +28 -0
- package/lib/app/gjs.js +14 -2
- package/lib/app/nativescript.js +15 -3
- package/lib/app/node.d.ts +1 -1
- package/lib/app/node.js +26 -10
- package/lib/index.d.ts +4 -2
- package/lib/index.js +4 -2
- package/lib/plugins/console-assign.d.ts +13 -0
- package/lib/plugins/console-assign.js +125 -0
- package/lib/plugins/css-as-string.js +126 -44
- package/lib/plugins/gi-renderer.d.ts +1 -1
- package/lib/plugins/gi-renderer.js +1 -1
- package/lib/plugins/gi-runtime-paths.js +5 -2
- package/lib/plugins/napi-node-addon.d.ts +48 -4
- package/lib/plugins/napi-node-addon.js +261 -85
- package/lib/plugins/node-native-external.d.ts +20 -0
- package/lib/plugins/node-native-external.js +145 -0
- package/lib/plugins/platform-resolve.d.ts +95 -5
- package/lib/plugins/platform-resolve.js +132 -11
- package/lib/plugins/rewrite-node-modules-paths.d.ts +18 -1
- package/lib/plugins/rewrite-node-modules-paths.js +181 -25
- package/lib/plugins/unresolved-workspace-import.js +89 -68
- package/lib/shims/addon-resolve.d.ts +19 -0
- package/lib/shims/addon-resolve.js +193 -0
- package/lib/utils/addon-platform.d.ts +23 -0
- package/lib/utils/addon-platform.js +50 -0
- package/lib/utils/auto-globals.d.ts +6 -0
- package/lib/utils/auto-globals.js +92 -15
- package/lib/utils/declare-build-input.d.ts +13 -0
- package/lib/utils/declare-build-input.js +37 -0
- package/lib/utils/detect-free-globals.d.ts +3 -0
- package/lib/utils/detect-free-globals.js +1 -1
- package/lib/utils/entry-wrapper.js +6 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/inline-static-reads.d.ts +15 -1
- package/lib/utils/inline-static-reads.js +16 -8
- package/lib/utils/runtime.js +1 -1
- package/lib/utils/scan-globals.d.ts +2 -2
- package/lib/utils/scan-globals.js +20 -14
- package/lib/utils/zip-path.d.ts +9 -0
- package/lib/utils/zip-path.js +12 -0
- package/package.json +13 -9
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Runtime resolver for a GJS bundle's Node-API addon, ADR 0084.
|
|
2
|
+
//
|
|
3
|
+
// The build enumerates every `.node` an addon package ships, keyed by
|
|
4
|
+
// platform (`linux-x64`, `linux-x64-musl`, `darwin-arm64`, …), and bakes the
|
|
5
|
+
// table into the bundle. At RUN time the bundle picks the entry for the host
|
|
6
|
+
// it finds itself on and resolves the addon's package root through the same
|
|
7
|
+
// `__gjsifyBundleUrl` banner + `createRequire` anchor the module-resolve shim
|
|
8
|
+
// uses. The addon is never copied and never leaves its package, so a `.node`
|
|
9
|
+
// that `dlopen`s a sibling `.so` or reads a data file beside itself keeps
|
|
10
|
+
// working.
|
|
11
|
+
//
|
|
12
|
+
// Three correctness constraints shape the implementation:
|
|
13
|
+
//
|
|
14
|
+
// 1. *Location anchor.* The resolver anchors at the BUNDLE's URL, not its
|
|
15
|
+
// own. When a consumer bundles this shim it lives under
|
|
16
|
+
// `node_modules/@gjsify/rolldown-plugin-gjsify/...`, so the path rewriter
|
|
17
|
+
// would rewrite this file's own `import.meta.url` too. We therefore take
|
|
18
|
+
// no `import.meta.url` here and read the anchor from `globalThis.
|
|
19
|
+
// __gjsifyBundleUrl`, captured by a one-line banner at byte 0 of the
|
|
20
|
+
// bundle — the single point where `import.meta.url` is unambiguously the
|
|
21
|
+
// bundle's URL.
|
|
22
|
+
//
|
|
23
|
+
// 2. *exports-map safety.* `createRequire(...).resolve("<pkg>/<deep/path>")`
|
|
24
|
+
// is rejected by strict `"exports"` maps under Node's native
|
|
25
|
+
// `createRequire` (the `--app node` target), and `@gjsify/module` is
|
|
26
|
+
// exports-aware too. So we never resolve the deep path directly: we
|
|
27
|
+
// resolve the PACKAGE ROOT and join the subpath literally, which no
|
|
28
|
+
// `exports` map can block.
|
|
29
|
+
//
|
|
30
|
+
// 3. *libc is a variable, not a probe.* It is read from `process.env.LIBC`,
|
|
31
|
+
// never sniffed off the filesystem. On a musl host with an unset `LIBC`
|
|
32
|
+
// the lookup falls through to the libc-agnostic entry, which is what
|
|
33
|
+
// node-gyp-build's own untagged prebuilds assume; a package shipping a
|
|
34
|
+
// glibc-only and a musl prebuild under one tuple is served the glibc one,
|
|
35
|
+
// which `LIBC=musl` fixes. (The one `package.json` read in this file is on
|
|
36
|
+
// the `exports`-blocked root fallback, not on the libc decision.)
|
|
37
|
+
//
|
|
38
|
+
// The `addons/` layout the resolver names when the package is not installed is
|
|
39
|
+
// the declared destination a packaging step would fill. `gjsify ship` does not
|
|
40
|
+
// fill it yet — see ADR 0084 § Consequences for the measurement.
|
|
41
|
+
//
|
|
42
|
+
// @ts-ignore — `node:{module,url,path,fs}` are resolved by the consumer's
|
|
43
|
+
// `gjsify build` run (aliased to `@gjsify/{module,url,path,fs}`), not by tsc here.
|
|
44
|
+
import { createRequire } from 'node:module';
|
|
45
|
+
// @ts-ignore — see above.
|
|
46
|
+
import { fileURLToPath } from 'node:url';
|
|
47
|
+
// @ts-ignore — see above.
|
|
48
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
49
|
+
// @ts-ignore — see above. Read on the `exports`-blocked fallback only; see
|
|
50
|
+
// `declaresPackage`. The no-filesystem rule above is about libc, not this.
|
|
51
|
+
import { readFileSync } from 'node:fs';
|
|
52
|
+
import { hostAddonKeys, selectAddonTarget } from '../utils/addon-platform.js';
|
|
53
|
+
/** The bundle's own URL (set by the byte-0 banner). Throws only if the banner didn't run. */
|
|
54
|
+
function bundleAnchorUrl() {
|
|
55
|
+
const anchor = globalThis.__gjsifyBundleUrl;
|
|
56
|
+
if (!anchor) {
|
|
57
|
+
throw new Error('gjsify: __gjsifyBundleUrl is not set — the bundle-URL banner did not run. ' +
|
|
58
|
+
'The addon-resolve shim is only valid in single-file app builds (gjsify build --app gjs).');
|
|
59
|
+
}
|
|
60
|
+
return anchor;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Split a bundled-file spec into its package name and the path within the
|
|
64
|
+
* package. Handles scoped names:
|
|
65
|
+
* "typedoc/dist/lib/app.js" → { pkg: "typedoc", subpath: "dist/lib/app.js" }
|
|
66
|
+
* "@scope/name/sub/file.js" → { pkg: "@scope/name", subpath: "sub/file.js" }
|
|
67
|
+
* "typedoc" → { pkg: "typedoc", subpath: "" }
|
|
68
|
+
*/
|
|
69
|
+
function splitPackageSpec(spec) {
|
|
70
|
+
const parts = spec.split('/');
|
|
71
|
+
const segments = spec.startsWith('@') ? 2 : 1;
|
|
72
|
+
return {
|
|
73
|
+
pkg: parts.slice(0, segments).join('/'),
|
|
74
|
+
subpath: parts.slice(segments).join('/'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Find the on-disk root directory of `pkg`, exports-map-agnostic.
|
|
79
|
+
*
|
|
80
|
+
* `package.json` is resolvable for nearly every package and points straight at
|
|
81
|
+
* the root. When a strict `exports` map blocks it, fall back to the package's
|
|
82
|
+
* main entry (always an export) and derive the root from the
|
|
83
|
+
* `node_modules/<pkg>` boundary in its path.
|
|
84
|
+
*
|
|
85
|
+
* A WORKSPACE-LINKED package resolves to its real path, which carries no
|
|
86
|
+
* `node_modules/<pkg>/` segment, and answering `dirname(main)` there named the
|
|
87
|
+
* directory the entry sits in — `packages/typedoc/dist` — so the subpath was
|
|
88
|
+
* joined onto `dist/` and the addon was reported missing at a path that never
|
|
89
|
+
* existed. The boundary is therefore only a shortcut; the fallback walks up to
|
|
90
|
+
* the nearest ancestor whose own `package.json` declares `pkg`, which is the
|
|
91
|
+
* root by definition. Returns null when `pkg` is not installed.
|
|
92
|
+
*/
|
|
93
|
+
function resolvePackageRoot(pkg) {
|
|
94
|
+
try {
|
|
95
|
+
const require = createRequire(bundleAnchorUrl());
|
|
96
|
+
return dirname(require.resolve(`${pkg}/package.json`));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* strict `exports`: fall through to the entry point */
|
|
100
|
+
}
|
|
101
|
+
let main;
|
|
102
|
+
try {
|
|
103
|
+
main = createRequire(bundleAnchorUrl()).resolve(pkg);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const marker = `/node_modules/${pkg}/`;
|
|
109
|
+
const idx = main.lastIndexOf(marker);
|
|
110
|
+
if (idx >= 0)
|
|
111
|
+
return main.slice(0, idx + marker.length - 1);
|
|
112
|
+
for (let dir = dirname(main), i = 0; i < 64; i++) {
|
|
113
|
+
if (declaresPackage(dir, pkg))
|
|
114
|
+
return dir;
|
|
115
|
+
const parent = dirname(dir);
|
|
116
|
+
if (parent === dir)
|
|
117
|
+
break;
|
|
118
|
+
dir = parent;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/** Does the `package.json` in `dir` declare `name: pkg`? The root test, read not inferred. */
|
|
123
|
+
function declaresPackage(dir, pkg) {
|
|
124
|
+
try {
|
|
125
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
126
|
+
return manifest.name === pkg;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the addon's `.node` for the RUNNING host from the build-time table.
|
|
134
|
+
*
|
|
135
|
+
* `targets` maps platform keys (`linux-x64`, `linux-x64-musl`, …) to
|
|
136
|
+
* `<package>/<subpath>` specs. The host keys are tried most-specific first
|
|
137
|
+
* (exact, then libc-agnostic, then `*`). The package root is resolved at run
|
|
138
|
+
* time through the bundle-URL anchor, which is what makes the bundle relocatable
|
|
139
|
+
* (ADR 0084).
|
|
140
|
+
*
|
|
141
|
+
* Throws when no entry matches the running host, when the table is empty, and
|
|
142
|
+
* when the addon package is not installed next to the bundle. The last one is
|
|
143
|
+
* a LIMIT, not an oversight: identity-based resolution needs the package to be
|
|
144
|
+
* somewhere on disk, so a bundle shipped with no `node_modules` around it cannot
|
|
145
|
+
* load a third-party addon. It throws rather than returning
|
|
146
|
+
* `<bundle dir>/addons/…` because that directory is a declared layout nothing
|
|
147
|
+
* fills yet, and a path that cannot exist reaches `loadAddon` as a bare ENOENT
|
|
148
|
+
* that names neither the package nor the remedy. Both facts are in the message.
|
|
149
|
+
*/
|
|
150
|
+
export function __gjsifyAddonResolve(targets) {
|
|
151
|
+
const keys = hostAddonKeys(process.platform, process.arch, process.env.LIBC);
|
|
152
|
+
const spec = selectAddonTarget(targets, keys);
|
|
153
|
+
if (spec === null) {
|
|
154
|
+
const known = Object.keys(targets).join(', ') || '(empty)';
|
|
155
|
+
throw new Error(`gjsify-napi-addon: no .node for host '${process.platform}-${process.arch}' ` +
|
|
156
|
+
`(libc: ${process.env.LIBC ?? 'unset'}). Addon table knows: ${known}. ` +
|
|
157
|
+
`Install the platform package or build the addon for this host.`);
|
|
158
|
+
}
|
|
159
|
+
// A `.node` outside every `node_modules` has no package IDENTITY to resolve
|
|
160
|
+
// by, and `packageSpecFor` recorded the path itself. Splitting it as a
|
|
161
|
+
// specifier made the first segment the package name and the rest the subpath,
|
|
162
|
+
// so a direct import of a locally built addon resolved a package named ``
|
|
163
|
+
// and threw naming neither the file nor the remedy. There is nothing to
|
|
164
|
+
// resolve — the build's path is the only answer — so it is returned, and the
|
|
165
|
+
// build warns once that this entry does not travel with the bundle.
|
|
166
|
+
if (isAbsolute(spec))
|
|
167
|
+
return spec;
|
|
168
|
+
const { pkg, subpath } = splitPackageSpec(spec);
|
|
169
|
+
const root = resolvePackageRoot(pkg);
|
|
170
|
+
if (root !== null) {
|
|
171
|
+
if (subpath === '')
|
|
172
|
+
return root;
|
|
173
|
+
// The subpath is the build's own `relative()` today, so this cannot
|
|
174
|
+
// reject a table this plugin wrote. It is here because this is the
|
|
175
|
+
// function that decides which file gets dlopen'd: a table naming `..`
|
|
176
|
+
// would otherwise walk out of the package it claims to belong to.
|
|
177
|
+
const target = resolve(root, subpath);
|
|
178
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
179
|
+
throw new Error(`gjsify-napi-addon: the addon table names '${subpath}', which leaves the package ` +
|
|
180
|
+
`root '${root}'. A package cannot provide a file outside itself.`);
|
|
181
|
+
}
|
|
182
|
+
return target;
|
|
183
|
+
}
|
|
184
|
+
// The package is not installed where this bundle can see it. `<bundle
|
|
185
|
+
// dir>/addons/<package>/` is the declared layout a packaging step would
|
|
186
|
+
// fill — `gjsify ship` does not yet — so name it and the two remedies.
|
|
187
|
+
const staged = join(dirname(fileURLToPath(bundleAnchorUrl())), 'addons', pkg, subpath);
|
|
188
|
+
throw new Error(`gjsify-napi-addon: cannot find the addon package '${pkg}' from this bundle. ` +
|
|
189
|
+
`A GJS bundle finds its addon by package identity, so '${pkg}' must be installed in a ` +
|
|
190
|
+
`node_modules reachable from the bundle's own location, or staged at ` +
|
|
191
|
+
`'${staged}' (a layout no gjsify step fills yet). Run the bundle next to its ` +
|
|
192
|
+
"project's node_modules, or rebuild where the package is installed.");
|
|
193
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The one spelling: `linux-x64`, `linux-x64-musl`, `darwin-arm64`, `win32-x64`. */
|
|
2
|
+
export declare function addonPlatformKey(platform: string, arch: string, libc?: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* A napi-rs triple as an {@link addonPlatformKey}. The ABI token napi-rs appends is
|
|
5
|
+
* either implied by the platform (`gnu` on linux, `msvc` on win32, the arm eabi
|
|
6
|
+
* flavours) or a real axis (`musl`) — only the latter survives into the key.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeNapiRsTriple(triple: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The keys to try for a host, most specific first.
|
|
11
|
+
*
|
|
12
|
+
* A musl host also accepts the UNTAGGED key, because that is what node-gyp-build's own
|
|
13
|
+
* untagged prebuilds mean — "no libc claim" — and a musl-only tree would otherwise find
|
|
14
|
+
* nothing. A glibc host does NOT accept the musl key: loading a musl binary against
|
|
15
|
+
* glibc is the failure this selection exists to avoid, and a missing entry with a clear
|
|
16
|
+
* message beats a `dlopen` crash.
|
|
17
|
+
*
|
|
18
|
+
* `*` is last and is not host-specific: it is the entry a DIRECTLY imported `.node`
|
|
19
|
+
* writes, where the source named one file and there is no selection to make.
|
|
20
|
+
*/
|
|
21
|
+
export declare function hostAddonKeys(platform: string, arch: string, libc?: string): string[];
|
|
22
|
+
/** The first key of `keys` present in `targets`, or null. */
|
|
23
|
+
export declare function selectAddonTarget(targets: Record<string, string>, keys: string[]): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The platform VOCABULARY shared by the two halves of ADR 0084: the build enumerates
|
|
2
|
+
// every `.node` an addon package ships, keyed by platform; the bundle picks one at run
|
|
3
|
+
// time from the host it finds itself on. Both halves must spell a platform the same way
|
|
4
|
+
// or the lookup silently misses, so the spelling lives here — a pure module with no
|
|
5
|
+
// `node:` imports, because the runtime half (`shims/addon-resolve.ts`) is bundled into
|
|
6
|
+
// user output and must not drag the filesystem in.
|
|
7
|
+
//
|
|
8
|
+
// The key is node-gyp-build's tuple (`<platform>-<arch>`) plus a `-musl` suffix, NOT
|
|
9
|
+
// napi-rs' triple: napi-rs spells the same platform `linux-x64-gnu` / `win32-x64-msvc`
|
|
10
|
+
// while prebuildify spells it `linux-x64`, and one bundle can carry addons of both
|
|
11
|
+
// conventions. {@link normalizeNapiRsTriple} folds the second spelling into the first.
|
|
12
|
+
/** The one spelling: `linux-x64`, `linux-x64-musl`, `darwin-arm64`, `win32-x64`. */
|
|
13
|
+
export function addonPlatformKey(platform, arch, libc) {
|
|
14
|
+
return libc === 'musl' ? `${platform}-${arch}-musl` : `${platform}-${arch}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A napi-rs triple as an {@link addonPlatformKey}. The ABI token napi-rs appends is
|
|
18
|
+
* either implied by the platform (`gnu` on linux, `msvc` on win32, the arm eabi
|
|
19
|
+
* flavours) or a real axis (`musl`) — only the latter survives into the key.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeNapiRsTriple(triple) {
|
|
22
|
+
if (triple.endsWith('-musl'))
|
|
23
|
+
return triple;
|
|
24
|
+
return triple.replace(/-(?:gnu|msvc|eabi|eabihf|gnueabihf|androideabi)$/, '');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The keys to try for a host, most specific first.
|
|
28
|
+
*
|
|
29
|
+
* A musl host also accepts the UNTAGGED key, because that is what node-gyp-build's own
|
|
30
|
+
* untagged prebuilds mean — "no libc claim" — and a musl-only tree would otherwise find
|
|
31
|
+
* nothing. A glibc host does NOT accept the musl key: loading a musl binary against
|
|
32
|
+
* glibc is the failure this selection exists to avoid, and a missing entry with a clear
|
|
33
|
+
* message beats a `dlopen` crash.
|
|
34
|
+
*
|
|
35
|
+
* `*` is last and is not host-specific: it is the entry a DIRECTLY imported `.node`
|
|
36
|
+
* writes, where the source named one file and there is no selection to make.
|
|
37
|
+
*/
|
|
38
|
+
export function hostAddonKeys(platform, arch, libc) {
|
|
39
|
+
const base = addonPlatformKey(platform, arch);
|
|
40
|
+
return libc === 'musl' ? [`${base}-musl`, base, '*'] : [base, '*'];
|
|
41
|
+
}
|
|
42
|
+
/** The first key of `keys` present in `targets`, or null. */
|
|
43
|
+
export function selectAddonTarget(targets, keys) {
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const hit = targets[key];
|
|
46
|
+
if (typeof hit === 'string' && hit.length > 0)
|
|
47
|
+
return hit;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
@@ -55,6 +55,12 @@ export interface DetectAutoGlobalsOptions {
|
|
|
55
55
|
* `process.cwd()`.
|
|
56
56
|
*/
|
|
57
57
|
cwd?: string;
|
|
58
|
+
/**
|
|
59
|
+
* A second root for that gate: the running CLI's directory when the build was given
|
|
60
|
+
* a `toolchainAnchor`, whose resolver takes a `@gjsify/*` the project lacks from
|
|
61
|
+
* beside the CLI. Only consulted together with `cwd`.
|
|
62
|
+
*/
|
|
63
|
+
toolchainDir?: string;
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
60
66
|
* Build options for the analyser: the same input options the caller would use for the
|
|
@@ -81,7 +81,7 @@ function isSubset(a, b) {
|
|
|
81
81
|
return false;
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
84
|
-
async function applyExcludeGlobals(detected, currentInject, extraRegisterPaths, excludeGlobals, cwd) {
|
|
84
|
+
async function applyExcludeGlobals(detected, currentInject, extraRegisterPaths, excludeGlobals, cwd, searchDirs = cwd) {
|
|
85
85
|
if (excludeGlobals?.length) {
|
|
86
86
|
for (const id of excludeGlobals)
|
|
87
87
|
detected.delete(id);
|
|
@@ -91,12 +91,12 @@ async function applyExcludeGlobals(detected, currentInject, extraRegisterPaths,
|
|
|
91
91
|
const finalPaths = detectedToRegisterPaths(detected);
|
|
92
92
|
for (const p of extraRegisterPaths)
|
|
93
93
|
finalPaths.add(p);
|
|
94
|
-
emitGiBackedDiagnostic(finalPaths, detected,
|
|
94
|
+
emitGiBackedDiagnostic(finalPaths, detected, searchDirs);
|
|
95
95
|
// No exclude: `currentInject` already reflects `finalPaths`, so reuse it —
|
|
96
96
|
// re-running filterResolvableRegisterPaths would repeat its skip warnings.
|
|
97
97
|
if (!excludeGlobals?.length)
|
|
98
98
|
return { detected, injectPath: currentInject };
|
|
99
|
-
const filtered =
|
|
99
|
+
const filtered = searchDirs ? filterResolvableRegisterPaths(finalPaths, searchDirs) : finalPaths;
|
|
100
100
|
const injectPath = filtered.size > 0 ? ((await writeRegisterInjectFile(filtered, cwd)) ?? undefined) : undefined;
|
|
101
101
|
return { detected, injectPath };
|
|
102
102
|
}
|
|
@@ -169,14 +169,14 @@ let giNoteEmitted = false;
|
|
|
169
169
|
* register, via `isRegisterPathResolvable` rather than
|
|
170
170
|
* `filterResolvableRegisterPaths` to avoid duplicating that one's skip warnings.
|
|
171
171
|
*/
|
|
172
|
-
function emitGiBackedDiagnostic(registerPaths, detected,
|
|
172
|
+
function emitGiBackedDiagnostic(registerPaths, detected, searchDirs) {
|
|
173
173
|
if (giNoteEmitted)
|
|
174
174
|
return;
|
|
175
175
|
const candidates = new Set();
|
|
176
176
|
for (const path of registerPaths) {
|
|
177
177
|
if (!giNamespacesForRegister(path))
|
|
178
178
|
continue;
|
|
179
|
-
if (
|
|
179
|
+
if (searchDirs && !isRegisterPathResolvable(path, searchDirs))
|
|
180
180
|
continue;
|
|
181
181
|
candidates.add(path);
|
|
182
182
|
}
|
|
@@ -189,6 +189,70 @@ function emitGiBackedDiagnostic(registerPaths, detected, cwd) {
|
|
|
189
189
|
function isFullConfig(v) {
|
|
190
190
|
return v !== null && typeof v === 'object' && !Array.isArray(v) && 'plugins' in v && 'options' in v;
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Merge the orchestrator's `transform` with the analysis side's, PER KEY — the
|
|
194
|
+
* analysis side wins for the keys it actually sets.
|
|
195
|
+
*
|
|
196
|
+
* `analysisOptions.transform ?? orchestratorOptions?.transform` was wrong for a
|
|
197
|
+
* MEASURED reason, not a stylistic one: the CLI's config merge does
|
|
198
|
+
* `bundler.transform ??= {}` (`@gjsify/cli` `src/config.ts`), so the analysis side is
|
|
199
|
+
* ALWAYS a non-nullish object and `??` therefore always took it — dropping the
|
|
200
|
+
* orchestrator's `target`, `define` and `inject` from every single analysis pass. The
|
|
201
|
+
* detector then reads a bundle transformed differently from the one that ships, which
|
|
202
|
+
* is exactly what the "analyse the bundled output AFTER tree-shaking" invariant exists
|
|
203
|
+
* to prevent.
|
|
204
|
+
*
|
|
205
|
+
* What that cost, measured on `--app gjs`, whose orchestrator defines
|
|
206
|
+
* `process.env.READABLE_STREAM` to `"disable"`. Entry:
|
|
207
|
+
*
|
|
208
|
+
* if (process.env.READABLE_STREAM !== 'disable') document.getElementById('app');
|
|
209
|
+
*
|
|
210
|
+
* The define makes that branch dead, so the emitted bundle carries no `document`
|
|
211
|
+
* reference at all — and the build injected `document` + `HTMLCanvasElement` + `Path2D`
|
|
212
|
+
* anyway: 24 globals and 276_726 bytes where the same entry now yields 21 and 176_487.
|
|
213
|
+
* 100 KB of DOM/canvas register code, one of it a GI-backed register that makes the
|
|
214
|
+
* artifact hard-require a GTK runtime at load. Silently, at exit 0.
|
|
215
|
+
*
|
|
216
|
+
* Note what does NOT discriminate here, so the next reproduction does not start with
|
|
217
|
+
* it: `process` is detected on every `--app gjs` build regardless, because the process
|
|
218
|
+
* stub the target prepends in `renderChunk` writes `globalThis.process` and the
|
|
219
|
+
* detector reads its own banner back.
|
|
220
|
+
*
|
|
221
|
+
* `define` and `inject` are string maps and merge per key; every other transform key is
|
|
222
|
+
* a scalar (`target`, `lang`, `sourceType`) or a coherent option bag (`typescript`,
|
|
223
|
+
* `decorator`, `assumptions`, `tsconfig`) that is replaced whole. This is deliberately
|
|
224
|
+
* the same shape as `mergeBundlerOptions` in `@gjsify/cli`, which merges the FINAL
|
|
225
|
+
* build's options: the two must agree, or the analysis is measuring a different bundle
|
|
226
|
+
* again one key further down.
|
|
227
|
+
*/
|
|
228
|
+
function mergeTransformOptions(orchestrator, analysis) {
|
|
229
|
+
if (!orchestrator)
|
|
230
|
+
return analysis;
|
|
231
|
+
if (!analysis)
|
|
232
|
+
return orchestrator;
|
|
233
|
+
const merged = { ...orchestrator, ...analysis };
|
|
234
|
+
if (orchestrator.define || analysis.define) {
|
|
235
|
+
merged.define = { ...orchestrator.define, ...analysis.define };
|
|
236
|
+
}
|
|
237
|
+
if (orchestrator.inject || analysis.inject) {
|
|
238
|
+
merged.inject = { ...orchestrator.inject, ...analysis.inject };
|
|
239
|
+
}
|
|
240
|
+
return merged;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Same per-key rule for `resolve`. No caller sets `analysisOptions.resolve` today, so
|
|
244
|
+
* this is inert right now — it is here because `resolve` is the third plain-object
|
|
245
|
+
* field on the same code path and a whole-object `??` on it would silently drop the
|
|
246
|
+
* orchestrator's `conditionNames`/`mainFields`, which is the divergence the class
|
|
247
|
+
* comment above describes.
|
|
248
|
+
*/
|
|
249
|
+
function mergeResolveOptions(orchestrator, analysis) {
|
|
250
|
+
if (!orchestrator)
|
|
251
|
+
return analysis;
|
|
252
|
+
if (!analysis)
|
|
253
|
+
return orchestrator;
|
|
254
|
+
return { ...orchestrator, ...analysis };
|
|
255
|
+
}
|
|
192
256
|
/**
|
|
193
257
|
* Run the iterative in-memory build with acorn-based global detection, each pass
|
|
194
258
|
* seeded by the previous pass's globals, until the detected set is stable. Returns the
|
|
@@ -201,10 +265,13 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
201
265
|
: new Set();
|
|
202
266
|
const excludeSet = new Set(options.excludeGlobals ?? []);
|
|
203
267
|
const cwd = options.cwd;
|
|
268
|
+
const searchDirs = cwd && options.toolchainDir ? [cwd, options.toolchainDir] : cwd;
|
|
204
269
|
let detected = new Set();
|
|
205
270
|
let currentInject = undefined;
|
|
206
271
|
if (extraRegisterPaths.size > 0) {
|
|
207
|
-
const resolvableExtra =
|
|
272
|
+
const resolvableExtra = searchDirs
|
|
273
|
+
? filterResolvableRegisterPaths(extraRegisterPaths, searchDirs)
|
|
274
|
+
: extraRegisterPaths;
|
|
208
275
|
currentInject = (await writeRegisterInjectFile(resolvableExtra, cwd)) ?? undefined;
|
|
209
276
|
}
|
|
210
277
|
// Caller plugins (e.g. the PnP relay) survive every pass; the gjsify plugin is
|
|
@@ -237,10 +304,16 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
237
304
|
: analysisOptions.input;
|
|
238
305
|
// Take resolve/external/transform from the orchestrator so the analysis bundle
|
|
239
306
|
// goes through the same module resolution as the final build; explicit
|
|
240
|
-
// analysis-side overrides win.
|
|
241
|
-
|
|
307
|
+
// analysis-side overrides win PER KEY (see `mergeTransformOptions`).
|
|
308
|
+
//
|
|
309
|
+
// `external` stays a whole-value choice on purpose: it is not a plain object
|
|
310
|
+
// (string | RegExp | array | predicate), the orchestrator's array has already
|
|
311
|
+
// folded the caller's own entries in, and the shape rules an array cannot
|
|
312
|
+
// express live in `externalsPlugin` — which is part of `gjsifyPluginsArray`
|
|
313
|
+
// below, so it applies to the analysis bundle whichever array wins here.
|
|
314
|
+
const mergedResolve = mergeResolveOptions(orchestratorOptions?.resolve, analysisOptions.resolve);
|
|
242
315
|
const mergedExternal = analysisOptions.external ?? orchestratorOptions?.external;
|
|
243
|
-
const mergedTransform = analysisOptions.transform
|
|
316
|
+
const mergedTransform = mergeTransformOptions(orchestratorOptions?.transform, analysisOptions.transform);
|
|
244
317
|
const orchTreeshake = orchestratorOptions?.treeshake;
|
|
245
318
|
const gjsifyPluginsArray = Array.isArray(gjsifyInstance) ? gjsifyInstance : [gjsifyInstance];
|
|
246
319
|
const chunkCodes = await bundler({
|
|
@@ -311,7 +384,7 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
311
384
|
const extras = extraRegisterPaths.size > 0 ? ` (+ ${extraRegisterPaths.size} extra register module(s))` : '';
|
|
312
385
|
console.debug(`[gjsify] --globals auto: converged after ${iteration - 1} iteration(s), ${detected.size} global(s)${sorted.length ? ': ' + sorted.join(', ') : ''}${extras}`);
|
|
313
386
|
}
|
|
314
|
-
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd);
|
|
387
|
+
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd, searchDirs);
|
|
315
388
|
}
|
|
316
389
|
// Seed the next pass with the precomputed closure. Unioning with the
|
|
317
390
|
// previously-injected set keeps growth monotonic — an over-approximated superset
|
|
@@ -335,8 +408,8 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
335
408
|
// Drop register paths whose polyfill package is not installed: an unresolvable
|
|
336
409
|
// import is a HARD Rolldown error, strictly worse than skipping with a warning.
|
|
337
410
|
// The caller adds the dep or acknowledges the gap via --exclude-globals.
|
|
338
|
-
if (
|
|
339
|
-
registerPaths = filterResolvableRegisterPaths(registerPaths,
|
|
411
|
+
if (searchDirs)
|
|
412
|
+
registerPaths = filterResolvableRegisterPaths(registerPaths, searchDirs);
|
|
340
413
|
if (registerPaths.size === 0) {
|
|
341
414
|
return { detected, injectPath: undefined };
|
|
342
415
|
}
|
|
@@ -349,7 +422,7 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
349
422
|
if (verbose) {
|
|
350
423
|
console.debug(`[gjsify] --globals auto: hit max iterations (${MAX_ITERATIONS}), using last detected set`);
|
|
351
424
|
}
|
|
352
|
-
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd);
|
|
425
|
+
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd, searchDirs);
|
|
353
426
|
}
|
|
354
427
|
/**
|
|
355
428
|
* `--app node` genuine-GJS-source detection (the reverse of `--globals auto`).
|
|
@@ -408,8 +481,12 @@ export async function detectNodeGiGlobals(analysisOptions, pluginOptions, gjsify
|
|
|
408
481
|
...baseOptions,
|
|
409
482
|
input: analysisOptions.input,
|
|
410
483
|
external: analysisOptions.external ?? baseOptions.external,
|
|
411
|
-
|
|
412
|
-
|
|
484
|
+
// Per key, for the reason spelled out on `mergeTransformOptions`: the
|
|
485
|
+
// CLI always hands an object here, so a whole-object `??` dropped this
|
|
486
|
+
// target's `target: 'node24'` and `define: { global, window }` from
|
|
487
|
+
// every reverse-bridge analysis pass.
|
|
488
|
+
resolve: mergeResolveOptions(baseOptions.resolve, analysisOptions.resolve),
|
|
489
|
+
transform: mergeTransformOptions(baseOptions.transform, analysisOptions.transform),
|
|
413
490
|
plugins: [...callerPlugins, ...gjsifyPluginsArray],
|
|
414
491
|
logLevel: 'silent',
|
|
415
492
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One file the output depends on. */
|
|
2
|
+
export type DeclareBuildInput = (abs: string) => void;
|
|
3
|
+
/** A plugin context that MAY carry `addWatchFile` — an older engine does not. */
|
|
4
|
+
export interface WatchFileCapableContext {
|
|
5
|
+
addWatchFile?: (id: string) => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `this.addWatchFile(abs)`, or one line saying it was missing.
|
|
9
|
+
*
|
|
10
|
+
* The context is passed in rather than bound, so each caller keeps its own `this`
|
|
11
|
+
* and this stays usable from any hook shape.
|
|
12
|
+
*/
|
|
13
|
+
export declare function declareBuildInput(ctx: WatchFileCapableContext, abs: string): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// "This file is an input of the output" — `this.addWatchFile`, one layer down.
|
|
3
|
+
//
|
|
4
|
+
// Two of this package's plugins read a file the BUNDLER never sees: a
|
|
5
|
+
// stylesheet's `@import` chain (`css-as-string`) and a package's own
|
|
6
|
+
// `readFileSync(new URL(..., import.meta.url))` (the static-read inliner, which
|
|
7
|
+
// folds the bytes into the bundle). Neither file is a module, so no graph names
|
|
8
|
+
// it and a `gjsify test` bundle that depends on one reads as fresh after it
|
|
9
|
+
// changes. `this.addWatchFile` is the standard contract for exactly that, and
|
|
10
|
+
// this is the one place it is issued from.
|
|
11
|
+
//
|
|
12
|
+
// FEATURE-DETECTED, and that is measured, not defensive: the engine the GJS CLI
|
|
13
|
+
// loads is resolved through several anchors, and one of them answered with a
|
|
14
|
+
// build whose plugin context had no such method — a direct call then fails the
|
|
15
|
+
// build with `this.addWatchFile is not a function`. A build must not fail over
|
|
16
|
+
// bookkeeping, so a miss degrades to a single line naming the consequence and the
|
|
17
|
+
// fix. The other half is in the CLI: a bundle whose build reported NO watch list
|
|
18
|
+
// can never be called fresh (`utils/bundle-inputs.ts`).
|
|
19
|
+
let _warned = false;
|
|
20
|
+
/**
|
|
21
|
+
* `this.addWatchFile(abs)`, or one line saying it was missing.
|
|
22
|
+
*
|
|
23
|
+
* The context is passed in rather than bound, so each caller keeps its own `this`
|
|
24
|
+
* and this stays usable from any hook shape.
|
|
25
|
+
*/
|
|
26
|
+
export function declareBuildInput(ctx, abs) {
|
|
27
|
+
if (typeof ctx.addWatchFile === 'function') {
|
|
28
|
+
ctx.addWatchFile.call(ctx, abs);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (_warned)
|
|
32
|
+
return;
|
|
33
|
+
_warned = true;
|
|
34
|
+
console.warn('[gjsify] this Rolldown engine has no `addWatchFile`, so a file a plugin read itself is ' +
|
|
35
|
+
'not declared as a build input — editing one may leave a `gjsify test` bundle looking ' +
|
|
36
|
+
'fresh. Upgrade `@gjsify/rolldown-native` (the GJS engine) to declare it.');
|
|
37
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
/** Every name bound by a binding pattern. */
|
|
3
|
+
export declare function extractBindingNames(node: acorn.AnyNode): string[];
|
|
1
4
|
/**
|
|
2
5
|
* Free identifiers in bundled output that match the known web/Node globals in
|
|
3
6
|
* `GJS_GLOBALS_MAP` — drives `--app gjs --globals auto`. Rules: `scanFreeGlobals`.
|
|
@@ -25,7 +25,12 @@ export function wrapInputWithSideEffects(input, sideEffects, opts = {}) {
|
|
|
25
25
|
const userEntries = new Map(); // virtualId → realPath
|
|
26
26
|
const PREFIX = `${GJSIFY_VIRTUAL_PREFIX}entry:`;
|
|
27
27
|
function wrap(realPath) {
|
|
28
|
-
|
|
28
|
+
// The wrapper is ESM whatever the entry is, and Rolldown reads a module's
|
|
29
|
+
// format off its id's EXTENSION: `\0gjsify-entry:…/prettier.cjs` was parsed as
|
|
30
|
+
// CommonJS and every `import` in the wrapper failed with `PARSE_ERROR: Cannot
|
|
31
|
+
// use import statement outside a module`. A CJS entry therefore gets `.mjs`
|
|
32
|
+
// appended; the others keep the id they always had.
|
|
33
|
+
const id = PREFIX + realPath + (/\.c[jt]sx?$/i.test(realPath) ? '.mjs' : '');
|
|
29
34
|
userEntries.set(id, realPath);
|
|
30
35
|
return id;
|
|
31
36
|
}
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { inlineStaticReads, isAbsoluteFsPath, isWithin, resourceRootFor } from '
|
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
10
|
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
11
|
export type { SurvivingJsx } from './jsx-survival.js';
|
|
12
|
+
export { wrapInputWithSideEffects } from './entry-wrapper.js';
|
package/lib/utils/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
|
8
8
|
export { inlineStaticReads, isAbsoluteFsPath, isWithin, resourceRootFor } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
10
|
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
|
+
export { wrapInputWithSideEffects } from './entry-wrapper.js';
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a source with the parser its extension calls for.
|
|
4
|
+
*
|
|
5
|
+
* The TypeScript half is why first-party sources were invisible to this inliner
|
|
6
|
+
* for as long as it was scoped to `node_modules`: an installed package ships JS,
|
|
7
|
+
* so plain acorn could always parse it, and nothing ever asked what happens to a
|
|
8
|
+
* `.ts`. The answer was that `acorn.parse` threw and the `catch` returned
|
|
9
|
+
* "nothing to inline" — a result indistinguishable from a file that genuinely has
|
|
10
|
+
* no static reads. Measured on `packages/infra/cli/src/utils/app-metadata.ts`:
|
|
11
|
+
* `inlined: 0`, while the identical expression in a `.js` file returned
|
|
12
|
+
* `inlined: 1`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseSource(src: string, sourceFilePath: string): acorn.Program;
|
|
15
|
+
export declare function inlineStaticReads(src: string, sourceFilePath: string, declare?: (abs: string) => void): {
|
|
2
16
|
contents: string;
|
|
3
17
|
inlined: number;
|
|
4
18
|
};
|