@gjsify/rolldown-plugin-gjsify 0.42.0 → 0.44.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/browser.d.ts +2 -0
- package/lib/app/browser.js +4 -2
- package/lib/app/gjs.d.ts +2 -0
- package/lib/app/gjs.js +32 -2
- package/lib/app/nativescript.d.ts +2 -0
- package/lib/app/nativescript.js +6 -4
- package/lib/app/node.d.ts +7 -2
- package/lib/app/node.js +122 -4
- package/lib/index.d.ts +9 -1
- package/lib/index.js +5 -1
- package/lib/library/lib.d.ts +2 -0
- package/lib/library/lib.js +12 -1
- package/lib/plugin.d.ts +15 -0
- package/lib/plugins/intl-segmenter-banner.d.ts +1 -0
- package/lib/plugins/intl-segmenter-banner.js +42 -0
- package/lib/plugins/platform-resolve.d.ts +133 -8
- package/lib/plugins/platform-resolve.js +214 -30
- package/lib/plugins/process-stub.js +4 -0
- 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 +108 -0
- package/lib/plugins/react-native-gate.js +235 -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/detect-free-globals.js +29 -7
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/jsx-survival.d.ts +31 -0
- package/lib/utils/jsx-survival.js +102 -0
- package/lib/utils/scan-named-imports.d.ts +47 -0
- package/lib/utils/scan-named-imports.js +239 -0
- package/package.json +17 -10
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Every dialect this build understands, for CLI choices and error messages. */
|
|
2
|
+
export const SOURCE_DIALECTS = ['react-native'];
|
|
3
|
+
/** Whether `value` names a dialect this build understands. */
|
|
4
|
+
export function isSourceDialect(value) {
|
|
5
|
+
return typeof value === 'string' && SOURCE_DIALECTS.includes(value);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The `--app` targets that actually compose a dialect's plugins.
|
|
9
|
+
*
|
|
10
|
+
* Declared here rather than inferred, because it cannot be inferred: the
|
|
11
|
+
* composition happens in `app/gjs.ts` and `app/node.ts`, and a plugin only ever
|
|
12
|
+
* sees the target it was composed on — so nothing downstream can tell "this
|
|
13
|
+
* target has no dialect support" from "this dialect asked for nothing". The CLI
|
|
14
|
+
* refuses the pair up front instead, and this is the set it refuses against.
|
|
15
|
+
*
|
|
16
|
+
* KEEP IN STEP with the two composition sites. A third target that starts
|
|
17
|
+
* composing and is not added here gets a build refused for no reason; one added
|
|
18
|
+
* here without composing gets a flag accepted and silently ignored, which is the
|
|
19
|
+
* failure this set exists to prevent.
|
|
20
|
+
*/
|
|
21
|
+
export const DIALECT_APPS = new Set(['gjs', 'node']);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as acorn from 'acorn';
|
|
10
10
|
import * as walk from 'acorn-walk';
|
|
11
11
|
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
12
|
+
import { classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
12
13
|
const KNOWN_GLOBALS = new Set(Object.keys(GJS_GLOBALS_MAP));
|
|
13
14
|
/**
|
|
14
15
|
* `<host>.<method>` patterns that must inject a global even though the identifier
|
|
@@ -88,6 +89,33 @@ function extractBindingNames(node) {
|
|
|
88
89
|
return [];
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Parse one analysis chunk, and say what a JSX failure IS.
|
|
94
|
+
*
|
|
95
|
+
* This parse is the first thing a `--globals auto` build does with bundled output, so a
|
|
96
|
+
* `jsx: "preserve"` project with no framework compiler dies HERE — and acorn's own
|
|
97
|
+
* message is `Unexpected token (3:11)`, which names neither JSX, nor the file, nor a
|
|
98
|
+
* setting. Every other parse failure is re-thrown untouched: acorn trailing
|
|
99
|
+
* SpiderMonkey on new syntax is acorn's problem, not the project's, and claiming JSX
|
|
100
|
+
* for it would be a lie with a fix attached.
|
|
101
|
+
*/
|
|
102
|
+
function parseAnalysisChunk(code) {
|
|
103
|
+
try {
|
|
104
|
+
return acorn.parse(code, {
|
|
105
|
+
ecmaVersion: 'latest',
|
|
106
|
+
sourceType: 'module',
|
|
107
|
+
// Acorn rejects shebangs by default, and any project bundling its own CLI gets
|
|
108
|
+
// one hoisted to byte 0.
|
|
109
|
+
allowHashBang: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const jsx = classifyJsxParseFailure(code, err);
|
|
114
|
+
if (jsx === null)
|
|
115
|
+
throw err;
|
|
116
|
+
throw new Error(formatSurvivingJsx(jsx, 'the analysis bundle'), { cause: err });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
91
119
|
/**
|
|
92
120
|
* Core scanner behind `detectFreeGlobals` (web/Node globals map) and
|
|
93
121
|
* `detectGjsAmbientGlobals` (the `--app node` GJS-ambient set), parametrised so the node
|
|
@@ -108,13 +136,7 @@ function scanFreeGlobals(code, opts) {
|
|
|
108
136
|
const knownGlobals = opts.knownGlobals;
|
|
109
137
|
const methodMarkers = opts.methodMarkers ?? {};
|
|
110
138
|
const wasmMarkers = opts.wasmMarkers ?? {};
|
|
111
|
-
const ast =
|
|
112
|
-
ecmaVersion: 'latest',
|
|
113
|
-
sourceType: 'module',
|
|
114
|
-
// Acorn rejects shebangs by default, and any project bundling its own CLI gets
|
|
115
|
-
// one hoisted to byte 0.
|
|
116
|
-
allowHashBang: true,
|
|
117
|
-
});
|
|
139
|
+
const ast = parseAnalysisChunk(code);
|
|
118
140
|
// Pass 1: every declared name in the module.
|
|
119
141
|
const declaredNames = new Set();
|
|
120
142
|
walk.simple(ast, {
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { detectFreeGlobals } from './detect-free-globals.js';
|
|
|
7
7
|
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
8
8
|
export { inlineStaticReads, isAbsoluteFsPath } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
|
+
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
|
+
export type { SurvivingJsx } from './jsx-survival.js';
|
package/lib/utils/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { detectFreeGlobals } from './detect-free-globals.js';
|
|
|
7
7
|
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
8
8
|
export { inlineStaticReads, isAbsoluteFsPath } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
|
+
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Where the parser stopped, and on what. */
|
|
2
|
+
export interface SurvivingJsx {
|
|
3
|
+
/** 1-based line, as acorn reports it. */
|
|
4
|
+
line: number;
|
|
5
|
+
/** 0-based column, as acorn reports it. */
|
|
6
|
+
column: number;
|
|
7
|
+
/** The text around the failure, trimmed so a minified bundle stays readable. */
|
|
8
|
+
excerpt: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Does `err` — a failure from parsing `code` — say that raw JSX survived? Returns the
|
|
12
|
+
* location when the position attributes it to JSX, `null` when it does not.
|
|
13
|
+
*
|
|
14
|
+
* Split from {@link locateSurvivingJsx} so a caller that ALREADY parsed (and threw) can
|
|
15
|
+
* attribute its own failure without parsing a second time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function classifyJsxParseFailure(code: string, err: unknown): SurvivingJsx | null;
|
|
18
|
+
/**
|
|
19
|
+
* Parse `code` as an ES module and report surviving JSX. `null` means either "parses
|
|
20
|
+
* fine" or "fails for a reason that is not JSX" — the two cases this must not conflate,
|
|
21
|
+
* because only the first is a healthy bundle and only the second is acorn's problem
|
|
22
|
+
* rather than the build's.
|
|
23
|
+
*/
|
|
24
|
+
export declare function locateSurvivingJsx(code: string): SurvivingJsx | null;
|
|
25
|
+
/**
|
|
26
|
+
* The actionable message: what the artifact does, what the runtime will do with it, and
|
|
27
|
+
* the two settings that fix it. Shared by the post-bundle artifact gate and the
|
|
28
|
+
* `--globals auto` analysis pass, which hits the same source with a bare
|
|
29
|
+
* "Unexpected token (3:11)" naming neither JSX nor a setting.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatSurvivingJsx(found: SurvivingJsx, label: string): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Raw JSX surviving into an emitted bundle — the syntax-level oracle, and why it is a
|
|
2
|
+
// syntax check rather than a pattern match.
|
|
3
|
+
//
|
|
4
|
+
// `jsx: "preserve"` is the setting `babel-preset-solid` and the Vue SFC compiler need:
|
|
5
|
+
// it tells oxc to leave JSX alone so the framework's own compiler can handle it. When
|
|
6
|
+
// that compiler is absent, raw JSX travels all the way into the `.gjs.mjs` and NOTHING
|
|
7
|
+
// between `gjsify build` and the artifact notices — rolldown emits it, the build exits
|
|
8
|
+
// 0, and GJS refuses the file at LOAD, before one line runs. MEASURED on a two-line
|
|
9
|
+
// `.tsx` built `--app gjs --globals none`: exit 0, `r=<box title="hi"/>` in the bundle,
|
|
10
|
+
// and `SyntaxError: expected expression, got '<'` under `gjs -m`.
|
|
11
|
+
//
|
|
12
|
+
// A pattern match is the wrong oracle in both directions: `<div` hits a string inside a
|
|
13
|
+
// test name, and misses `<Foo.Bar>`, `<>` and every tag the pattern did not think of.
|
|
14
|
+
// The right question is the one GJS asks — "does this parse as an ES module?" — so this
|
|
15
|
+
// asks acorn, the same parser the `--globals auto` detector already runs over bundled
|
|
16
|
+
// output.
|
|
17
|
+
//
|
|
18
|
+
// A parse failure ALONE is not attributed to JSX: acorn trails SpiderMonkey on new
|
|
19
|
+
// syntax, and blaming JSX for something else would send the reader after a setting that
|
|
20
|
+
// is already right. The attribution is the FAILURE POSITION — a `<` followed by an
|
|
21
|
+
// identifier start, `>` (fragment) or `/` (closing tag). That is where acorn stops on
|
|
22
|
+
// every JSX shape measured: `<box …/>`, `<box>hi</box>`, `<>…</>`, `<Foo.Bar x={1}>`,
|
|
23
|
+
// JSX as a call argument, and JSX in a multi-declarator `const`.
|
|
24
|
+
import * as acorn from 'acorn';
|
|
25
|
+
/**
|
|
26
|
+
* What may follow the `<` for the failure to read as JSX rather than as a broken
|
|
27
|
+
* comparison: a tag name, a fragment (`<>`) or a closing tag (`</`).
|
|
28
|
+
*/
|
|
29
|
+
const JSX_AFTER_ANGLE_RE = /^[A-Za-z_$>/]/;
|
|
30
|
+
/** Characters of context kept around the failure position. */
|
|
31
|
+
const EXCERPT_RADIUS = 60;
|
|
32
|
+
function excerptAt(code, pos) {
|
|
33
|
+
const from = Math.max(0, pos - EXCERPT_RADIUS);
|
|
34
|
+
const to = Math.min(code.length, pos + EXCERPT_RADIUS);
|
|
35
|
+
// Newlines and tabs would break the one-line-per-fact shape of the message.
|
|
36
|
+
const text = code.slice(from, to).replace(/[\r\n\t]+/g, ' ');
|
|
37
|
+
return `${from > 0 ? '…' : ''}${text}${to < code.length ? '…' : ''}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Does `err` — a failure from parsing `code` — say that raw JSX survived? Returns the
|
|
41
|
+
* location when the position attributes it to JSX, `null` when it does not.
|
|
42
|
+
*
|
|
43
|
+
* Split from {@link locateSurvivingJsx} so a caller that ALREADY parsed (and threw) can
|
|
44
|
+
* attribute its own failure without parsing a second time.
|
|
45
|
+
*/
|
|
46
|
+
export function classifyJsxParseFailure(code, err) {
|
|
47
|
+
const pos = err.pos;
|
|
48
|
+
const loc = err.loc;
|
|
49
|
+
if (typeof pos !== 'number')
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof loc?.line !== 'number' || typeof loc.column !== 'number')
|
|
52
|
+
return null;
|
|
53
|
+
if (code[pos] !== '<')
|
|
54
|
+
return null;
|
|
55
|
+
if (!JSX_AFTER_ANGLE_RE.test(code.slice(pos + 1, pos + 2)))
|
|
56
|
+
return null;
|
|
57
|
+
return { line: loc.line, column: loc.column, excerpt: excerptAt(code, pos) };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse `code` as an ES module and report surviving JSX. `null` means either "parses
|
|
61
|
+
* fine" or "fails for a reason that is not JSX" — the two cases this must not conflate,
|
|
62
|
+
* because only the first is a healthy bundle and only the second is acorn's problem
|
|
63
|
+
* rather than the build's.
|
|
64
|
+
*/
|
|
65
|
+
export function locateSurvivingJsx(code) {
|
|
66
|
+
try {
|
|
67
|
+
acorn.parse(code, {
|
|
68
|
+
ecmaVersion: 'latest',
|
|
69
|
+
sourceType: 'module',
|
|
70
|
+
// Any project bundling its own CLI gets a shebang hoisted to byte 0.
|
|
71
|
+
allowHashBang: true,
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return classifyJsxParseFailure(code, err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The actionable message: what the artifact does, what the runtime will do with it, and
|
|
81
|
+
* the two settings that fix it. Shared by the post-bundle artifact gate and the
|
|
82
|
+
* `--globals auto` analysis pass, which hits the same source with a bare
|
|
83
|
+
* "Unexpected token (3:11)" naming neither JSX nor a setting.
|
|
84
|
+
*/
|
|
85
|
+
export function formatSurvivingJsx(found, label) {
|
|
86
|
+
return [
|
|
87
|
+
`gjsify build: ${label} still contains raw JSX at ${found.line}:${found.column} — it is not valid ` +
|
|
88
|
+
'JavaScript, so GJS aborts at load with "SyntaxError: expected expression, got \'<\'" before one ' +
|
|
89
|
+
'line runs.',
|
|
90
|
+
` ${found.excerpt}`,
|
|
91
|
+
'',
|
|
92
|
+
'JSX reaches an artifact only when the transform was told to PRESERVE it (`jsx: "preserve"` in ' +
|
|
93
|
+
'`gjsify.bundler.transform.jsx` or in tsconfig `compilerOptions.jsx`) and nothing compiled it ' +
|
|
94
|
+
'afterwards. Either:',
|
|
95
|
+
' - register the framework compiler that consumes preserved JSX (`babel-preset-solid` for Solid, the ' +
|
|
96
|
+
'Vue SFC compiler) under `gjsify.bundler.plugins`, or',
|
|
97
|
+
' - drop `preserve` and give the automatic runtime an import source that resolves AND runs: ' +
|
|
98
|
+
'`bundler.transform.jsx: { "importSource": "<pkg exporting ./jsx-runtime>" }`, or tsconfig ' +
|
|
99
|
+
'`"jsx": "react-jsx"` + `"jsxImportSource"`. Not `@gjsify/gtk-host` — its `/jsx-runtime` is a ' +
|
|
100
|
+
'TYPE surface and throws when called.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** One named import (or re-export) of `name` from a watched package. */
|
|
2
|
+
export interface NamedImport {
|
|
3
|
+
/** The name as the SOURCE module exports it, not the local alias. */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The verbatim specifier it came from. */
|
|
6
|
+
readonly specifier: string;
|
|
7
|
+
/** 1-based line, as acorn reports it. */
|
|
8
|
+
readonly line: number;
|
|
9
|
+
/** 0-based column, as acorn reports it. */
|
|
10
|
+
readonly column: number;
|
|
11
|
+
}
|
|
12
|
+
/** A reference to a watched package whose imported NAMES are not statically knowable. */
|
|
13
|
+
export interface OpaqueReference {
|
|
14
|
+
/** Which form hid the names. */
|
|
15
|
+
readonly form: 'namespace' | 'default' | 'export-all' | 'dynamic-import' | 'require';
|
|
16
|
+
readonly specifier: string;
|
|
17
|
+
readonly line: number;
|
|
18
|
+
readonly column: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ScanResult {
|
|
21
|
+
readonly named: readonly NamedImport[];
|
|
22
|
+
readonly opaque: readonly OpaqueReference[];
|
|
23
|
+
}
|
|
24
|
+
/** Raised when a module that mentions a watched package cannot be parsed. */
|
|
25
|
+
export declare class ImportScanParseError extends Error {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly name = "ImportScanParseError";
|
|
28
|
+
constructor(id: string, cause: unknown);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Every named import and every opaque reference to one of `specifiers`.
|
|
32
|
+
*
|
|
33
|
+
* `specifiers` is matched EXACTLY. `react-native-web` and
|
|
34
|
+
* `react-native/Libraries/…` are different modules with different contracts, and
|
|
35
|
+
* a prefix match would fail a build over a package this layer never claimed —
|
|
36
|
+
* the deep-subpath case has its own named error in the alias plugin, where the
|
|
37
|
+
* importer is known and the message can say what to do.
|
|
38
|
+
*
|
|
39
|
+
* Type-only imports are absent from the result by construction. ADR 0032 counted
|
|
40
|
+
* five of them (`ViewProps`, `TextProps`, `PressableProps`, `LayoutChangeEvent`,
|
|
41
|
+
* `ColorValue`) in the measured application and recorded that they cost nothing:
|
|
42
|
+
* a type erases at build time and reaches no runtime, so a gate that failed on
|
|
43
|
+
* `import type { ViewProps }` would refuse a program that cannot fail.
|
|
44
|
+
*
|
|
45
|
+
* @throws {ImportScanParseError} when the source cannot be parsed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function scanNamedImports(code: string, id: string, specifiers: readonly string[]): ScanResult;
|
|
@@ -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.44.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,13 +33,18 @@
|
|
|
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",
|
|
42
|
-
"url": "git+https://github.com/gjsify/gjsify.git"
|
|
46
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
47
|
+
"directory": "packages/infra/rolldown-plugin-gjsify"
|
|
43
48
|
},
|
|
44
49
|
"bugs": {
|
|
45
50
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
@@ -55,11 +60,11 @@
|
|
|
55
60
|
],
|
|
56
61
|
"license": "MIT",
|
|
57
62
|
"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.
|
|
63
|
+
"@gjsify/console": "^0.44.0",
|
|
64
|
+
"@gjsify/resolve-npm": "^0.44.0",
|
|
65
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.44.0",
|
|
66
|
+
"@gjsify/rolldown-plugin-pnp": "^0.44.0",
|
|
67
|
+
"@gjsify/vite-plugin-blueprint": "^0.44.0",
|
|
63
68
|
"@rollup/pluginutils": "^5.4.0",
|
|
64
69
|
"acorn": "^8.17.0",
|
|
65
70
|
"acorn-typescript": "^1.4.13",
|
|
@@ -69,7 +74,7 @@
|
|
|
69
74
|
"sass": "^1.101.0"
|
|
70
75
|
},
|
|
71
76
|
"peerDependencies": {
|
|
72
|
-
"@gjsify/lightningcss-native": "^0.
|
|
77
|
+
"@gjsify/lightningcss-native": "^0.44.0",
|
|
73
78
|
"rolldown": "^1.1.4"
|
|
74
79
|
},
|
|
75
80
|
"peerDependenciesMeta": {
|
|
@@ -81,6 +86,8 @@
|
|
|
81
86
|
}
|
|
82
87
|
},
|
|
83
88
|
"devDependencies": {
|
|
89
|
+
"@gjsify/cli": "^0.44.0",
|
|
90
|
+
"@gjsify/unit": "^0.44.0",
|
|
84
91
|
"@types/node": "^25.9.2",
|
|
85
92
|
"rolldown": "^1.1.4",
|
|
86
93
|
"typescript": "^6.0.3"
|