@gjsify/rolldown-plugin-gjsify 0.52.0 → 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 +1 -0
- package/lib/app/gjs.js +14 -2
- package/lib/app/nativescript.js +1 -1
- package/lib/app/node.d.ts +1 -1
- package/lib/app/node.js +26 -10
- package/lib/index.d.ts +3 -1
- package/lib/index.js +3 -1
- 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/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 +13 -10
- 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/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,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the addon's `.node` for the RUNNING host from the build-time table.
|
|
3
|
+
*
|
|
4
|
+
* `targets` maps platform keys (`linux-x64`, `linux-x64-musl`, …) to
|
|
5
|
+
* `<package>/<subpath>` specs. The host keys are tried most-specific first
|
|
6
|
+
* (exact, then libc-agnostic, then `*`). The package root is resolved at run
|
|
7
|
+
* time through the bundle-URL anchor, which is what makes the bundle relocatable
|
|
8
|
+
* (ADR 0084).
|
|
9
|
+
*
|
|
10
|
+
* Throws when no entry matches the running host, when the table is empty, and
|
|
11
|
+
* when the addon package is not installed next to the bundle. The last one is
|
|
12
|
+
* a LIMIT, not an oversight: identity-based resolution needs the package to be
|
|
13
|
+
* somewhere on disk, so a bundle shipped with no `node_modules` around it cannot
|
|
14
|
+
* load a third-party addon. It throws rather than returning
|
|
15
|
+
* `<bundle dir>/addons/…` because that directory is a declared layout nothing
|
|
16
|
+
* fills yet, and a path that cannot exist reaches `loadAddon` as a bare ENOENT
|
|
17
|
+
* that names neither the package nor the remedy. Both facts are in the message.
|
|
18
|
+
*/
|
|
19
|
+
export declare function __gjsifyAddonResolve(targets: Record<string, string>): string;
|
|
@@ -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
|
}
|
|
@@ -265,10 +265,13 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
265
265
|
: new Set();
|
|
266
266
|
const excludeSet = new Set(options.excludeGlobals ?? []);
|
|
267
267
|
const cwd = options.cwd;
|
|
268
|
+
const searchDirs = cwd && options.toolchainDir ? [cwd, options.toolchainDir] : cwd;
|
|
268
269
|
let detected = new Set();
|
|
269
270
|
let currentInject = undefined;
|
|
270
271
|
if (extraRegisterPaths.size > 0) {
|
|
271
|
-
const resolvableExtra =
|
|
272
|
+
const resolvableExtra = searchDirs
|
|
273
|
+
? filterResolvableRegisterPaths(extraRegisterPaths, searchDirs)
|
|
274
|
+
: extraRegisterPaths;
|
|
272
275
|
currentInject = (await writeRegisterInjectFile(resolvableExtra, cwd)) ?? undefined;
|
|
273
276
|
}
|
|
274
277
|
// Caller plugins (e.g. the PnP relay) survive every pass; the gjsify plugin is
|
|
@@ -381,7 +384,7 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
381
384
|
const extras = extraRegisterPaths.size > 0 ? ` (+ ${extraRegisterPaths.size} extra register module(s))` : '';
|
|
382
385
|
console.debug(`[gjsify] --globals auto: converged after ${iteration - 1} iteration(s), ${detected.size} global(s)${sorted.length ? ': ' + sorted.join(', ') : ''}${extras}`);
|
|
383
386
|
}
|
|
384
|
-
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd);
|
|
387
|
+
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd, searchDirs);
|
|
385
388
|
}
|
|
386
389
|
// Seed the next pass with the precomputed closure. Unioning with the
|
|
387
390
|
// previously-injected set keeps growth monotonic — an over-approximated superset
|
|
@@ -405,8 +408,8 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
405
408
|
// Drop register paths whose polyfill package is not installed: an unresolvable
|
|
406
409
|
// import is a HARD Rolldown error, strictly worse than skipping with a warning.
|
|
407
410
|
// The caller adds the dep or acknowledges the gap via --exclude-globals.
|
|
408
|
-
if (
|
|
409
|
-
registerPaths = filterResolvableRegisterPaths(registerPaths,
|
|
411
|
+
if (searchDirs)
|
|
412
|
+
registerPaths = filterResolvableRegisterPaths(registerPaths, searchDirs);
|
|
410
413
|
if (registerPaths.size === 0) {
|
|
411
414
|
return { detected, injectPath: undefined };
|
|
412
415
|
}
|
|
@@ -419,7 +422,7 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
419
422
|
if (verbose) {
|
|
420
423
|
console.debug(`[gjsify] --globals auto: hit max iterations (${MAX_ITERATIONS}), using last detected set`);
|
|
421
424
|
}
|
|
422
|
-
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd);
|
|
425
|
+
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals, cwd, searchDirs);
|
|
423
426
|
}
|
|
424
427
|
/**
|
|
425
428
|
* `--app node` genuine-GJS-source detection (the reverse of `--globals auto`).
|
|
@@ -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
|
};
|
|
@@ -66,6 +66,7 @@ import { tsPlugin } from 'acorn-typescript';
|
|
|
66
66
|
import { dirname, join, resolve, basename, relative, extname, posix, win32 } from 'node:path';
|
|
67
67
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
68
68
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
69
|
+
import { hasZipSegment } from './zip-path.js';
|
|
69
70
|
/**
|
|
70
71
|
* Run the inliner on a source string. Returns the rewritten source (or the
|
|
71
72
|
* original string when no inlining applied) and the count of edits applied.
|
|
@@ -101,7 +102,7 @@ const TS_PARSER = acorn.Parser.extend(tsPlugin());
|
|
|
101
102
|
* `inlined: 0`, while the identical expression in a `.js` file returned
|
|
102
103
|
* `inlined: 1`.
|
|
103
104
|
*/
|
|
104
|
-
function parseSource(src, sourceFilePath) {
|
|
105
|
+
export function parseSource(src, sourceFilePath) {
|
|
105
106
|
const shared = {
|
|
106
107
|
ecmaVersion: 'latest',
|
|
107
108
|
sourceType: 'module',
|
|
@@ -142,7 +143,7 @@ function forEachCallExpression(root, visit) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
|
-
export function inlineStaticReads(src, sourceFilePath) {
|
|
146
|
+
export function inlineStaticReads(src, sourceFilePath, declare) {
|
|
146
147
|
if (!src.includes('readFileSync') && !src.includes('readdirSync') && !src.includes('existsSync')) {
|
|
147
148
|
return { contents: src, inlined: 0 };
|
|
148
149
|
}
|
|
@@ -160,6 +161,7 @@ export function inlineStaticReads(src, sourceFilePath) {
|
|
|
160
161
|
const ctx = {
|
|
161
162
|
sourceUrl: pathToFileURL(sourceFilePath).href,
|
|
162
163
|
resourceRoot: resourceRootFor(sourceFilePath),
|
|
164
|
+
declare,
|
|
163
165
|
};
|
|
164
166
|
const edits = [];
|
|
165
167
|
forEachCallExpression(ast, (node) => {
|
|
@@ -246,6 +248,7 @@ function tryInlineCall(node, ctx, _src) {
|
|
|
246
248
|
if (path && existsSync(path) && isDirectorySafe(path)) {
|
|
247
249
|
try {
|
|
248
250
|
const names = readdirSync(path);
|
|
251
|
+
ctx.declare?.(path);
|
|
249
252
|
return {
|
|
250
253
|
start: node.start,
|
|
251
254
|
end: node.end,
|
|
@@ -260,11 +263,11 @@ function tryInlineCall(node, ctx, _src) {
|
|
|
260
263
|
if (calleeName === 'existsSync') {
|
|
261
264
|
const path = evalPathExpr(node.arguments[0], ctx);
|
|
262
265
|
if (path !== undefined) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
};
|
|
266
|
+
// The ANSWER is in the bundle, so the file's existence is an input
|
|
267
|
+
// of the output — a file that appears changes the emitted literal.
|
|
268
|
+
const present = existsSync(path);
|
|
269
|
+
ctx.declare?.(path);
|
|
270
|
+
return { start: node.start, end: node.end, replacement: present ? 'true' : 'false' };
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
// `createRequire(<URL>)` from `node:module` returns a CJS-style require.
|
|
@@ -288,8 +291,11 @@ function tryInlineCall(node, ctx, _src) {
|
|
|
288
291
|
// - the path contains a `.zip/` segment (Yarn PnP virtual zip,
|
|
289
292
|
// where Node's PnP hooks make `existsSync` return true at build
|
|
290
293
|
// time but the path doesn't exist under GJS at runtime).
|
|
291
|
-
const isZip = path !== undefined && path
|
|
294
|
+
const isZip = path !== undefined && hasZipSegment(path);
|
|
292
295
|
if (path !== undefined && (isZip || !existsSync(path))) {
|
|
296
|
+
// The stub-vs-real choice reads the file's existence too, so the
|
|
297
|
+
// same declaration applies to the branch that replaces the call.
|
|
298
|
+
ctx.declare?.(path);
|
|
293
299
|
return {
|
|
294
300
|
start: node.start,
|
|
295
301
|
end: node.end,
|
|
@@ -330,6 +336,7 @@ function tryInlineReadFile(node, ctx, forceTextEncoding) {
|
|
|
330
336
|
try {
|
|
331
337
|
if (encoding) {
|
|
332
338
|
const text = readFileSync(path, encoding);
|
|
339
|
+
ctx.declare?.(path);
|
|
333
340
|
return jsStringLiteral(text);
|
|
334
341
|
}
|
|
335
342
|
else {
|
|
@@ -337,6 +344,7 @@ function tryInlineReadFile(node, ctx, forceTextEncoding) {
|
|
|
337
344
|
// Buffer-vs-Uint8Array semantic difference is mostly irrelevant in
|
|
338
345
|
// bundled GJS code (Buffer is polyfilled on top of Uint8Array).
|
|
339
346
|
const bytes = readFileSync(path);
|
|
347
|
+
ctx.declare?.(path);
|
|
340
348
|
return `new Uint8Array([${Array.from(bytes).join(',')}])`;
|
|
341
349
|
}
|
|
342
350
|
}
|
|
@@ -34,7 +34,7 @@ export declare function resolveGlobalsList(globalsArg: string): Set<string>;
|
|
|
34
34
|
* Returns false — never throws — so a missing package causes a graceful
|
|
35
35
|
* skip rather than a build crash.
|
|
36
36
|
*/
|
|
37
|
-
export declare function isRegisterPathResolvable(registerPath: string, fromDir: string): boolean;
|
|
37
|
+
export declare function isRegisterPathResolvable(registerPath: string, fromDir: string | readonly string[]): boolean;
|
|
38
38
|
/**
|
|
39
39
|
* Filter `registerPaths` to only those whose base package is resolvable
|
|
40
40
|
* from `fromDir`. Emits a `console.warn` for each skipped path —
|
|
@@ -45,7 +45,7 @@ export declare function isRegisterPathResolvable(registerPath: string, fromDir:
|
|
|
45
45
|
* dep, and that polyfill is not a direct/transitive dep, the warn makes
|
|
46
46
|
* the gap visible so the developer can add the dep explicitly.
|
|
47
47
|
*/
|
|
48
|
-
export declare function filterResolvableRegisterPaths(registerPaths: Set<string>, fromDir: string): Set<string>;
|
|
48
|
+
export declare function filterResolvableRegisterPaths(registerPaths: Set<string>, fromDir: string | readonly string[]): Set<string>;
|
|
49
49
|
/** Options for {@link writeRegisterInjectFile}. */
|
|
50
50
|
export interface WriteRegisterInjectOptions {
|
|
51
51
|
/**
|