@gjsify/rolldown-plugin-gjsify 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/gjs.js +28 -0
- package/lib/app/nativescript.js +2 -2
- package/lib/app/node.d.ts +5 -2
- package/lib/app/node.js +118 -2
- package/lib/index.d.ts +9 -1
- package/lib/index.js +5 -1
- package/lib/plugins/platform-resolve.d.ts +133 -8
- package/lib/plugins/platform-resolve.js +214 -30
- package/lib/plugins/react-native-alias.d.ts +63 -0
- package/lib/plugins/react-native-alias.js +128 -0
- package/lib/plugins/react-native-gate.d.ts +117 -0
- package/lib/plugins/react-native-gate.js +240 -0
- package/lib/plugins/rn-route-manifest.d.ts +67 -0
- package/lib/plugins/rn-route-manifest.js +166 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/plugin-options.d.ts +20 -0
- package/lib/types/source-dialect.d.ts +42 -0
- package/lib/types/source-dialect.js +21 -0
- package/lib/utils/scan-named-imports.d.ts +47 -0
- package/lib/utils/scan-named-imports.js +239 -0
- package/package.json +15 -9
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Which NAMES a module imports from one package — the input the ADR 0032 § 8
|
|
2
|
+
// build-time gate needs and no bundler hook hands over.
|
|
3
|
+
//
|
|
4
|
+
// WHY A PARSE HERE RATHER THAN A HOOK. The gate has to name the IDENTIFIER, not
|
|
5
|
+
// the module: § 8's whole advantage over the runtime backstop is failing on
|
|
6
|
+
// `import { FlatList } from 'react-native'` with the importing file and position.
|
|
7
|
+
// Of the hooks BOTH engines honour — `@gjsify/rolldown-native`'s `NativePlugin`
|
|
8
|
+
// lists load, transform, resolveId, renderChunk, the four addons and the
|
|
9
|
+
// lifecycle pair, and that list is the ceiling — `resolveId` sees the specifier
|
|
10
|
+
// and never the bindings, and there is no `moduleParsed`, no `ModuleInfo.ast` and
|
|
11
|
+
// no `this.parse`. `transform` hands over the source and nothing else does, so
|
|
12
|
+
// the import list is ours to read.
|
|
13
|
+
//
|
|
14
|
+
// WHY NOT A TEXT SCAN. A regex over `import {…} from '<pkg>'` cannot tell a live
|
|
15
|
+
// import from one inside a block comment or a template literal, and it cannot
|
|
16
|
+
// see `importKind` — which is the difference between a build error and five
|
|
17
|
+
// type-only imports that ADR 0032 measured as costing nothing. Both of those are
|
|
18
|
+
// false POSITIVES, the failure mode that teaches people to switch a gate off.
|
|
19
|
+
//
|
|
20
|
+
// WHY ACORN AND NOT THE BUNDLER'S PARSER. Same measured reason as
|
|
21
|
+
// `inline-static-reads.ts`: under GJS the engine is `@gjsify/rolldown-native`,
|
|
22
|
+
// npm `rolldown` is a Node-only N-API crate, and a top-level
|
|
23
|
+
// `import { parseAst } from 'rolldown/parseAst'` puts it in a module that has to
|
|
24
|
+
// load under GJS — the CLI's own bundle died at startup with `createRequire:
|
|
25
|
+
// Cannot require builtin module "fs" synchronously in GJS`. `acorn-typescript`
|
|
26
|
+
// is pure JS and reports `importKind` on both the declaration and each specifier
|
|
27
|
+
// (verified against `.tsx` with JSX, a namespace import and inline `type`
|
|
28
|
+
// specifiers), which is exactly the discrimination the gate turns on.
|
|
29
|
+
//
|
|
30
|
+
// TWO BLIND SPOTS, BOTH MEASURED, AND NEITHER IS SILENT.
|
|
31
|
+
//
|
|
32
|
+
// 1. FORMS THAT CARRY NO NAME — the point of `opaque`. A named import is the
|
|
33
|
+
// only form that carries a name at build time. `import * as RN`, a default
|
|
34
|
+
// import, `export * from`, `await import('…')` and `require('…')` all reach
|
|
35
|
+
// the surface through a value whose members are resolved at RUNTIME, and no
|
|
36
|
+
// parser recovers them without type information this layer does not have.
|
|
37
|
+
// They come back as {@link OpaqueReference}s so the caller can say out loud
|
|
38
|
+
// that the gate cannot answer for this module and that the runtime refusal is
|
|
39
|
+
// what covers it.
|
|
40
|
+
//
|
|
41
|
+
// 2. SYNTAX acorn-typescript 1.4.13 DOES NOT HAVE. Measured against the pinned
|
|
42
|
+
// version (acorn 8.17.0): `const x = 1 satisfies number` fails with
|
|
43
|
+
// `Unexpected token (1:12)`, and a `const` type parameter
|
|
44
|
+
// (`<const T,>(x: T) => x`) fails with `Unexpected token (1:18)`. Both are
|
|
45
|
+
// ordinary TypeScript, and this repository pins `typescript: "^6.0.3"`, so
|
|
46
|
+
// both WILL appear in consumer source. This function throws
|
|
47
|
+
// {@link ImportScanParseError} for them, and the plugin turns that into a
|
|
48
|
+
// warning rather than a build failure — see `react-native-gate.ts` for why a
|
|
49
|
+
// false violation is the worse outcome. `react-native-gate.spec.ts` pins the
|
|
50
|
+
// gap as a fixture that must NOT parse, beside the modern syntax that must, so
|
|
51
|
+
// the day acorn-typescript gains it that vector goes red and someone moves it.
|
|
52
|
+
//
|
|
53
|
+
// A gate whose limits are undocumented is worse than no gate; a gate that fails
|
|
54
|
+
// a valid program is how a gate gets switched off.
|
|
55
|
+
import * as acorn from 'acorn';
|
|
56
|
+
import { tsPlugin } from 'acorn-typescript';
|
|
57
|
+
/**
|
|
58
|
+
* The TypeScript/TSX-capable parser, built once.
|
|
59
|
+
*
|
|
60
|
+
* `acorn-typescript` refuses to parse without `locations: true` (measured in
|
|
61
|
+
* `inline-static-reads.ts`), and the gate needs the positions anyway.
|
|
62
|
+
*/
|
|
63
|
+
const TS_PARSER = acorn.Parser.extend(tsPlugin());
|
|
64
|
+
const PARSE_OPTIONS = {
|
|
65
|
+
ecmaVersion: 'latest',
|
|
66
|
+
sourceType: 'module',
|
|
67
|
+
locations: true,
|
|
68
|
+
allowAwaitOutsideFunction: true,
|
|
69
|
+
allowReturnOutsideFunction: true,
|
|
70
|
+
allowImportExportEverywhere: true,
|
|
71
|
+
};
|
|
72
|
+
/** Raised when a module that mentions a watched package cannot be parsed. */
|
|
73
|
+
export class ImportScanParseError extends Error {
|
|
74
|
+
id;
|
|
75
|
+
name = 'ImportScanParseError';
|
|
76
|
+
constructor(id, cause) {
|
|
77
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
78
|
+
super(`gjsify import scan: cannot read the import list of ${id} — ${detail}. This module mentions a ` +
|
|
79
|
+
`package the build-time support gate watches, so for THIS file the gate is downgraded to ` +
|
|
80
|
+
`the runtime backstop. Known causes, both measured on acorn-typescript 1.4.13: a ` +
|
|
81
|
+
`\`satisfies\` expression, or a \`const\` type parameter. Otherwise the syntax is newer ` +
|
|
82
|
+
`than the bundled parser, or the file is not the module type its extension claims.`, { cause });
|
|
83
|
+
this.id = id;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function positionOf(node) {
|
|
87
|
+
return { line: node.loc?.start?.line ?? 0, column: node.loc?.start?.column ?? 0 };
|
|
88
|
+
}
|
|
89
|
+
/** The string value of a node when it is a plain string literal, else `undefined`. */
|
|
90
|
+
function literalString(node) {
|
|
91
|
+
const value = node?.value;
|
|
92
|
+
return node?.type === 'Literal' && typeof value === 'string' ? value : undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Visit every node in the tree, generically.
|
|
96
|
+
*
|
|
97
|
+
* A bare descent rather than `acorn-walk`, and not as a preference: `walk.simple`
|
|
98
|
+
* looks its visitor up in a table keyed by node type and throws `No walker
|
|
99
|
+
* function defined for node type TSInterfaceDeclaration` on the first interface
|
|
100
|
+
* in a first-party source. A walker with no node table cannot fall behind the
|
|
101
|
+
* parser — which matters precisely because the parser emits TS node types this
|
|
102
|
+
* file has never heard of.
|
|
103
|
+
*/
|
|
104
|
+
function forEachNode(root, visit) {
|
|
105
|
+
const stack = [root];
|
|
106
|
+
while (stack.length > 0) {
|
|
107
|
+
const node = stack.pop();
|
|
108
|
+
if (node === null || typeof node !== 'object')
|
|
109
|
+
continue;
|
|
110
|
+
if (Array.isArray(node)) {
|
|
111
|
+
for (const child of node)
|
|
112
|
+
stack.push(child);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (typeof node.type === 'string')
|
|
116
|
+
visit(node);
|
|
117
|
+
for (const value of Object.values(node)) {
|
|
118
|
+
if (value !== null && typeof value === 'object')
|
|
119
|
+
stack.push(value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Every named import and every opaque reference to one of `specifiers`.
|
|
125
|
+
*
|
|
126
|
+
* `specifiers` is matched EXACTLY. `react-native-web` and
|
|
127
|
+
* `react-native/Libraries/…` are different modules with different contracts, and
|
|
128
|
+
* a prefix match would fail a build over a package this layer never claimed —
|
|
129
|
+
* the deep-subpath case has its own named error in the alias plugin, where the
|
|
130
|
+
* importer is known and the message can say what to do.
|
|
131
|
+
*
|
|
132
|
+
* Type-only imports are absent from the result by construction. ADR 0032 counted
|
|
133
|
+
* five of them (`ViewProps`, `TextProps`, `PressableProps`, `LayoutChangeEvent`,
|
|
134
|
+
* `ColorValue`) in the measured application and recorded that they cost nothing:
|
|
135
|
+
* a type erases at build time and reaches no runtime, so a gate that failed on
|
|
136
|
+
* `import type { ViewProps }` would refuse a program that cannot fail.
|
|
137
|
+
*
|
|
138
|
+
* @throws {ImportScanParseError} when the source cannot be parsed.
|
|
139
|
+
*/
|
|
140
|
+
export function scanNamedImports(code, id, specifiers) {
|
|
141
|
+
const named = [];
|
|
142
|
+
const opaque = [];
|
|
143
|
+
const watched = new Set(specifiers);
|
|
144
|
+
let ast;
|
|
145
|
+
try {
|
|
146
|
+
ast = TS_PARSER.parse(code, PARSE_OPTIONS);
|
|
147
|
+
}
|
|
148
|
+
catch (cause) {
|
|
149
|
+
throw new ImportScanParseError(id, cause);
|
|
150
|
+
}
|
|
151
|
+
// Import and re-export declarations are top level by grammar, so `body` is
|
|
152
|
+
// the whole population — no descent needed and none wanted, because a
|
|
153
|
+
// descent would also visit a `TSModuleDeclaration`'s ambient imports, which
|
|
154
|
+
// declare types and import nothing.
|
|
155
|
+
for (const node of ast.body) {
|
|
156
|
+
if (node.type === 'ImportDeclaration') {
|
|
157
|
+
const specifier = literalString(node.source);
|
|
158
|
+
if (specifier === undefined || !watched.has(specifier))
|
|
159
|
+
continue;
|
|
160
|
+
// `import type { X } from …` — the whole declaration erases.
|
|
161
|
+
if (node.importKind === 'type')
|
|
162
|
+
continue;
|
|
163
|
+
for (const spec of node.specifiers) {
|
|
164
|
+
const at = positionOf(spec);
|
|
165
|
+
if (spec.type === 'ImportSpecifier') {
|
|
166
|
+
// `import { type X }` — the one specifier erases while its
|
|
167
|
+
// value-importing siblings in the same declaration do not.
|
|
168
|
+
if (spec.importKind === 'type')
|
|
169
|
+
continue;
|
|
170
|
+
const imported = spec.imported;
|
|
171
|
+
const name = imported.name ?? (typeof imported.value === 'string' ? imported.value : undefined);
|
|
172
|
+
if (name !== undefined)
|
|
173
|
+
named.push({ name, specifier, ...at });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
opaque.push({
|
|
177
|
+
form: spec.type === 'ImportNamespaceSpecifier' ? 'namespace' : 'default',
|
|
178
|
+
specifier,
|
|
179
|
+
...at,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
186
|
+
const specifier = literalString(node.source);
|
|
187
|
+
if (specifier === undefined || !watched.has(specifier))
|
|
188
|
+
continue;
|
|
189
|
+
if (node.exportKind === 'type')
|
|
190
|
+
continue;
|
|
191
|
+
for (const spec of node.specifiers) {
|
|
192
|
+
if (spec.exportKind === 'type')
|
|
193
|
+
continue;
|
|
194
|
+
// On an `ExportSpecifier` it is `local` that names the binding in
|
|
195
|
+
// the SOURCE module; `exported` is the name this module publishes.
|
|
196
|
+
const local = spec.local;
|
|
197
|
+
const name = local.name ?? (typeof local.value === 'string' ? local.value : undefined);
|
|
198
|
+
if (name !== undefined)
|
|
199
|
+
named.push({ name, specifier, ...positionOf(spec) });
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (node.type === 'ExportAllDeclaration') {
|
|
204
|
+
const specifier = literalString(node.source);
|
|
205
|
+
if (specifier === undefined || !watched.has(specifier))
|
|
206
|
+
continue;
|
|
207
|
+
if (node.exportKind === 'type')
|
|
208
|
+
continue;
|
|
209
|
+
opaque.push({ form: 'export-all', specifier, ...positionOf(node) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// `import('…')` and `require('…')` can sit anywhere, so these need the
|
|
213
|
+
// descent. Only the specifier is recoverable — which member of the namespace
|
|
214
|
+
// the caller reaches for is a runtime question by construction.
|
|
215
|
+
forEachNode(ast, (node) => {
|
|
216
|
+
if (node.type === 'ImportExpression') {
|
|
217
|
+
const specifier = literalString(node.source);
|
|
218
|
+
if (specifier !== undefined && watched.has(specifier)) {
|
|
219
|
+
opaque.push({ form: 'dynamic-import', specifier, ...positionOf(node) });
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (node.type !== 'CallExpression')
|
|
224
|
+
return;
|
|
225
|
+
const call = node;
|
|
226
|
+
// `import(…)` also arrives as a CallExpression with an `Import` callee on
|
|
227
|
+
// some acorn versions; both spellings are accepted so the form is not
|
|
228
|
+
// silently missed by a parser upgrade.
|
|
229
|
+
const isImport = call.callee?.type === 'Import';
|
|
230
|
+
const isRequire = call.callee?.type === 'Identifier' && call.callee.name === 'require';
|
|
231
|
+
if (!isImport && !isRequire)
|
|
232
|
+
return;
|
|
233
|
+
const specifier = literalString(call.arguments?.[0]);
|
|
234
|
+
if (specifier === undefined || !watched.has(specifier))
|
|
235
|
+
return;
|
|
236
|
+
opaque.push({ form: isImport ? 'dynamic-import' : 'require', specifier, ...positionOf(node) });
|
|
237
|
+
});
|
|
238
|
+
return { named, opaque };
|
|
239
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -33,9 +33,13 @@
|
|
|
33
33
|
"lib"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"clear": "gjsify clear lib tsconfig.tsbuildinfo",
|
|
36
|
+
"clear": "gjsify clear lib tsconfig.tsbuildinfo test.node.mjs",
|
|
37
37
|
"check": "gjsify tsc --noEmit",
|
|
38
|
-
"build": "gjsify tsc"
|
|
38
|
+
"build": "gjsify tsc -p tsconfig.build.json",
|
|
39
|
+
"build:test": "gjsify run build:test:node",
|
|
40
|
+
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
41
|
+
"test": "gjsify run build:test && gjsify run test:node",
|
|
42
|
+
"test:node": "node test.node.mjs"
|
|
39
43
|
},
|
|
40
44
|
"repository": {
|
|
41
45
|
"type": "git",
|
|
@@ -56,11 +60,11 @@
|
|
|
56
60
|
],
|
|
57
61
|
"license": "MIT",
|
|
58
62
|
"dependencies": {
|
|
59
|
-
"@gjsify/console": "^0.
|
|
60
|
-
"@gjsify/resolve-npm": "^0.
|
|
61
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
62
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
63
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
63
|
+
"@gjsify/console": "^0.45.0",
|
|
64
|
+
"@gjsify/resolve-npm": "^0.45.0",
|
|
65
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.45.0",
|
|
66
|
+
"@gjsify/rolldown-plugin-pnp": "^0.45.0",
|
|
67
|
+
"@gjsify/vite-plugin-blueprint": "^0.45.0",
|
|
64
68
|
"@rollup/pluginutils": "^5.4.0",
|
|
65
69
|
"acorn": "^8.17.0",
|
|
66
70
|
"acorn-typescript": "^1.4.13",
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
"sass": "^1.101.0"
|
|
71
75
|
},
|
|
72
76
|
"peerDependencies": {
|
|
73
|
-
"@gjsify/lightningcss-native": "^0.
|
|
77
|
+
"@gjsify/lightningcss-native": "^0.45.0",
|
|
74
78
|
"rolldown": "^1.1.4"
|
|
75
79
|
},
|
|
76
80
|
"peerDependenciesMeta": {
|
|
@@ -82,6 +86,8 @@
|
|
|
82
86
|
}
|
|
83
87
|
},
|
|
84
88
|
"devDependencies": {
|
|
89
|
+
"@gjsify/cli": "^0.45.0",
|
|
90
|
+
"@gjsify/unit": "^0.45.0",
|
|
85
91
|
"@types/node": "^25.9.2",
|
|
86
92
|
"rolldown": "^1.1.4",
|
|
87
93
|
"typescript": "^6.0.3"
|