@gjsify/rolldown-plugin-gjsify 0.24.1 → 0.25.1
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/browser.js +16 -1
- package/lib/app/gjs.js +17 -6
- package/lib/app/nativescript.js +16 -1
- package/lib/app/node.js +13 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/plugins/unresolved-workspace-import.d.ts +105 -0
- package/lib/plugins/unresolved-workspace-import.js +318 -0
- package/package.json +7 -7
package/lib/app/browser.js
CHANGED
|
@@ -19,6 +19,7 @@ import { ALIASES_NODE_FOR_BROWSER, getDerivedAliasesSync } from '@gjsify/resolve
|
|
|
19
19
|
import { globToEntryPoints } from '../utils/entry-points.js';
|
|
20
20
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
21
21
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
22
|
+
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
22
23
|
export const setupForBrowser = async (input) => {
|
|
23
24
|
const userExternal = input.userExternal ?? [];
|
|
24
25
|
const external = [...userExternal];
|
|
@@ -81,12 +82,26 @@ export const setupForBrowser = async (input) => {
|
|
|
81
82
|
},
|
|
82
83
|
treeshake: true,
|
|
83
84
|
};
|
|
85
|
+
// The substitution table exactly as `aliasPlugin` receives it — shared with
|
|
86
|
+
// the unresolved-workspace-import guard below so the guard reports the same
|
|
87
|
+
// promise the alias layer made.
|
|
88
|
+
const aliasEntries = flattenAliases(aliasMap);
|
|
84
89
|
const plugins = [
|
|
85
90
|
gjsImportsEmptyPlugin(),
|
|
86
|
-
aliasPlugin({ entries:
|
|
91
|
+
aliasPlugin({ entries: aliasEntries }),
|
|
87
92
|
blueprintPlugin(),
|
|
88
93
|
deepkitPlugin({ reflection: input.pluginOptions.reflection }),
|
|
89
94
|
cssAsStringPlugin(),
|
|
95
|
+
// `order: 'post'` — see app/gjs.ts. The browser target's whole job is to
|
|
96
|
+
// replace Node builtins with their `@gjsify/*` browser entries; when one
|
|
97
|
+
// of those cannot be resolved, Rolldown externalises the ORIGINAL bare
|
|
98
|
+
// specifier and a browser cannot resolve it at all. There is no
|
|
99
|
+
// `node:`-shaped emit-time symptom here, so this guard is the only signal.
|
|
100
|
+
unresolvedWorkspaceImportPlugin({
|
|
101
|
+
target: 'browser',
|
|
102
|
+
aliases: aliasEntries,
|
|
103
|
+
isExternal: (id) => external.includes(id),
|
|
104
|
+
}),
|
|
90
105
|
];
|
|
91
106
|
return { options, plugins };
|
|
92
107
|
};
|
package/lib/app/gjs.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
16
16
|
import { externalsPlugin } from '../plugins/externals.js';
|
|
17
17
|
import { napiNodeAddonPlugin } from '../plugins/napi-node-addon.js';
|
|
18
|
+
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
18
19
|
import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
|
|
19
20
|
import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
|
|
20
21
|
import { getAliasesForGjs } from '../utils/alias.js';
|
|
@@ -188,6 +189,13 @@ export const setupForGjs = async (input) => {
|
|
|
188
189
|
treeshake: true,
|
|
189
190
|
};
|
|
190
191
|
const bundleDir = getBundleDirFromOutput(input.output);
|
|
192
|
+
// The substitution table exactly as `aliasPlugin` receives it — shared with
|
|
193
|
+
// the unresolved-workspace-import guard below so the guard reports the same
|
|
194
|
+
// promise the alias layer made.
|
|
195
|
+
const aliasEntries = {
|
|
196
|
+
'random-access-file': 'random-access-file/index.js',
|
|
197
|
+
...flattenAliases(aliasMap),
|
|
198
|
+
};
|
|
191
199
|
const plugins = [
|
|
192
200
|
// Virtual-entry plugin runs FIRST so its resolveId/load match the
|
|
193
201
|
// synthetic input ids that `wrapInputWithSideEffects` produces.
|
|
@@ -201,12 +209,7 @@ export const setupForGjs = async (input) => {
|
|
|
201
209
|
// random-access-file's 'browser' field maps to a throwing stub; force
|
|
202
210
|
// the fs-backed Node entry. Implemented via the gjsify alias plugin
|
|
203
211
|
// as a direct entry-table override.
|
|
204
|
-
aliasPlugin({
|
|
205
|
-
entries: {
|
|
206
|
-
'random-access-file': 'random-access-file/index.js',
|
|
207
|
-
...flattenAliases(aliasMap),
|
|
208
|
-
},
|
|
209
|
-
}),
|
|
212
|
+
aliasPlugin({ entries: aliasEntries }),
|
|
210
213
|
// Transparent N-API `.node`-addon loader — the forward mirror of
|
|
211
214
|
// `gjsGiNodePlugin`. Its `order:'pre'` resolveId claims a native-addon
|
|
212
215
|
// acquisition (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs
|
|
@@ -239,6 +242,14 @@ export const setupForGjs = async (input) => {
|
|
|
239
242
|
const line = resolveShebangLine(input.shebang);
|
|
240
243
|
return shebangPlugin({ enabled: line !== null, line: line ?? undefined });
|
|
241
244
|
})(),
|
|
245
|
+
// LAST claim on an id nothing else wanted (`order: 'post'`): a
|
|
246
|
+
// `@gjsify/*` substitution that cannot be resolved is a build ERROR, not
|
|
247
|
+
// a silent external. Without it Rolldown externalises the ORIGINAL
|
|
248
|
+
// specifier, exits 0, and writes a bundle stock GJS aborts on at load
|
|
249
|
+
// (`ImportError: Unsupported URI scheme for importing: node`) — the
|
|
250
|
+
// failure `utils/gjs-bundle-guard.ts` catches at emit time, caught here
|
|
251
|
+
// at its source instead, with the importer and the cause named.
|
|
252
|
+
unresolvedWorkspaceImportPlugin({ target: 'gjs', aliases: aliasEntries, isExternal: external }),
|
|
242
253
|
];
|
|
243
254
|
return { options, plugins };
|
|
244
255
|
};
|
package/lib/app/nativescript.js
CHANGED
|
@@ -40,6 +40,7 @@ import { ALIASES_NODE_FOR_NATIVESCRIPT, getDerivedAliasesSync } from '@gjsify/re
|
|
|
40
40
|
import { globToEntryPoints } from '../utils/entry-points.js';
|
|
41
41
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
42
42
|
import { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
|
|
43
|
+
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
43
44
|
export const setupForNativescript = async (input) => {
|
|
44
45
|
const userExternal = input.userExternal ?? [];
|
|
45
46
|
const external = [...userExternal];
|
|
@@ -116,18 +117,32 @@ export const setupForNativescript = async (input) => {
|
|
|
116
117
|
},
|
|
117
118
|
treeshake: true,
|
|
118
119
|
};
|
|
120
|
+
// The substitution table exactly as `aliasPlugin` receives it — shared with
|
|
121
|
+
// the unresolved-workspace-import guard below so the guard reports the same
|
|
122
|
+
// promise the alias layer made.
|
|
123
|
+
const aliasEntries = flattenAliases(aliasMap);
|
|
119
124
|
const plugins = [
|
|
120
125
|
gjsImportsEmptyPlugin(),
|
|
121
126
|
// Platform-specific source variants (`*.android` / `*.ios` /
|
|
122
127
|
// `*.native`) win over the base file — resolved BEFORE the Node-builtin
|
|
123
128
|
// alias routing so a platform fork of a portable module is honored.
|
|
124
129
|
platformResolvePlugin({ platform }),
|
|
125
|
-
aliasPlugin({ entries:
|
|
130
|
+
aliasPlugin({ entries: aliasEntries }),
|
|
126
131
|
// NO blueprintPlugin — Blueprint is a GTK-specific UI DSL
|
|
127
132
|
// NO cssAsStringPlugin — NS ships its own CSS pipeline via
|
|
128
133
|
// @nativescript/core; .css imports are handled by the consuming
|
|
129
134
|
// @nativescript/webpack or @nativescript/vite build
|
|
130
135
|
deepkitPlugin({ reflection: input.pluginOptions.reflection }),
|
|
136
|
+
// `order: 'post'` — see app/gjs.ts. Native-bridge identifiers
|
|
137
|
+
// (`java.*`, `NS*`, …) are ambient GLOBALS on this target, never
|
|
138
|
+
// imports, so they never reach this hook; only a `@gjsify/*` edge does,
|
|
139
|
+
// and on NS as everywhere else an unresolvable one is a broken bundle
|
|
140
|
+
// rather than something the runtime will provide.
|
|
141
|
+
unresolvedWorkspaceImportPlugin({
|
|
142
|
+
target: 'nativescript',
|
|
143
|
+
aliases: aliasEntries,
|
|
144
|
+
isExternal: (id) => external.includes(id),
|
|
145
|
+
}),
|
|
131
146
|
];
|
|
132
147
|
return { options, plugins };
|
|
133
148
|
};
|
package/lib/app/node.js
CHANGED
|
@@ -14,6 +14,7 @@ import { nodeModulesPathRewritePlugin, getBundleDirFromOutput } from '../plugins
|
|
|
14
14
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
15
15
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
16
16
|
import { gjsGiNodePlugin, gjsBuiltinModulesNodePlugin } from '../plugins/gjs-gi-node.js';
|
|
17
|
+
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
17
18
|
import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
|
|
18
19
|
/**
|
|
19
20
|
* Side-effect specifier the node target injects when GJS ambient globals
|
|
@@ -219,6 +220,10 @@ export const setupForNode = async (input) => {
|
|
|
219
220
|
...input.pluginOptions.aliases,
|
|
220
221
|
...input.userAliases,
|
|
221
222
|
};
|
|
223
|
+
// The substitution table exactly as `aliasPlugin` receives it — shared with
|
|
224
|
+
// the unresolved-workspace-import guard below so the guard reports the same
|
|
225
|
+
// promise the alias layer made.
|
|
226
|
+
const aliasEntries = flattenAliases(aliasMap);
|
|
222
227
|
const bundleDir = getBundleDirFromOutput(input.output);
|
|
223
228
|
// Rolldown's CJS interop wraps bundled CJS via `__commonJSMin` and
|
|
224
229
|
// routes external Node-builtin `require()` through `__require` —
|
|
@@ -317,7 +322,7 @@ export const setupForNode = async (input) => {
|
|
|
317
322
|
// `requireGi`, exactly like the ambient-globals case. Same predicate as
|
|
318
323
|
// the register-alias lift above — one question, one flag.
|
|
319
324
|
gjsImportsEmptyPlugin({ emptyGirs: !gjsSourceBuild }),
|
|
320
|
-
aliasPlugin({ entries:
|
|
325
|
+
aliasPlugin({ entries: aliasEntries }),
|
|
321
326
|
// Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on
|
|
322
327
|
// Node, so a GJS app entry (composite-template windows) must build for
|
|
323
328
|
// `--app node` too. Only claims `.blp` files — inert for plain-Node
|
|
@@ -334,6 +339,13 @@ export const setupForNode = async (input) => {
|
|
|
334
339
|
// symptom). A plain-Node bundle (no node-gi) keeps the CSS pristine.
|
|
335
340
|
cssAsStringPlugin(input.pluginOptions.nodeGiGlobalsInject ? { targets: { firefox: 60 << 16 } } : {}),
|
|
336
341
|
nodeModulesPathRewritePlugin({ bundleDir }),
|
|
342
|
+
// `order: 'post'` — fires only for ids nothing above claimed, i.e. the
|
|
343
|
+
// ones headed for Rolldown's unresolved-import → external fallback. The
|
|
344
|
+
// node target has no `node:`-shaped symptom for the emit-time guard to
|
|
345
|
+
// detect (its `node:` builtins ARE external by policy), so this is the
|
|
346
|
+
// ONLY thing standing between an unbuilt `@gjsify/*` workspace edge and
|
|
347
|
+
// a bundle that re-imports the bare specifier at runtime.
|
|
348
|
+
unresolvedWorkspaceImportPlugin({ target: 'node', aliases: aliasEntries, isExternal: external }),
|
|
337
349
|
];
|
|
338
350
|
return { options, plugins };
|
|
339
351
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type { AliasPluginOptions } from './plugins/alias.js';
|
|
|
16
16
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
17
17
|
export { externalsPlugin } from './plugins/externals.js';
|
|
18
18
|
export type { ExternalsPredicate, ExternalsPluginOptions } from './plugins/externals.js';
|
|
19
|
+
export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
|
|
20
|
+
export type { WorkspaceImportGuardOptions, WorkspaceImportGuardTarget, ImportVerdict, ClassifyImportInput, UnresolvedWorkspaceImportDetails, } from './plugins/unresolved-workspace-import.js';
|
|
19
21
|
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
20
22
|
export type { NapiNodeAddonPluginOptions, AddonPackageJson } from './plugins/napi-node-addon.js';
|
|
21
23
|
export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShe
|
|
|
11
11
|
export { aliasPlugin } from './plugins/alias.js';
|
|
12
12
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
13
13
|
export { externalsPlugin } from './plugins/externals.js';
|
|
14
|
+
export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
|
|
14
15
|
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
15
16
|
export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
|
16
17
|
export * from './plugin.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
/** Build targets that compose this guard. Used only for the error message. */
|
|
3
|
+
export type WorkspaceImportGuardTarget = 'gjs' | 'node' | 'browser' | 'nativescript';
|
|
4
|
+
export interface WorkspaceImportGuardOptions {
|
|
5
|
+
/** `--app <target>` this build is for; named verbatim in the error. */
|
|
6
|
+
target: WorkspaceImportGuardTarget;
|
|
7
|
+
/**
|
|
8
|
+
* The substitution table this target promised (specifier → target), already
|
|
9
|
+
* merged across tiers exactly as `aliasPlugin` received it. A miss here is
|
|
10
|
+
* fine — case (b) still covers a direct `@gjsify/*` import.
|
|
11
|
+
*/
|
|
12
|
+
aliases?: Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* The target's own externals policy. Anything it claims is a DELIBERATE
|
|
15
|
+
* external and is ignored. Pass the same predicate `externalsPlugin` gets
|
|
16
|
+
* (plus exact-name membership) so the two cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
isExternal?: (id: string) => boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Is `id` a bare `@gjsify/*` package specifier (root or subpath)?
|
|
22
|
+
*
|
|
23
|
+
* Deliberately excludes disk paths and synthetic ids: a resolved
|
|
24
|
+
* `/…/node_modules/@gjsify/fs/lib/esm/index.js` is already resolved, and a
|
|
25
|
+
* `\0gjsify-*` virtual id belongs to the plugin that emitted it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isWorkspaceSpecifier(id: string): boolean;
|
|
28
|
+
/** What the guard decided about one specifier. */
|
|
29
|
+
export type ImportVerdict = {
|
|
30
|
+
verdict: 'ignore';
|
|
31
|
+
reason: 'entry' | 'not-bare' | 'declared-external' | 'out-of-scope';
|
|
32
|
+
} | {
|
|
33
|
+
verdict: 'check';
|
|
34
|
+
candidate: string;
|
|
35
|
+
aliasTarget?: string;
|
|
36
|
+
};
|
|
37
|
+
export interface ClassifyImportInput {
|
|
38
|
+
source: string;
|
|
39
|
+
importer: string | undefined;
|
|
40
|
+
aliases?: Record<string, string>;
|
|
41
|
+
isExternal?: (id: string) => boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pure decision: does this import edge have to resolve, and to what?
|
|
45
|
+
*
|
|
46
|
+
* Split out from the plugin so the legitimate-vs-broken line is unit-testable
|
|
47
|
+
* without a bundler (`packages/infra/cli/src/unresolved-workspace-import.spec.ts`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function classifyImport(input: ClassifyImportInput): ImportVerdict;
|
|
50
|
+
export interface UnresolvedWorkspaceImportDetails {
|
|
51
|
+
target: WorkspaceImportGuardTarget;
|
|
52
|
+
/** The specifier as written in the importing module. */
|
|
53
|
+
source: string;
|
|
54
|
+
/** What actually had to resolve (the alias target, or `source` itself). */
|
|
55
|
+
candidate: string;
|
|
56
|
+
/** Set when `source` was rewritten by the substitution table. */
|
|
57
|
+
aliasTarget?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Specifiers whose substitution-table entry names `candidate`, discovered by
|
|
60
|
+
* REVERSE lookup — the half of the story `source` alone cannot tell.
|
|
61
|
+
*
|
|
62
|
+
* `aliasPlugin` resolves its target through `this.resolve(target, importer)`,
|
|
63
|
+
* so when `node:fs` → `@gjsify/fs` fails, the id that reaches THIS hook is
|
|
64
|
+
* `@gjsify/fs` with no alias entry of its own: the message would name the
|
|
65
|
+
* missing package but not the `node:fs` the user actually wrote, which is
|
|
66
|
+
* the specifier that survives into the bundle and the one they will grep
|
|
67
|
+
* for. Reversing the table recovers it (`['fs', 'node:fs']` — both forms
|
|
68
|
+
* route to the same target, and naming both is accurate).
|
|
69
|
+
*/
|
|
70
|
+
aliasedFrom?: readonly string[];
|
|
71
|
+
importer: string;
|
|
72
|
+
}
|
|
73
|
+
/** A `@gjsify/*` edge the build promised to substitute could not be resolved. */
|
|
74
|
+
export declare class UnresolvedWorkspaceImportError extends Error {
|
|
75
|
+
readonly details: UnresolvedWorkspaceImportDetails;
|
|
76
|
+
constructor(details: UnresolvedWorkspaceImportDetails);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The actionable message. Names the specifier, the importer and the likely
|
|
80
|
+
* cause — the three things `UNRESOLVED_IMPORT` (and the post-hoc `node:` guard)
|
|
81
|
+
* cannot tell you.
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatUnresolvedWorkspaceImport(details: UnresolvedWorkspaceImportDetails): string;
|
|
84
|
+
/**
|
|
85
|
+
* Reverse the substitution table: alias TARGET → the specifiers that route to it.
|
|
86
|
+
*
|
|
87
|
+
* Built once per plugin instance (the tables run to ~150 entries per target, and
|
|
88
|
+
* the guard only consults this on the failure path).
|
|
89
|
+
*/
|
|
90
|
+
export declare function buildReverseAliasIndex(aliases: Record<string, string> | undefined): Map<string, string[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Refuse a build whose `@gjsify/*` substitution could not be resolved.
|
|
93
|
+
*
|
|
94
|
+
* `order: 'post'` so every deliberate claim — `aliasPlugin` (`pre`),
|
|
95
|
+
* `napiNodeAddonPlugin` (`pre`), `gjsGiNodePlugin` (`pre`),
|
|
96
|
+
* `gjsImportsEmptyPlugin`, `externalsPlugin` — gets first refusal. Only ids that
|
|
97
|
+
* NOTHING claimed reach this hook, i.e. exactly the ones headed for the
|
|
98
|
+
* unresolved-import fallback.
|
|
99
|
+
*
|
|
100
|
+
* The hook re-runs the full resolution chain via `this.resolve(…, { skipSelf })`
|
|
101
|
+
* — which is what Rolldown's builtin resolver would have done next — and returns
|
|
102
|
+
* its result when it succeeds, so a healthy build resolves the specifier exactly
|
|
103
|
+
* once and behaves identically. Only a genuine `null` throws.
|
|
104
|
+
*/
|
|
105
|
+
export declare function unresolvedWorkspaceImportPlugin(options: WorkspaceImportGuardOptions): Plugin;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Resolver-layer guard: an unresolvable `@gjsify/*` edge is a build ERROR,
|
|
2
|
+
// never a silent external.
|
|
3
|
+
//
|
|
4
|
+
// ## The failure this closes
|
|
5
|
+
//
|
|
6
|
+
// Rolldown (like Rollup) treats an import nothing resolves as EXTERNAL: it
|
|
7
|
+
// emits an `UNRESOLVED_IMPORT` **warning**, re-emits the ORIGINAL specifier
|
|
8
|
+
// into the output, and exits 0. For a normal bundler that is a reasonable
|
|
9
|
+
// default — the specifier might be provided by the host. For `gjsify build` it
|
|
10
|
+
// is not, because the `@gjsify/*` edge that failed is precisely the SUBSTITUTION
|
|
11
|
+
// that makes the bundle runnable on the target:
|
|
12
|
+
//
|
|
13
|
+
// import { readFileSync } from 'node:fs'
|
|
14
|
+
// → aliasPlugin rewrites to `@gjsify/fs`
|
|
15
|
+
// → `@gjsify/fs` cannot be resolved (no `node_modules`, or the workspace
|
|
16
|
+
// package has no built `lib/` so its `exports["."]` points at a file
|
|
17
|
+
// that does not exist)
|
|
18
|
+
// → aliasPlugin falls through, nothing else claims it
|
|
19
|
+
// → `node:fs` is externalised, the build "succeeds"
|
|
20
|
+
//
|
|
21
|
+
// Measured on `packages/infra/cli/dist/cli.gjs.mjs` (repo task #63): 1 bare
|
|
22
|
+
// `node:` import on `origin/main` vs 11 from a worktree with no `node_modules`
|
|
23
|
+
// — a bundle stock GJS aborts on before one line runs
|
|
24
|
+
// (`ImportError: Unsupported URI scheme for importing: node`). The pre-commit
|
|
25
|
+
// hook then rebuilt and AUTO-STAGED it, and nothing local objected: tsc, lint,
|
|
26
|
+
// format, unit and e2e all run the Node entry, never the emitted GJS bundle.
|
|
27
|
+
//
|
|
28
|
+
// ## Why this is upstream of the `--app gjs` `node:` guard (PR #828)
|
|
29
|
+
//
|
|
30
|
+
// `utils/gjs-bundle-guard.ts` reads the finished module graph and refuses to
|
|
31
|
+
// EMIT a `--app gjs` bundle that still statically imports a bare `node:`
|
|
32
|
+
// specifier. That is the right last line of defence, and it stays. But it is:
|
|
33
|
+
//
|
|
34
|
+
// - **gjs-only** — the same silent externalisation happens on `--app node`
|
|
35
|
+
// and `--app browser`, where a surviving bare `@gjsify/foo` is equally
|
|
36
|
+
// unloadable and no `node:`-shaped symptom exists to detect;
|
|
37
|
+
// - **symptom-shaped** — it names `node:fs`, not the `@gjsify/fs` edge that
|
|
38
|
+
// actually failed, and cannot name the importer;
|
|
39
|
+
// - **post-tree-shake** — an unresolved edge that gets shaken out leaves no
|
|
40
|
+
// trace at all, so the artefact passes while the build is still wrong.
|
|
41
|
+
//
|
|
42
|
+
// This guard fires at RESOLUTION time instead: it names the specifier, the
|
|
43
|
+
// substitution target, the importer, and the likely cause.
|
|
44
|
+
//
|
|
45
|
+
// ## The legitimate-vs-broken line
|
|
46
|
+
//
|
|
47
|
+
// A blanket "fail on any external" would break every real build — externals are
|
|
48
|
+
// load-bearing on every target (`gi://*`, `cairo`/`system`/`gettext` on gjs; the
|
|
49
|
+
// whole `EXTERNALS_NODE` set plus `@gjsify/node-gi*` and `node-datachannel` on
|
|
50
|
+
// node; whatever the user passed to `--external`). Those never reach the
|
|
51
|
+
// unresolved fallback by accident — a `resolveId` hook or the `external` option
|
|
52
|
+
// CLAIMS them, deliberately. So the distinction drawn here is:
|
|
53
|
+
//
|
|
54
|
+
// DECLARED external → legitimate, ignored. Asked via the target's own
|
|
55
|
+
// externals predicate (`isExternal`), i.e. exactly the
|
|
56
|
+
// policy `externalsPlugin` enforces, so the guard and the
|
|
57
|
+
// policy cannot drift.
|
|
58
|
+
// PROMISED, missing → broken. Either (a) the specifier has an entry in the
|
|
59
|
+
// target's substitution table and that target does not
|
|
60
|
+
// resolve, or (b) the specifier IS a bare `@gjsify/*`
|
|
61
|
+
// workspace package that does not resolve.
|
|
62
|
+
// anything else → not our business. A missing `lodash` is a plain
|
|
63
|
+
// dependency bug that `UNRESOLVED_IMPORT` already
|
|
64
|
+
// reports; widening to every bare specifier would turn a
|
|
65
|
+
// genuinely optional peer into a hard build failure, and
|
|
66
|
+
// this guard's whole claim is that the `@gjsify/*` edge
|
|
67
|
+
// is NOT optional — the target cannot run without it.
|
|
68
|
+
//
|
|
69
|
+
// On (a): the check is keyed on the ALIAS TARGET being a `@gjsify/*` package,
|
|
70
|
+
// not merely on the entry existing. Every value in the curated + derived tables
|
|
71
|
+
// is a `@gjsify/*` specifier today, so this is a distinction without a
|
|
72
|
+
// difference for them — but it keeps a user `--alias foo=./some/local.js` (or a
|
|
73
|
+
// shim path like `random-access-file/index.js`) on the old fall-through
|
|
74
|
+
// behaviour instead of promoting a user's own mapping mistake to a fatal error
|
|
75
|
+
// in a plugin they did not ask for.
|
|
76
|
+
//
|
|
77
|
+
// ## Not registered for `--library`
|
|
78
|
+
//
|
|
79
|
+
// Library mode PRESERVES imports on purpose (`preserveModules`, bare specifiers
|
|
80
|
+
// kept so the consumer resolves them). An unresolved `@gjsify/*` there is the
|
|
81
|
+
// contract, not a bug, so `library/lib.ts` deliberately does not compose this
|
|
82
|
+
// plugin.
|
|
83
|
+
import { dirname } from 'node:path';
|
|
84
|
+
/** Bare `@gjsify/*` package specifier — the workspace edge this guard protects. */
|
|
85
|
+
const WORKSPACE_SCOPE = '@gjsify/';
|
|
86
|
+
/**
|
|
87
|
+
* How many reverse-looked-up source specifiers the error may name before the
|
|
88
|
+
* list stops being evidence and becomes noise. A per-builtin target inverts to
|
|
89
|
+
* 2 (`fs` + `node:fs`); a shared sink like `@gjsify/empty` inverts to ~50.
|
|
90
|
+
*/
|
|
91
|
+
const MAX_REPORTED_ALIAS_SOURCES = 4;
|
|
92
|
+
/**
|
|
93
|
+
* Is `id` a bare `@gjsify/*` package specifier (root or subpath)?
|
|
94
|
+
*
|
|
95
|
+
* Deliberately excludes disk paths and synthetic ids: a resolved
|
|
96
|
+
* `/…/node_modules/@gjsify/fs/lib/esm/index.js` is already resolved, and a
|
|
97
|
+
* `\0gjsify-*` virtual id belongs to the plugin that emitted it.
|
|
98
|
+
*/
|
|
99
|
+
export function isWorkspaceSpecifier(id) {
|
|
100
|
+
if (!id.startsWith(WORKSPACE_SCOPE))
|
|
101
|
+
return false;
|
|
102
|
+
if (id.includes('\0'))
|
|
103
|
+
return false;
|
|
104
|
+
// `@gjsify/fs` and `@gjsify/fs/register/x` qualify; `@gjsify/` and
|
|
105
|
+
// `@gjsify//x` do not (no package name). A resolved disk path can never
|
|
106
|
+
// reach here — it is absolute or `./`-relative, so it never starts with the
|
|
107
|
+
// scope in the first place.
|
|
108
|
+
const rest = id.slice(WORKSPACE_SCOPE.length);
|
|
109
|
+
return rest.length > 0 && !rest.startsWith('/');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Pure decision: does this import edge have to resolve, and to what?
|
|
113
|
+
*
|
|
114
|
+
* Split out from the plugin so the legitimate-vs-broken line is unit-testable
|
|
115
|
+
* without a bundler (`packages/infra/cli/src/unresolved-workspace-import.spec.ts`).
|
|
116
|
+
*/
|
|
117
|
+
export function classifyImport(input) {
|
|
118
|
+
const { source, importer, aliases, isExternal } = input;
|
|
119
|
+
// Entry modules are files the caller named; they are resolved by the
|
|
120
|
+
// orchestrator, never externalised.
|
|
121
|
+
if (importer === undefined)
|
|
122
|
+
return { verdict: 'ignore', reason: 'entry' };
|
|
123
|
+
// Relative / absolute / synthetic ids are not workspace edges.
|
|
124
|
+
if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0')) {
|
|
125
|
+
return { verdict: 'ignore', reason: 'not-bare' };
|
|
126
|
+
}
|
|
127
|
+
// A specifier the target DECLARES external is a deliberate external.
|
|
128
|
+
if (isExternal?.(source))
|
|
129
|
+
return { verdict: 'ignore', reason: 'declared-external' };
|
|
130
|
+
const aliasTarget = aliases?.[source];
|
|
131
|
+
const candidate = aliasTarget ?? source;
|
|
132
|
+
// The alias target may itself be declared external (`system` →
|
|
133
|
+
// `@gjsify/node-gi/system` on the node target).
|
|
134
|
+
if (candidate !== source && isExternal?.(candidate)) {
|
|
135
|
+
return { verdict: 'ignore', reason: 'declared-external' };
|
|
136
|
+
}
|
|
137
|
+
if (!isWorkspaceSpecifier(candidate))
|
|
138
|
+
return { verdict: 'ignore', reason: 'out-of-scope' };
|
|
139
|
+
return aliasTarget === undefined ? { verdict: 'check', candidate } : { verdict: 'check', candidate, aliasTarget };
|
|
140
|
+
}
|
|
141
|
+
/** A `@gjsify/*` edge the build promised to substitute could not be resolved. */
|
|
142
|
+
export class UnresolvedWorkspaceImportError extends Error {
|
|
143
|
+
details;
|
|
144
|
+
constructor(details) {
|
|
145
|
+
super(formatUnresolvedWorkspaceImport(details));
|
|
146
|
+
this.name = 'UnresolvedWorkspaceImportError';
|
|
147
|
+
this.details = details;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The actionable message. Names the specifier, the importer and the likely
|
|
152
|
+
* cause — the three things `UNRESOLVED_IMPORT` (and the post-hoc `node:` guard)
|
|
153
|
+
* cannot tell you.
|
|
154
|
+
*/
|
|
155
|
+
export function formatUnresolvedWorkspaceImport(details) {
|
|
156
|
+
const { target, source, candidate, aliasTarget, aliasedFrom, importer } = details;
|
|
157
|
+
// The specifiers `candidate` was standing in for: `source` itself when IT
|
|
158
|
+
// carried the table entry, else whatever the reverse lookup recovered.
|
|
159
|
+
//
|
|
160
|
+
// The reverse lookup is only reported when its fan-in is SMALL. A shared
|
|
161
|
+
// sink inverts to a useless wall of text — `@gjsify/empty` is the browser
|
|
162
|
+
// target for 49 specifiers, and printing all of them says nothing about
|
|
163
|
+
// which one this importer wrote, while `@gjsify/fs` inverts to exactly
|
|
164
|
+
// `['fs','node:fs']` (the two spellings of one builtin), which is the
|
|
165
|
+
// information the reader needs. Above the threshold, fall back to naming
|
|
166
|
+
// the package alone rather than guessing.
|
|
167
|
+
const reversed = aliasedFrom ?? [];
|
|
168
|
+
const substitutedFor = aliasTarget === undefined ? (reversed.length <= MAX_REPORTED_ALIAS_SOURCES ? reversed : []) : [source];
|
|
169
|
+
const quoted = substitutedFor.map((s) => `\`${s}\``).join(', ');
|
|
170
|
+
const headline = substitutedFor.length === 0
|
|
171
|
+
? `gjsify build --app ${target}: cannot resolve the workspace package \`${candidate}\`.`
|
|
172
|
+
: `gjsify build --app ${target}: cannot resolve \`${candidate}\`, the \`--app ${target}\` ` +
|
|
173
|
+
`substitution for ${quoted}.`;
|
|
174
|
+
// The symptom is a property of the SPECIFIER that would survive, not of the
|
|
175
|
+
// target: only a `node:` specifier reaching stock GJS produces the URI-scheme
|
|
176
|
+
// abort; a bare `@gjsify/*` or `<pkg>/register` one produces `Module not
|
|
177
|
+
// found` (GJS's ESM loader has no node_modules walker and does not follow
|
|
178
|
+
// `exports` maps). Naming the wrong one sends the reader hunting the wrong bug.
|
|
179
|
+
const surviving = substitutedFor.length === 0 ? [source] : substitutedFor;
|
|
180
|
+
const symptom = target !== 'gjs'
|
|
181
|
+
? ''
|
|
182
|
+
: surviving.some((s) => s.startsWith('node:'))
|
|
183
|
+
? ' Stock GJS aborts the whole module graph at load: ' +
|
|
184
|
+
'"ImportError: Unsupported URI scheme for importing: node".'
|
|
185
|
+
: ' Stock GJS would fail at load with "Module not found" — its ESM loader has no node_modules ' +
|
|
186
|
+
'walker and does not follow `package.json#exports` for bare specifiers.';
|
|
187
|
+
const consequence = (substitutedFor.length === 0
|
|
188
|
+
? `Leaving it unresolved would silently externalise \`${source}\` into the bundle, which the ` +
|
|
189
|
+
`\`--app ${target}\` runtime cannot resolve.`
|
|
190
|
+
: `Leaving it unresolved would silently externalise the ORIGINAL ${substitutedFor.length === 1 ? 'specifier' : 'specifiers'} ${quoted} into the ` +
|
|
191
|
+
`bundle, defeating the substitution the \`--app ${target}\` target exists to perform.`) + symptom;
|
|
192
|
+
return [
|
|
193
|
+
headline,
|
|
194
|
+
` imported by: ${importer}`,
|
|
195
|
+
'',
|
|
196
|
+
consequence,
|
|
197
|
+
'',
|
|
198
|
+
'Likely causes:',
|
|
199
|
+
' - `node_modules` is missing or incomplete — run `gjsify install` (`--immutable` in CI).',
|
|
200
|
+
` - the workspace package is not built: its \`exports["."]\` names a \`lib/\` that does not exist —`,
|
|
201
|
+
' run `gjsify run build:infra`, or `gjsify workspace <pkg> build` for the one package.',
|
|
202
|
+
` - \`${candidate}\` is genuinely not a dependency of this project — add it.`,
|
|
203
|
+
].join('\n');
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Reverse the substitution table: alias TARGET → the specifiers that route to it.
|
|
207
|
+
*
|
|
208
|
+
* Built once per plugin instance (the tables run to ~150 entries per target, and
|
|
209
|
+
* the guard only consults this on the failure path).
|
|
210
|
+
*/
|
|
211
|
+
export function buildReverseAliasIndex(aliases) {
|
|
212
|
+
const index = new Map();
|
|
213
|
+
if (!aliases)
|
|
214
|
+
return index;
|
|
215
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
216
|
+
const existing = index.get(to);
|
|
217
|
+
if (existing)
|
|
218
|
+
existing.push(from);
|
|
219
|
+
else
|
|
220
|
+
index.set(to, [from]);
|
|
221
|
+
}
|
|
222
|
+
for (const list of index.values())
|
|
223
|
+
list.sort();
|
|
224
|
+
return index;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Refuse a build whose `@gjsify/*` substitution could not be resolved.
|
|
228
|
+
*
|
|
229
|
+
* `order: 'post'` so every deliberate claim — `aliasPlugin` (`pre`),
|
|
230
|
+
* `napiNodeAddonPlugin` (`pre`), `gjsGiNodePlugin` (`pre`),
|
|
231
|
+
* `gjsImportsEmptyPlugin`, `externalsPlugin` — gets first refusal. Only ids that
|
|
232
|
+
* NOTHING claimed reach this hook, i.e. exactly the ones headed for the
|
|
233
|
+
* unresolved-import fallback.
|
|
234
|
+
*
|
|
235
|
+
* The hook re-runs the full resolution chain via `this.resolve(…, { skipSelf })`
|
|
236
|
+
* — which is what Rolldown's builtin resolver would have done next — and returns
|
|
237
|
+
* its result when it succeeds, so a healthy build resolves the specifier exactly
|
|
238
|
+
* once and behaves identically. Only a genuine `null` throws.
|
|
239
|
+
*/
|
|
240
|
+
export function unresolvedWorkspaceImportPlugin(options) {
|
|
241
|
+
const { target, aliases, isExternal } = options;
|
|
242
|
+
// Re-entrancy belt: `skipSelf: true` is supposed to keep our own
|
|
243
|
+
// `this.resolve` from re-entering this hook, but a hang is a far worse
|
|
244
|
+
// failure than a missed check, so the in-flight candidate is tracked
|
|
245
|
+
// explicitly. Keyed by `candidate\0importer` — the same package legitimately
|
|
246
|
+
// resolves from many importers.
|
|
247
|
+
const inFlight = new Set();
|
|
248
|
+
// Alias TARGET → the specifiers that route to it. Only read on the failure
|
|
249
|
+
// path, to recover the `node:fs` behind a failed `@gjsify/fs`.
|
|
250
|
+
const reverseAliases = buildReverseAliasIndex(aliases);
|
|
251
|
+
// Successful resolutions, memoized per (candidate, importer DIRECTORY) —
|
|
252
|
+
// the pair node resolution actually depends on, so a hit is the answer the
|
|
253
|
+
// chain would have recomputed. Without it this hook was the single most
|
|
254
|
+
// expensive plugin in a CLI-bundle build (32% of plugin time): `@gjsify/*`
|
|
255
|
+
// edges repeat across hundreds of modules, and each miss re-runs the whole
|
|
256
|
+
// `pre`-order chain (alias, napi-addon, gi-node) for the same target. Same
|
|
257
|
+
// shape + rationale as `napiNodeAddonPlugin`'s `bareEntryCache` (#840).
|
|
258
|
+
// Only POSITIVE results are cached: a failure throws, so it is never hot,
|
|
259
|
+
// and caching it could outlive a package appearing mid-watch.
|
|
260
|
+
const resolvedCache = new Map();
|
|
261
|
+
return {
|
|
262
|
+
name: 'gjsify-unresolved-workspace-import',
|
|
263
|
+
resolveId: {
|
|
264
|
+
order: 'post',
|
|
265
|
+
async handler(source, rawImporter, extraOptions) {
|
|
266
|
+
// The two engines disagree on "no importer": npm `rolldown`
|
|
267
|
+
// passes `undefined`, `@gjsify/rolldown-native` passes `null`
|
|
268
|
+
// (its hook payload round-trips through JSON, which has no
|
|
269
|
+
// `undefined`). `classifyImport` reads an absent importer as
|
|
270
|
+
// "this is an ENTRY, never externalised" via `=== undefined`,
|
|
271
|
+
// and `null` fails that test — so under the GJS bundler an
|
|
272
|
+
// entry module would have been resolved as a normal edge and
|
|
273
|
+
// could throw this plugin's error for a file the caller named
|
|
274
|
+
// explicitly. Normalise once, at the boundary — same fix as
|
|
275
|
+
// `napiNodeAddonPlugin` (#840), whose `dirname(null)` threw
|
|
276
|
+
// and took the whole GJS build down as an UNHANDLEABLE_ERROR.
|
|
277
|
+
const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
|
|
278
|
+
if (extraOptions?.isEntry)
|
|
279
|
+
return null;
|
|
280
|
+
const verdict = classifyImport({ source, importer, aliases, isExternal });
|
|
281
|
+
if (verdict.verdict === 'ignore')
|
|
282
|
+
return null;
|
|
283
|
+
// `kind` participates in the key: it selects the `import`
|
|
284
|
+
// vs `require` export condition, so the same specifier can
|
|
285
|
+
// legitimately resolve to different files from one directory.
|
|
286
|
+
const key = `${verdict.candidate}\0${importer === undefined ? '' : dirname(importer)}\0${extraOptions?.kind ?? ''}`;
|
|
287
|
+
const cached = resolvedCache.get(key);
|
|
288
|
+
if (cached)
|
|
289
|
+
return cached;
|
|
290
|
+
if (inFlight.has(key))
|
|
291
|
+
return null;
|
|
292
|
+
inFlight.add(key);
|
|
293
|
+
let resolved;
|
|
294
|
+
try {
|
|
295
|
+
resolved = await this.resolve(verdict.candidate, importer, {
|
|
296
|
+
skipSelf: true,
|
|
297
|
+
kind: extraOptions?.kind,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
inFlight.delete(key);
|
|
302
|
+
}
|
|
303
|
+
if (resolved) {
|
|
304
|
+
resolvedCache.set(key, resolved);
|
|
305
|
+
return resolved;
|
|
306
|
+
}
|
|
307
|
+
throw new UnresolvedWorkspaceImportError({
|
|
308
|
+
target,
|
|
309
|
+
source,
|
|
310
|
+
candidate: verdict.candidate,
|
|
311
|
+
aliasTarget: verdict.aliasTarget,
|
|
312
|
+
aliasedFrom: reverseAliases.get(verdict.candidate)?.filter((s) => s !== source),
|
|
313
|
+
importer: importer ?? '<entry>',
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
],
|
|
56
56
|
"license": "MIT",
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@gjsify/console": "^0.
|
|
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.25.1",
|
|
59
|
+
"@gjsify/resolve-npm": "^0.25.1",
|
|
60
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.25.1",
|
|
61
|
+
"@gjsify/rolldown-plugin-pnp": "^0.25.1",
|
|
62
|
+
"@gjsify/vite-plugin-blueprint": "^0.25.1",
|
|
63
63
|
"@rollup/pluginutils": "^5.4.0",
|
|
64
64
|
"acorn": "^8.17.0",
|
|
65
65
|
"acorn-walk": "^8.3.5",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"sass": "^1.101.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@gjsify/lightningcss-native": "^0.
|
|
71
|
+
"@gjsify/lightningcss-native": "^0.25.1",
|
|
72
72
|
"rolldown": "^1.1.4"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|