@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.
Files changed (37) hide show
  1. package/lib/app/browser.d.ts +2 -0
  2. package/lib/app/browser.js +4 -2
  3. package/lib/app/gjs.d.ts +2 -0
  4. package/lib/app/gjs.js +32 -2
  5. package/lib/app/nativescript.d.ts +2 -0
  6. package/lib/app/nativescript.js +6 -4
  7. package/lib/app/node.d.ts +7 -2
  8. package/lib/app/node.js +122 -4
  9. package/lib/index.d.ts +9 -1
  10. package/lib/index.js +5 -1
  11. package/lib/library/lib.d.ts +2 -0
  12. package/lib/library/lib.js +12 -1
  13. package/lib/plugin.d.ts +15 -0
  14. package/lib/plugins/intl-segmenter-banner.d.ts +1 -0
  15. package/lib/plugins/intl-segmenter-banner.js +42 -0
  16. package/lib/plugins/platform-resolve.d.ts +133 -8
  17. package/lib/plugins/platform-resolve.js +214 -30
  18. package/lib/plugins/process-stub.js +4 -0
  19. package/lib/plugins/react-native-alias.d.ts +63 -0
  20. package/lib/plugins/react-native-alias.js +128 -0
  21. package/lib/plugins/react-native-gate.d.ts +108 -0
  22. package/lib/plugins/react-native-gate.js +235 -0
  23. package/lib/plugins/rn-route-manifest.d.ts +67 -0
  24. package/lib/plugins/rn-route-manifest.js +166 -0
  25. package/lib/types/index.d.ts +1 -0
  26. package/lib/types/index.js +1 -0
  27. package/lib/types/plugin-options.d.ts +20 -0
  28. package/lib/types/source-dialect.d.ts +42 -0
  29. package/lib/types/source-dialect.js +21 -0
  30. package/lib/utils/detect-free-globals.js +29 -7
  31. package/lib/utils/index.d.ts +2 -0
  32. package/lib/utils/index.js +1 -0
  33. package/lib/utils/jsx-survival.d.ts +31 -0
  34. package/lib/utils/jsx-survival.js +102 -0
  35. package/lib/utils/scan-named-imports.d.ts +47 -0
  36. package/lib/utils/scan-named-imports.js +239 -0
  37. 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 = acorn.parse(code, {
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, {
@@ -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';
@@ -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.42.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.42.0",
59
- "@gjsify/resolve-npm": "^0.42.0",
60
- "@gjsify/rolldown-plugin-deepkit": "^0.42.0",
61
- "@gjsify/rolldown-plugin-pnp": "^0.42.0",
62
- "@gjsify/vite-plugin-blueprint": "^0.42.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.42.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"