@gjsify/rolldown-plugin-gjsify 0.45.0 → 0.47.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/index.d.ts +3 -3
- package/lib/index.js +2 -2
- package/lib/plugins/gjs-imports-empty.js +16 -1
- package/lib/plugins/react-native-alias.d.ts +110 -7
- package/lib/plugins/react-native-alias.js +211 -33
- package/lib/plugins/react-native-gate.d.ts +30 -46
- package/lib/plugins/react-native-gate.js +46 -70
- package/package.json +9 -9
package/lib/index.d.ts
CHANGED
|
@@ -26,9 +26,9 @@ export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, des
|
|
|
26
26
|
export type { PlatformResolvePluginOptions, NativescriptPlatform } from './plugins/platform-resolve.js';
|
|
27
27
|
export { rnRouteManifestPlugin, renderRouteManifest, walkRoutes, RouteManifestError, RN_ROUTES_MODULE_ID, MAX_ROUTE_DEPTH, } from './plugins/rn-route-manifest.js';
|
|
28
28
|
export type { RnRouteManifestOptions, FoundRoute } from './plugins/rn-route-manifest.js';
|
|
29
|
-
export { reactNativeAliasPlugin, classifyReactNativeSpecifier, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, } from './plugins/react-native-alias.js';
|
|
30
|
-
export type { ReactNativeSpecifierKind } from './plugins/react-native-alias.js';
|
|
31
|
-
export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
|
|
29
|
+
export { reactNativeAliasPlugin, classifyReactNativeSpecifier, couldBeSurfaceSpecifier, loadLayer, FALLBACK_SURFACES, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, SURFACE_MENTION, SURFACE_NAME_PREFIXES, } from './plugins/react-native-alias.js';
|
|
30
|
+
export type { AliasedSurface, LayerReader, ReactNativeAliasOptions, ReactNativeSpecifierKind, } from './plugins/react-native-alias.js';
|
|
31
|
+
export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, watchedSpecifiers, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
|
|
32
32
|
export type { ReactNativeSupportGateOptions, SupportTableReader, SupportTableResolver, SupportViolation, } from './plugins/react-native-gate.js';
|
|
33
33
|
export { scanNamedImports, ImportScanParseError } from './utils/scan-named-imports.js';
|
|
34
34
|
export type { NamedImport, OpaqueReference, ScanResult } from './utils/scan-named-imports.js';
|
package/lib/index.js
CHANGED
|
@@ -16,8 +16,8 @@ export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier,
|
|
|
16
16
|
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, isGjsifyNativeBridge, detectNapiRsEntry, hostNapiRsTriple, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
17
17
|
export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
|
18
18
|
export { rnRouteManifestPlugin, renderRouteManifest, walkRoutes, RouteManifestError, RN_ROUTES_MODULE_ID, MAX_ROUTE_DEPTH, } from './plugins/rn-route-manifest.js';
|
|
19
|
-
export { reactNativeAliasPlugin, classifyReactNativeSpecifier, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, } from './plugins/react-native-alias.js';
|
|
20
|
-
export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
|
|
19
|
+
export { reactNativeAliasPlugin, classifyReactNativeSpecifier, couldBeSurfaceSpecifier, loadLayer, FALLBACK_SURFACES, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, SURFACE_MENTION, SURFACE_NAME_PREFIXES, } from './plugins/react-native-alias.js';
|
|
20
|
+
export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, watchedSpecifiers, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
|
|
21
21
|
export { scanNamedImports, ImportScanParseError } from './utils/scan-named-imports.js';
|
|
22
22
|
export * from './plugin.js';
|
|
23
23
|
import { gjsifyPlugin } from './plugin.js';
|
|
@@ -37,6 +37,21 @@ export function gjsImportsEmptyPlugin(options = {}) {
|
|
|
37
37
|
const emptyGirs = options.emptyGirs ?? true;
|
|
38
38
|
// When `@girs/*` is carved out, only `gi://*` reaches the empty redirect.
|
|
39
39
|
const matcher = emptyGirs ? /^(@girs\/|gi:\/\/)/ : /^gi:\/\//;
|
|
40
|
+
// …with one subpath excluded on EVERY target, carve-out or not.
|
|
41
|
+
//
|
|
42
|
+
// `@girs/<ns>/vocabulary` is generated DATA — property names, signal names,
|
|
43
|
+
// enum nicks, versions (ADR 0029). It imports no `gi://`, binds no library and
|
|
44
|
+
// loads under plain Node; the reason it sits in `@girs` is that ts-for-gir
|
|
45
|
+
// generates it, not that it needs GJS. Emptying it produces
|
|
46
|
+
// `[MISSING_EXPORT] "PROVENANCE" is not exported by "\0gjsify-empty-gjs-import"`
|
|
47
|
+
// at bundle time, which reads like a missing export rather than a substituted
|
|
48
|
+
// module.
|
|
49
|
+
//
|
|
50
|
+
// The same over-broad assumption lived in `GIRS_VALUE_RE`
|
|
51
|
+
// (manifest-conformance), where it made gtk-host's honest `node: "polyfill"`
|
|
52
|
+
// read as runtime drift. Two tools, one rule: the package scope says who
|
|
53
|
+
// generated a file, the subpath says what it needs.
|
|
54
|
+
const isVocabulary = (source) => /^@girs\/[^/]+\/vocabulary$/.test(source);
|
|
40
55
|
return {
|
|
41
56
|
name: 'gjsify-gjs-imports-empty',
|
|
42
57
|
resolveId: {
|
|
@@ -48,7 +63,7 @@ export function gjsImportsEmptyPlugin(options = {}) {
|
|
|
48
63
|
// matched specifiers; let everything else (including `@girs/*`
|
|
49
64
|
// when `emptyGirs` is false) fall through to the default
|
|
50
65
|
// resolver chain.
|
|
51
|
-
if (!matcher.test(source))
|
|
66
|
+
if (!matcher.test(source) || isVocabulary(source))
|
|
52
67
|
return null;
|
|
53
68
|
return { id: GJSIMPORTS_VIRTUAL_ID };
|
|
54
69
|
},
|
|
@@ -3,15 +3,43 @@ import type { Plugin } from 'rolldown';
|
|
|
3
3
|
export declare const REACT_NATIVE_ALIAS_TARGET = "@gjsify/react-native";
|
|
4
4
|
/** The specifier an unmodified React Native application writes. */
|
|
5
5
|
export declare const REACT_NATIVE_SPECIFIER = "react-native";
|
|
6
|
+
/**
|
|
7
|
+
* One npm package the layer answers for, and the specifier that answers it.
|
|
8
|
+
*
|
|
9
|
+
* ADR 0036 § 2: `react-native` and `expo-router` are the first two rows of a registry
|
|
10
|
+
* rather than two special cases, because a real React Native application imports
|
|
11
|
+
* sixteen more package names and every one of them used to fail at MODULE RESOLUTION —
|
|
12
|
+
* the bundler said npm could not find it, which names npm rather than this layer.
|
|
13
|
+
*
|
|
14
|
+
* READ FROM THE LAYER, never listed here. `@gjsify/react-native/support-table` exports
|
|
15
|
+
* the registry, the gate already imports that subpath to read the table, and a second
|
|
16
|
+
* list in the bundler plugin is the drift this whole arrangement exists to remove: the
|
|
17
|
+
* plugin would then alias a surface whose table it cannot see, or refuse one the layer
|
|
18
|
+
* has grown.
|
|
19
|
+
*/
|
|
20
|
+
export interface AliasedSurface {
|
|
21
|
+
readonly module: string;
|
|
22
|
+
readonly target: string;
|
|
23
|
+
}
|
|
6
24
|
/** What a specifier is, as far as this alias is concerned. */
|
|
7
25
|
export type ReactNativeSpecifierKind =
|
|
8
|
-
/** Exactly
|
|
26
|
+
/** Exactly a declared surface's own npm name — the mirrored export surface. */
|
|
9
27
|
{
|
|
10
28
|
readonly kind: 'root';
|
|
29
|
+
readonly target: string;
|
|
11
30
|
}
|
|
12
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* `<a declared surface>/<subpath>` — internals, which are not mirrored.
|
|
33
|
+
*
|
|
34
|
+
* The MODULE and the TARGET come along because the refusal names them: `react-native`
|
|
35
|
+
* is no longer the only surface with a subpath layout below it, and
|
|
36
|
+
* `@expo/vector-icons/Ionicons` and `expo-router/entry` are both ordinary things a
|
|
37
|
+
* real application writes.
|
|
38
|
+
*/
|
|
13
39
|
| {
|
|
14
40
|
readonly kind: 'subpath';
|
|
41
|
+
readonly module: string;
|
|
42
|
+
readonly target: string;
|
|
15
43
|
readonly subpath: string;
|
|
16
44
|
}
|
|
17
45
|
/** Anything else, including `@gjsify/react-native` itself. */
|
|
@@ -29,7 +57,7 @@ export type ReactNativeSpecifierKind =
|
|
|
29
57
|
* onto the mirror would replace a package that legitimately does not work with
|
|
30
58
|
* one that silently does the wrong thing.
|
|
31
59
|
*/
|
|
32
|
-
export declare function classifyReactNativeSpecifier(source: string): ReactNativeSpecifierKind;
|
|
60
|
+
export declare function classifyReactNativeSpecifier(source: string, surfaces?: readonly AliasedSurface[]): ReactNativeSpecifierKind;
|
|
33
61
|
/**
|
|
34
62
|
* Thrown for `react-native/<subpath>` — an import of React Native's internals.
|
|
35
63
|
*
|
|
@@ -43,21 +71,96 @@ export declare function classifyReactNativeSpecifier(source: string): ReactNativ
|
|
|
43
71
|
export declare class ReactNativeDeepImportError extends Error {
|
|
44
72
|
readonly specifier: string;
|
|
45
73
|
readonly importer: string;
|
|
74
|
+
readonly module: string;
|
|
75
|
+
readonly target: string;
|
|
46
76
|
readonly name = "ReactNativeDeepImportError";
|
|
47
|
-
constructor(specifier: string, importer: string);
|
|
77
|
+
constructor(specifier: string, importer: string, module?: string, target?: string);
|
|
48
78
|
}
|
|
49
79
|
/** Thrown when the alias target itself cannot be resolved. */
|
|
50
80
|
export declare class ReactNativeAliasTargetMissingError extends Error {
|
|
81
|
+
readonly specifier: string;
|
|
82
|
+
readonly target: string;
|
|
51
83
|
readonly importer: string;
|
|
52
84
|
readonly name = "ReactNativeAliasTargetMissingError";
|
|
53
|
-
constructor(importer: string);
|
|
85
|
+
constructor(specifier: string, target: string, importer: string);
|
|
86
|
+
}
|
|
87
|
+
/** The subpath that carries the registry and the table in an installed layer. */
|
|
88
|
+
export declare const SUPPORT_TABLE_SUBPATH = "@gjsify/react-native/support-table";
|
|
89
|
+
/** The slice of the layer both plugins need, structurally so a spec can supply a fixture. */
|
|
90
|
+
export interface LayerReader {
|
|
91
|
+
/** Every declared surface, in lookup order. */
|
|
92
|
+
readonly SURFACES: readonly AliasedSurface[];
|
|
93
|
+
/** May a build import this name from this module? */
|
|
94
|
+
isImportable(name: string, module?: string): boolean;
|
|
95
|
+
/** The one sentence a build error and a runtime throw both print. */
|
|
96
|
+
explainUnsupported(name: string, module?: string): string;
|
|
54
97
|
}
|
|
98
|
+
/** The resolver slice the loader needs, so it is testable without a bundler. */
|
|
99
|
+
export interface SupportTableResolver {
|
|
100
|
+
resolve(specifier: string, importer: string): Promise<{
|
|
101
|
+
id: string;
|
|
102
|
+
external?: boolean | string;
|
|
103
|
+
} | null>;
|
|
104
|
+
}
|
|
105
|
+
/** Thrown when a plugin is on but the layer it must read is not reachable. */
|
|
106
|
+
export declare class SupportTableUnreadableError extends Error {
|
|
107
|
+
readonly name = "SupportTableUnreadableError";
|
|
108
|
+
constructor(detail: string, cause?: unknown);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolve and import the layer's support-table subpath.
|
|
112
|
+
*
|
|
113
|
+
* `importer` is a module from the project being built, so the resolve lands in the
|
|
114
|
+
* PROJECT's dependency tree rather than the CLI's.
|
|
115
|
+
*/
|
|
116
|
+
export declare function loadLayer(resolver: SupportTableResolver, importer: string, load?: (href: string) => Promise<unknown>): Promise<LayerReader>;
|
|
55
117
|
/**
|
|
56
|
-
*
|
|
118
|
+
* The one row that is known WITHOUT reading the layer.
|
|
119
|
+
*
|
|
120
|
+
* Used when the layer cannot be read at all, and that is a deliberate degradation
|
|
121
|
+
* rather than a default. `gjsify build --dialect react-native` is an opt-in, so a
|
|
122
|
+
* consumer who asked for it and has no layer installed meant `react-native` — the one
|
|
123
|
+
* specifier whose name is a fact rather than a lookup — and that import gets the
|
|
124
|
+
* named "target does not resolve" error it always got. Every other surface then
|
|
125
|
+
* simply fails to resolve the way it did before ADR 0036, which is no worse than
|
|
126
|
+
* before and much better than every `resolveId` in the build throwing.
|
|
127
|
+
*/
|
|
128
|
+
export declare const FALLBACK_SURFACES: readonly AliasedSurface[];
|
|
129
|
+
/**
|
|
130
|
+
* Rewrite a declared surface's specifier onto the subpath that answers it.
|
|
57
131
|
*
|
|
58
132
|
* `order: 'pre'` so the claim lands before the externals policy and the
|
|
59
133
|
* substitution-table alias layer get a look — the same position
|
|
60
134
|
* `platformResolvePlugin` holds, and for the same reason: a redirect that runs
|
|
61
135
|
* after `externalsPlugin` would find the specifier already externalised.
|
|
62
136
|
*/
|
|
63
|
-
export
|
|
137
|
+
export interface ReactNativeAliasOptions {
|
|
138
|
+
/** The layer, when the caller already has it. Left unset in a real build. */
|
|
139
|
+
layer?: LayerReader;
|
|
140
|
+
}
|
|
141
|
+
export declare function reactNativeAliasPlugin(options?: ReactNativeAliasOptions): Plugin;
|
|
142
|
+
/**
|
|
143
|
+
* The prefixes every declared surface's npm name starts with.
|
|
144
|
+
*
|
|
145
|
+
* A PREFILTER AND NOT A DECISION: the registry decides, and these exist only so the
|
|
146
|
+
* two hot paths — one `resolveId` per specifier in the build, one `transform` per
|
|
147
|
+
* module — can say "certainly not a surface" without resolving or parsing anything.
|
|
148
|
+
* `react-native-alias.spec.ts` asserts every row of the LAYER's own registry starts
|
|
149
|
+
* with one of them, so a surface named outside this list fails a test rather than
|
|
150
|
+
* being silently skipped by the alias and the gate together.
|
|
151
|
+
*
|
|
152
|
+
* `@gjsify/react-native` is here because a gjsify-native application writes the target
|
|
153
|
+
* directly, and the gate watches both spellings of every surface.
|
|
154
|
+
*/
|
|
155
|
+
export declare const SURFACE_NAME_PREFIXES: readonly string[];
|
|
156
|
+
/** Could this specifier be a declared surface? Cheap, and deliberately over-inclusive. */
|
|
157
|
+
export declare const couldBeSurfaceSpecifier: (specifier: string) => boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Does this SOURCE mention a specifier that could be a surface?
|
|
160
|
+
*
|
|
161
|
+
* Anchored on the opening QUOTE, which is the whole point: a bare `expo-` test over
|
|
162
|
+
* source text would match the word `export`, so the fragment has to sit where a module
|
|
163
|
+
* specifier sits. Over-inclusive by design (a string literal in a comment matches);
|
|
164
|
+
* under-inclusive never, which is the direction that matters for a gate.
|
|
165
|
+
*/
|
|
166
|
+
export declare const SURFACE_MENTION: RegExp;
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// unresolvable target is a named error rather than a silent external — the
|
|
30
30
|
// substitution IS the promise, so its failure is the one thing that must not
|
|
31
31
|
// exit 0.
|
|
32
|
+
import { pathToFileURL } from 'node:url';
|
|
32
33
|
/** The package whose export surface mirrors React Native's (ADR 0032 § 2). */
|
|
33
34
|
export const REACT_NATIVE_ALIAS_TARGET = '@gjsify/react-native';
|
|
34
35
|
/** The specifier an unmodified React Native application writes. */
|
|
@@ -44,11 +45,36 @@ export const REACT_NATIVE_SPECIFIER = 'react-native';
|
|
|
44
45
|
* onto the mirror would replace a package that legitimately does not work with
|
|
45
46
|
* one that silently does the wrong thing.
|
|
46
47
|
*/
|
|
47
|
-
export function classifyReactNativeSpecifier(source) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
export function classifyReactNativeSpecifier(source, surfaces = [{ module: REACT_NATIVE_SPECIFIER, target: REACT_NATIVE_ALIAS_TARGET }]) {
|
|
49
|
+
for (const surface of surfaces) {
|
|
50
|
+
if (source === surface.module)
|
|
51
|
+
return { kind: 'root', target: surface.target };
|
|
52
|
+
}
|
|
53
|
+
// A DEEP IMPORT is only a deep import of a surface this layer answers for. The
|
|
54
|
+
// exact-match loop above runs FIRST for the same reason it always did:
|
|
55
|
+
// `react-native-web`, `react-native-reanimated` and `react-native-svg` all start
|
|
56
|
+
// with the same eleven characters and none of them is `react-native`, so the
|
|
57
|
+
// prefix test has to come after every exact name — including the surface names
|
|
58
|
+
// `react-native-safe-area-context` and `react-native-gesture-handler`, which a
|
|
59
|
+
// prefix test on `react-native/` would not catch but a careless `react-native-`
|
|
60
|
+
// one would.
|
|
61
|
+
//
|
|
62
|
+
// EVERY SURFACE, not only `react-native`, and that gap was reachable from ordinary
|
|
63
|
+
// code: `@expo/vector-icons/Ionicons` is the spelling `@expo/vector-icons`' own
|
|
64
|
+
// documentation uses, and `expo-router/entry` is what an Expo application's
|
|
65
|
+
// `package.json#main` points at. Both classified as `other` before this, so the
|
|
66
|
+
// alias returned null and the build failed at MODULE RESOLUTION — npm's "cannot
|
|
67
|
+
// find package", which is the exact failure ADR 0036 exists to replace with a
|
|
68
|
+
// sentence.
|
|
69
|
+
for (const surface of surfaces) {
|
|
70
|
+
if (source.startsWith(`${surface.module}/`)) {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'subpath',
|
|
73
|
+
module: surface.module,
|
|
74
|
+
target: surface.target,
|
|
75
|
+
subpath: source.slice(surface.module.length + 1),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
52
78
|
}
|
|
53
79
|
return { kind: 'other' };
|
|
54
80
|
}
|
|
@@ -65,64 +91,216 @@ export function classifyReactNativeSpecifier(source) {
|
|
|
65
91
|
export class ReactNativeDeepImportError extends Error {
|
|
66
92
|
specifier;
|
|
67
93
|
importer;
|
|
94
|
+
module;
|
|
95
|
+
target;
|
|
68
96
|
name = 'ReactNativeDeepImportError';
|
|
69
|
-
constructor(specifier, importer) {
|
|
70
|
-
super(`gjsify react-native alias: "${specifier}" (imported by ${importer}) reaches into
|
|
71
|
-
`
|
|
72
|
-
`
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
constructor(specifier, importer, module = REACT_NATIVE_SPECIFIER, target = REACT_NATIVE_ALIAS_TARGET) {
|
|
98
|
+
super(`gjsify react-native alias: "${specifier}" (imported by ${importer}) reaches into ` +
|
|
99
|
+
`"${module}"'s internals. The alias to ${target} covers the PACKAGE ROOT, because that is ` +
|
|
100
|
+
`the export surface ADR 0032 § 2 mirrors and ADR 0036 § 1 gives one subpath each; the ` +
|
|
101
|
+
`module layout below it has no counterpart here` +
|
|
102
|
+
(module === REACT_NATIVE_SPECIFIER
|
|
103
|
+
? ` — react-native's own exists because of Metro and the native bridge`
|
|
104
|
+
: '') +
|
|
105
|
+
`. Import the name from "${module}" instead, or from "${target}" directly if it is a ` +
|
|
106
|
+
`gjsify-only addition.`);
|
|
76
107
|
this.specifier = specifier;
|
|
77
108
|
this.importer = importer;
|
|
109
|
+
this.module = module;
|
|
110
|
+
this.target = target;
|
|
78
111
|
}
|
|
79
112
|
}
|
|
80
113
|
/** Thrown when the alias target itself cannot be resolved. */
|
|
81
114
|
export class ReactNativeAliasTargetMissingError extends Error {
|
|
115
|
+
specifier;
|
|
116
|
+
target;
|
|
82
117
|
importer;
|
|
83
118
|
name = 'ReactNativeAliasTargetMissingError';
|
|
84
|
-
constructor(importer) {
|
|
85
|
-
super(`gjsify react-native alias: "${
|
|
86
|
-
`to ${
|
|
87
|
-
`drop the react-native opt-in. NOT falling back
|
|
88
|
-
`written for a React Native runtime, so the bundle
|
|
89
|
-
`on a
|
|
119
|
+
constructor(specifier, target, importer) {
|
|
120
|
+
super(`gjsify react-native alias: "${specifier}" (imported by ${importer}) is aliased ` +
|
|
121
|
+
`to ${target}, which does not resolve from this project. Install ${REACT_NATIVE_ALIAS_TARGET} ` +
|
|
122
|
+
`(and build it, if it is a workspace link), or drop the react-native opt-in. NOT falling back ` +
|
|
123
|
+
`to the real package on purpose: it is written for a React Native runtime, so the bundle ` +
|
|
124
|
+
`would build and then fail in the window on a native module that does not exist here.`);
|
|
125
|
+
this.specifier = specifier;
|
|
126
|
+
this.target = target;
|
|
90
127
|
this.importer = importer;
|
|
91
128
|
}
|
|
92
129
|
}
|
|
130
|
+
// --- reading the layer (ADR 0036 § 2) ----------------------------------------
|
|
131
|
+
//
|
|
132
|
+
// BOTH PLUGINS READ THE LAYER, NEVER A COPY. `@gjsify/react-native/support-table` is
|
|
133
|
+
// a published subpath exporting the surface registry plus `isImportable` and
|
|
134
|
+
// `explainUnsupported`, and it is resolved through the BUNDLER's own resolver from a
|
|
135
|
+
// module of the project being built — so the answer describes the layer this bundle
|
|
136
|
+
// will actually contain, and a version skew between plugin and layer cannot make
|
|
137
|
+
// either lie. Reading the SOURCE the way `scripts/check-rn-surface.mjs` does is not an
|
|
138
|
+
// option here: `files` ships `lib` and not `src`, so a consumer's `node_modules` has
|
|
139
|
+
// no `support-table.ts` to parse.
|
|
140
|
+
//
|
|
141
|
+
// AND IT IS NOT A DEPENDENCY, deliberately. `@gjsify/rolldown-plugin-gjsify` is tier 1
|
|
142
|
+
// and `@gjsify/react-native` is tier 3; a tier-1 package may not depend on a higher
|
|
143
|
+
// tier (`scripts/audit-runtimes.mjs`).
|
|
144
|
+
/** The subpath that carries the registry and the table in an installed layer. */
|
|
145
|
+
export const SUPPORT_TABLE_SUBPATH = `${REACT_NATIVE_ALIAS_TARGET}/support-table`;
|
|
146
|
+
/** Thrown when a plugin is on but the layer it must read is not reachable. */
|
|
147
|
+
export class SupportTableUnreadableError extends Error {
|
|
148
|
+
name = 'SupportTableUnreadableError';
|
|
149
|
+
constructor(detail, cause) {
|
|
150
|
+
super(`gjsify react-native gate: cannot read ${SUPPORT_TABLE_SUBPATH} — ${detail}. The gate has no ` +
|
|
151
|
+
`second source to fall back on, and ADR 0032 § 8 is explicit that a hand-maintained table ` +
|
|
152
|
+
`beside it would be the second truth this repository has already collected several times. ` +
|
|
153
|
+
`Install ${REACT_NATIVE_ALIAS_TARGET} (and build it, if it is a workspace link), or drop ` +
|
|
154
|
+
`the react-native opt-in.`, cause === undefined ? undefined : { cause });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve and import the layer's support-table subpath.
|
|
159
|
+
*
|
|
160
|
+
* `importer` is a module from the project being built, so the resolve lands in the
|
|
161
|
+
* PROJECT's dependency tree rather than the CLI's.
|
|
162
|
+
*/
|
|
163
|
+
export async function loadLayer(resolver, importer, load = (href) => import(/* @vite-ignore */ href)) {
|
|
164
|
+
let resolved;
|
|
165
|
+
try {
|
|
166
|
+
resolved = await resolver.resolve(SUPPORT_TABLE_SUBPATH, importer);
|
|
167
|
+
}
|
|
168
|
+
catch (cause) {
|
|
169
|
+
throw new SupportTableUnreadableError(`the resolver threw for ${importer}`, cause);
|
|
170
|
+
}
|
|
171
|
+
if (!resolved)
|
|
172
|
+
throw new SupportTableUnreadableError(`it does not resolve from ${importer}`);
|
|
173
|
+
if (resolved.external) {
|
|
174
|
+
throw new SupportTableUnreadableError(`it resolved as EXTERNAL from ${importer}, so there is no file to read`);
|
|
175
|
+
}
|
|
176
|
+
let mod;
|
|
177
|
+
try {
|
|
178
|
+
// A file path, not the bare specifier: GJS's ESM loader does not follow
|
|
179
|
+
// `package.json#exports`, so the bare form works on Node and fails under the
|
|
180
|
+
// GJS engine — the same limitation the CLI's by-name plugin loader documents.
|
|
181
|
+
// The resolver already did the exports-map hop.
|
|
182
|
+
mod = await load(pathToFileURL(resolved.id).href);
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
throw new SupportTableUnreadableError(`importing ${resolved.id} failed`, cause);
|
|
186
|
+
}
|
|
187
|
+
const candidate = mod;
|
|
188
|
+
if (typeof candidate.isImportable !== 'function' ||
|
|
189
|
+
typeof candidate.explainUnsupported !== 'function' ||
|
|
190
|
+
!Array.isArray(candidate.SURFACES)) {
|
|
191
|
+
throw new SupportTableUnreadableError(`${resolved.id} does not export SURFACES + isImportable + explainUnsupported. ` +
|
|
192
|
+
`That is a version skew between the bundler plugin and the layer, not a missing install`);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
SURFACES: candidate.SURFACES,
|
|
196
|
+
isImportable: candidate.isImportable,
|
|
197
|
+
explainUnsupported: candidate.explainUnsupported,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
93
200
|
/**
|
|
94
|
-
*
|
|
201
|
+
* The one row that is known WITHOUT reading the layer.
|
|
95
202
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
203
|
+
* Used when the layer cannot be read at all, and that is a deliberate degradation
|
|
204
|
+
* rather than a default. `gjsify build --dialect react-native` is an opt-in, so a
|
|
205
|
+
* consumer who asked for it and has no layer installed meant `react-native` — the one
|
|
206
|
+
* specifier whose name is a fact rather than a lookup — and that import gets the
|
|
207
|
+
* named "target does not resolve" error it always got. Every other surface then
|
|
208
|
+
* simply fails to resolve the way it did before ADR 0036, which is no worse than
|
|
209
|
+
* before and much better than every `resolveId` in the build throwing.
|
|
100
210
|
*/
|
|
101
|
-
export
|
|
211
|
+
export const FALLBACK_SURFACES = [
|
|
212
|
+
{ module: REACT_NATIVE_SPECIFIER, target: REACT_NATIVE_ALIAS_TARGET },
|
|
213
|
+
];
|
|
214
|
+
export function reactNativeAliasPlugin(options = {}) {
|
|
215
|
+
let surfaces = options.layer?.SURFACES ?? null;
|
|
216
|
+
let unreadable = false;
|
|
217
|
+
/**
|
|
218
|
+
* The registry, read once.
|
|
219
|
+
*
|
|
220
|
+
* A FAILURE IS CACHED TOO. Without that, a project without the layer installed
|
|
221
|
+
* would retry the resolve for every specifier in the build and throw from every
|
|
222
|
+
* one — including `node:fs`, which has nothing to do with this plugin.
|
|
223
|
+
* {@link FALLBACK_SURFACES} says what the degradation is and why it is the honest
|
|
224
|
+
* one.
|
|
225
|
+
*/
|
|
226
|
+
const registry = async (context, importer) => {
|
|
227
|
+
if (surfaces !== null)
|
|
228
|
+
return surfaces;
|
|
229
|
+
if (unreadable)
|
|
230
|
+
return FALLBACK_SURFACES;
|
|
231
|
+
try {
|
|
232
|
+
surfaces = (await loadLayer(context, importer)).SURFACES;
|
|
233
|
+
return surfaces;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
unreadable = true;
|
|
237
|
+
return FALLBACK_SURFACES;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
102
240
|
return {
|
|
103
241
|
name: 'gjsify-react-native-alias',
|
|
104
242
|
resolveId: {
|
|
105
243
|
order: 'pre',
|
|
106
244
|
async handler(source, importer, extraOptions) {
|
|
107
|
-
|
|
108
|
-
|
|
245
|
+
// A CHEAP PREFILTER BEFORE THE REGISTRY, because this hook runs for
|
|
246
|
+
// every specifier in the build and the registry needs a resolve.
|
|
247
|
+
// {@link SURFACE_NAME_PREFIXES} says why the list is a prefilter and
|
|
248
|
+
// what holds it against the layer's own rows.
|
|
249
|
+
if (!couldBeSurfaceSpecifier(source))
|
|
109
250
|
return null;
|
|
110
251
|
// An ENTRY has no importer to name; report the specifier itself
|
|
111
252
|
// rather than the string "undefined".
|
|
112
253
|
const from = importer ?? source;
|
|
113
|
-
|
|
114
|
-
|
|
254
|
+
const classified = classifyReactNativeSpecifier(source, await registry(this, importer ?? source));
|
|
255
|
+
if (classified.kind === 'other')
|
|
256
|
+
return null;
|
|
257
|
+
if (classified.kind === 'subpath') {
|
|
258
|
+
throw new ReactNativeDeepImportError(source, from, classified.module, classified.target);
|
|
259
|
+
}
|
|
115
260
|
// `skipSelf: true` keeps the nested resolve out of this hook. The
|
|
116
|
-
// target is a plain package name, so the normal chain
|
|
117
|
-
// link, node_modules, the exports map) answers it.
|
|
118
|
-
const resolved = await this.resolve(
|
|
261
|
+
// target is a plain package name plus a subpath, so the normal chain
|
|
262
|
+
// (workspace link, node_modules, the exports map) answers it.
|
|
263
|
+
const resolved = await this.resolve(classified.target, importer, {
|
|
119
264
|
skipSelf: true,
|
|
120
265
|
...(extraOptions?.kind ? { kind: extraOptions.kind } : {}),
|
|
121
266
|
});
|
|
122
|
-
if (!resolved || resolved.external)
|
|
123
|
-
throw new ReactNativeAliasTargetMissingError(from);
|
|
267
|
+
if (!resolved || resolved.external) {
|
|
268
|
+
throw new ReactNativeAliasTargetMissingError(source, classified.target, from);
|
|
269
|
+
}
|
|
124
270
|
return resolved;
|
|
125
271
|
},
|
|
126
272
|
},
|
|
127
273
|
};
|
|
128
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* The prefixes every declared surface's npm name starts with.
|
|
277
|
+
*
|
|
278
|
+
* A PREFILTER AND NOT A DECISION: the registry decides, and these exist only so the
|
|
279
|
+
* two hot paths — one `resolveId` per specifier in the build, one `transform` per
|
|
280
|
+
* module — can say "certainly not a surface" without resolving or parsing anything.
|
|
281
|
+
* `react-native-alias.spec.ts` asserts every row of the LAYER's own registry starts
|
|
282
|
+
* with one of them, so a surface named outside this list fails a test rather than
|
|
283
|
+
* being silently skipped by the alias and the gate together.
|
|
284
|
+
*
|
|
285
|
+
* `@gjsify/react-native` is here because a gjsify-native application writes the target
|
|
286
|
+
* directly, and the gate watches both spellings of every surface.
|
|
287
|
+
*/
|
|
288
|
+
export const SURFACE_NAME_PREFIXES = [
|
|
289
|
+
'react-native',
|
|
290
|
+
'expo-',
|
|
291
|
+
'@expo/',
|
|
292
|
+
'@react-native-',
|
|
293
|
+
'nativewind',
|
|
294
|
+
REACT_NATIVE_ALIAS_TARGET,
|
|
295
|
+
];
|
|
296
|
+
/** Could this specifier be a declared surface? Cheap, and deliberately over-inclusive. */
|
|
297
|
+
export const couldBeSurfaceSpecifier = (specifier) => SURFACE_NAME_PREFIXES.some((prefix) => specifier.startsWith(prefix));
|
|
298
|
+
/**
|
|
299
|
+
* Does this SOURCE mention a specifier that could be a surface?
|
|
300
|
+
*
|
|
301
|
+
* Anchored on the opening QUOTE, which is the whole point: a bare `expo-` test over
|
|
302
|
+
* source text would match the word `export`, so the fragment has to sit where a module
|
|
303
|
+
* specifier sits. Over-inclusive by design (a string literal in a comment matches);
|
|
304
|
+
* under-inclusive never, which is the direction that matters for a gate.
|
|
305
|
+
*/
|
|
306
|
+
export const SURFACE_MENTION = new RegExp(`['"\`](?:${SURFACE_NAME_PREFIXES.map((prefix) => prefix.replace(/[/@.]/g, '\\$&')).join('|')})`);
|
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
import type { Plugin } from 'rolldown';
|
|
2
|
+
import { type AliasedSurface, type LayerReader } from './react-native-alias.js';
|
|
2
3
|
import { ImportScanParseError, type NamedImport, type OpaqueReference } from '../utils/scan-named-imports.js';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* What the gate needs off the layer, and it is now three things rather than two.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* `SURFACES` is the addition ADR 0036 makes: the gate has to know WHICH specifiers to
|
|
8
|
+
* watch, and that list is the layer's rather than the plugin's. A structural type, so
|
|
9
|
+
* a spec can pass a three-row fixture and so the plugin never types itself against a
|
|
10
|
+
* tier-3 package it must not import.
|
|
8
11
|
*/
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
export type SupportTableReader = LayerReader;
|
|
13
|
+
export { SUPPORT_TABLE_SUBPATH, SupportTableUnreadableError } from './react-native-alias.js';
|
|
14
|
+
export type { SupportTableResolver } from './react-native-alias.js';
|
|
15
|
+
/**
|
|
16
|
+
* `loadLayer` under its historical name.
|
|
17
|
+
*
|
|
18
|
+
* Kept because it is a published export of `@gjsify/rolldown-plugin-gjsify`, and
|
|
19
|
+
* renaming a published function to say "surfaces" as well as "table" would be a
|
|
20
|
+
* breaking change for nothing — the loader reads one module either way.
|
|
21
|
+
*/
|
|
22
|
+
export { loadLayer as loadSupportTable } from './react-native-alias.js';
|
|
23
|
+
/**
|
|
24
|
+
* Every specifier the gate watches, from the layer's own registry.
|
|
25
|
+
*
|
|
26
|
+
* BOTH SPELLINGS OF EACH SURFACE. A ported application writes `expo-status-bar` (the
|
|
27
|
+
* alias rewrites it, and the rewrite happens in `resolveId` — the SOURCE text still
|
|
28
|
+
* says `expo-status-bar` when this hook reads it). A gjsify-native application, this
|
|
29
|
+
* repository's own showcase included, writes `@gjsify/react-native/expo-status-bar`.
|
|
30
|
+
* The gate is about the SURFACE, not about which name reached it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const watchedSpecifiers: (surfaces: readonly AliasedSurface[]) => readonly string[];
|
|
26
33
|
/**
|
|
27
|
-
*
|
|
34
|
+
* The two specifiers that are known WITHOUT reading the layer.
|
|
28
35
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* when this hook reads it). A gjsify-native application, this repository's own
|
|
32
|
-
* showcase included, writes `@gjsify/react-native`. The gate is about the
|
|
33
|
-
* SURFACE, not about which name reached it.
|
|
36
|
+
* Kept as a published constant because it was one, and it is now the FALLBACK rather
|
|
37
|
+
* than the whole list: `watchedSpecifiers(layer.SURFACES)` is what a real build uses.
|
|
34
38
|
*/
|
|
35
39
|
export declare const WATCHED_SPECIFIERS: readonly string[];
|
|
36
40
|
/** One refused import, with everything the message needs. */
|
|
@@ -49,11 +53,6 @@ export declare class ReactNativeUnsupportedImportError extends Error {
|
|
|
49
53
|
readonly name = "ReactNativeUnsupportedImportError";
|
|
50
54
|
constructor(id: string, violations: readonly SupportViolation[]);
|
|
51
55
|
}
|
|
52
|
-
/** Thrown when the gate is on but the table it must read is not reachable. */
|
|
53
|
-
export declare class SupportTableUnreadableError extends Error {
|
|
54
|
-
readonly name = "SupportTableUnreadableError";
|
|
55
|
-
constructor(detail: string, cause?: unknown);
|
|
56
|
-
}
|
|
57
56
|
/**
|
|
58
57
|
* All violations for one module as one message.
|
|
59
58
|
*
|
|
@@ -81,21 +80,6 @@ export declare function formatOpaqueReference(id: string, ref: OpaqueReference):
|
|
|
81
80
|
* loud, per file, rather than degrade quietly.
|
|
82
81
|
*/
|
|
83
82
|
export declare function formatUnreadableModule(error: ImportScanParseError): string;
|
|
84
|
-
/** The resolver slice this plugin needs, so `loadSupportTable` is testable. */
|
|
85
|
-
export interface SupportTableResolver {
|
|
86
|
-
resolve(specifier: string, importer: string): Promise<{
|
|
87
|
-
id: string;
|
|
88
|
-
external?: boolean | string;
|
|
89
|
-
} | null>;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Resolve and import the support table.
|
|
93
|
-
*
|
|
94
|
-
* `importer` is a module from the project being built, so the resolve lands in
|
|
95
|
-
* the PROJECT's dependency tree rather than the CLI's — the gate must describe
|
|
96
|
-
* the layer this bundle will actually contain.
|
|
97
|
-
*/
|
|
98
|
-
export declare function loadSupportTable(resolver: SupportTableResolver, importer: string, load?: (href: string) => Promise<unknown>): Promise<SupportTableReader>;
|
|
99
83
|
export interface ReactNativeSupportGateOptions {
|
|
100
84
|
/**
|
|
101
85
|
* The table, when the caller already has it. Left unset in a real build,
|
|
@@ -32,22 +32,35 @@
|
|
|
32
32
|
// gate then states exactly what the build's own copy of the layer supports, so a
|
|
33
33
|
// version skew between plugin and layer cannot make the gate lie in either
|
|
34
34
|
// direction.
|
|
35
|
-
import {
|
|
36
|
-
import { REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER } from './react-native-alias.js';
|
|
35
|
+
import { FALLBACK_SURFACES, SURFACE_MENTION, loadLayer, } from './react-native-alias.js';
|
|
37
36
|
import { REWRITE_FILTER } from './rewrite-node-modules-paths.js';
|
|
38
37
|
import { ImportScanParseError, scanNamedImports, } from '../utils/scan-named-imports.js';
|
|
39
|
-
|
|
40
|
-
export const SUPPORT_TABLE_SUBPATH = `${REACT_NATIVE_ALIAS_TARGET}/support-table`;
|
|
38
|
+
export { SUPPORT_TABLE_SUBPATH, SupportTableUnreadableError } from './react-native-alias.js';
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
40
|
+
* `loadLayer` under its historical name.
|
|
43
41
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* showcase included, writes `@gjsify/react-native`. The gate is about the
|
|
48
|
-
* SURFACE, not about which name reached it.
|
|
42
|
+
* Kept because it is a published export of `@gjsify/rolldown-plugin-gjsify`, and
|
|
43
|
+
* renaming a published function to say "surfaces" as well as "table" would be a
|
|
44
|
+
* breaking change for nothing — the loader reads one module either way.
|
|
49
45
|
*/
|
|
50
|
-
export
|
|
46
|
+
export { loadLayer as loadSupportTable } from './react-native-alias.js';
|
|
47
|
+
/**
|
|
48
|
+
* Every specifier the gate watches, from the layer's own registry.
|
|
49
|
+
*
|
|
50
|
+
* BOTH SPELLINGS OF EACH SURFACE. A ported application writes `expo-status-bar` (the
|
|
51
|
+
* alias rewrites it, and the rewrite happens in `resolveId` — the SOURCE text still
|
|
52
|
+
* says `expo-status-bar` when this hook reads it). A gjsify-native application, this
|
|
53
|
+
* repository's own showcase included, writes `@gjsify/react-native/expo-status-bar`.
|
|
54
|
+
* The gate is about the SURFACE, not about which name reached it.
|
|
55
|
+
*/
|
|
56
|
+
export const watchedSpecifiers = (surfaces) => surfaces.flatMap((surface) => [surface.module, surface.target]);
|
|
57
|
+
/**
|
|
58
|
+
* The two specifiers that are known WITHOUT reading the layer.
|
|
59
|
+
*
|
|
60
|
+
* Kept as a published constant because it was one, and it is now the FALLBACK rather
|
|
61
|
+
* than the whole list: `watchedSpecifiers(layer.SURFACES)` is what a real build uses.
|
|
62
|
+
*/
|
|
63
|
+
export const WATCHED_SPECIFIERS = watchedSpecifiers(FALLBACK_SURFACES);
|
|
51
64
|
/** Thrown for every refused import in one module, in one message. */
|
|
52
65
|
export class ReactNativeUnsupportedImportError extends Error {
|
|
53
66
|
id;
|
|
@@ -59,17 +72,6 @@ export class ReactNativeUnsupportedImportError extends Error {
|
|
|
59
72
|
this.violations = violations;
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
|
-
/** Thrown when the gate is on but the table it must read is not reachable. */
|
|
63
|
-
export class SupportTableUnreadableError extends Error {
|
|
64
|
-
name = 'SupportTableUnreadableError';
|
|
65
|
-
constructor(detail, cause) {
|
|
66
|
-
super(`gjsify react-native gate: cannot read ${SUPPORT_TABLE_SUBPATH} — ${detail}. The gate has no ` +
|
|
67
|
-
`second source to fall back on, and ADR 0032 § 8 is explicit that a hand-maintained table ` +
|
|
68
|
-
`beside it would be the second truth this repository has already collected several times. ` +
|
|
69
|
-
`Install ${REACT_NATIVE_ALIAS_TARGET} (and build it, if it is a workspace link), or drop ` +
|
|
70
|
-
`the react-native opt-in.`, cause === undefined ? undefined : { cause });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
75
|
/**
|
|
74
76
|
* All violations for one module as one message.
|
|
75
77
|
*
|
|
@@ -79,7 +81,7 @@ export class SupportTableUnreadableError extends Error {
|
|
|
79
81
|
* run.
|
|
80
82
|
*/
|
|
81
83
|
export function formatSupportViolations(id, violations) {
|
|
82
|
-
const lines = violations.map((v) => ` ${id}:${v.line}:${v.column} ${v.name}\n ${v.reason}`);
|
|
84
|
+
const lines = violations.map((v) => ` ${id}:${v.line}:${v.column} ${v.name} (from "${v.specifier}")\n ${v.reason}`);
|
|
83
85
|
return (`gjsify react-native gate: ${violations.length} import(s) this layer does not answer for:\n` +
|
|
84
86
|
`${lines.join('\n')}\n` +
|
|
85
87
|
`Each sentence above is the support table's own (ADR 0032 § 8), so it is the same reason the ` +
|
|
@@ -90,7 +92,13 @@ export function formatSupportViolations(id, violations) {
|
|
|
90
92
|
export function findSupportViolations(named, table) {
|
|
91
93
|
const out = [];
|
|
92
94
|
for (const entry of named) {
|
|
93
|
-
|
|
95
|
+
// THE SPECIFIER IS PASSED, and after ADR 0036 it is load-bearing rather than
|
|
96
|
+
// extra context: `StatusBar` is a `react-native` export AND the whole of
|
|
97
|
+
// `expo-status-bar`, `Image` is `react-native`'s and `expo-image`'s. Without
|
|
98
|
+
// the module the lookup answers from whichever surface the registry lists
|
|
99
|
+
// first, so `import { Image } from 'expo-image'` would report react-native's
|
|
100
|
+
// "is available" and the build would go green on a name nothing implements.
|
|
101
|
+
if (table.isImportable(entry.name, entry.specifier))
|
|
94
102
|
continue;
|
|
95
103
|
out.push({
|
|
96
104
|
name: entry.name,
|
|
@@ -100,7 +108,7 @@ export function findSupportViolations(named, table) {
|
|
|
100
108
|
// The table's sentence, never a rephrasing of it: `explainUnsupported`
|
|
101
109
|
// exists so the build error and the runtime throw cannot describe the
|
|
102
110
|
// same gap differently.
|
|
103
|
-
reason: table.explainUnsupported(entry.name),
|
|
111
|
+
reason: table.explainUnsupported(entry.name, entry.specifier),
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
return out;
|
|
@@ -137,44 +145,6 @@ export function formatUnreadableModule(error) {
|
|
|
137
145
|
return (`${error.message} The build continues: the runtime refusal reads the same table (ADR 0032 § 8) and ` +
|
|
138
146
|
`still covers this module, but it fires when the code path runs rather than now.`);
|
|
139
147
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Resolve and import the support table.
|
|
142
|
-
*
|
|
143
|
-
* `importer` is a module from the project being built, so the resolve lands in
|
|
144
|
-
* the PROJECT's dependency tree rather than the CLI's — the gate must describe
|
|
145
|
-
* the layer this bundle will actually contain.
|
|
146
|
-
*/
|
|
147
|
-
export async function loadSupportTable(resolver, importer, load = (href) => import(/* @vite-ignore */ href)) {
|
|
148
|
-
let resolved;
|
|
149
|
-
try {
|
|
150
|
-
resolved = await resolver.resolve(SUPPORT_TABLE_SUBPATH, importer);
|
|
151
|
-
}
|
|
152
|
-
catch (cause) {
|
|
153
|
-
throw new SupportTableUnreadableError(`the resolver threw for ${importer}`, cause);
|
|
154
|
-
}
|
|
155
|
-
if (!resolved)
|
|
156
|
-
throw new SupportTableUnreadableError(`it does not resolve from ${importer}`);
|
|
157
|
-
if (resolved.external) {
|
|
158
|
-
throw new SupportTableUnreadableError(`it resolved as EXTERNAL from ${importer}, so there is no file to read`);
|
|
159
|
-
}
|
|
160
|
-
let mod;
|
|
161
|
-
try {
|
|
162
|
-
// A file path, not the bare specifier: GJS's ESM loader does not follow
|
|
163
|
-
// `package.json#exports`, so the bare form works on Node and fails under
|
|
164
|
-
// the GJS engine — the same limitation the CLI's by-name plugin loader
|
|
165
|
-
// documents. The resolver already did the exports-map hop.
|
|
166
|
-
mod = await load(pathToFileURL(resolved.id).href);
|
|
167
|
-
}
|
|
168
|
-
catch (cause) {
|
|
169
|
-
throw new SupportTableUnreadableError(`importing ${resolved.id} failed`, cause);
|
|
170
|
-
}
|
|
171
|
-
const candidate = mod;
|
|
172
|
-
if (typeof candidate.isImportable !== 'function' || typeof candidate.explainUnsupported !== 'function') {
|
|
173
|
-
throw new SupportTableUnreadableError(`${resolved.id} does not export isImportable + explainUnsupported. ` +
|
|
174
|
-
`That is a version skew between the bundler plugin and the layer, not a missing install`);
|
|
175
|
-
}
|
|
176
|
-
return { isImportable: candidate.isImportable, explainUnsupported: candidate.explainUnsupported };
|
|
177
|
-
}
|
|
178
148
|
/**
|
|
179
149
|
* Fail the build on an import of a React Native name whose status is not
|
|
180
150
|
* `supported` or `partial`.
|
|
@@ -201,20 +171,27 @@ export function reactNativeSupportGatePlugin(options = {}) {
|
|
|
201
171
|
// a data URL, all of which acorn would reject as invalid JS.
|
|
202
172
|
if (!REWRITE_FILTER.test(id))
|
|
203
173
|
return null;
|
|
204
|
-
// Text prefilter before the parse
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
174
|
+
// Text prefilter before the parse AND before the layer is read.
|
|
175
|
+
// ADR 0036 turned two watched specifiers into eighteen surfaces with
|
|
176
|
+
// two spellings each, and the list of those is the LAYER's — so the
|
|
177
|
+
// gate cannot know what to scan for until it has read it, and reading
|
|
178
|
+
// it on every module would make the one module that mentions a surface
|
|
179
|
+
// in a comment pay for the whole build. `SURFACE_MENTION` is anchored
|
|
180
|
+
// on the opening quote of a module specifier, which is why a bare
|
|
181
|
+
// `expo-` test is not enough: the word `export` contains it.
|
|
182
|
+
if (!SURFACE_MENTION.test(code))
|
|
209
183
|
return null;
|
|
210
184
|
// The ONE catch in this file, with a real throw path and a
|
|
211
185
|
// stated reason: `scanNamedImports` throws on syntax the pinned
|
|
212
186
|
// acorn-typescript cannot read, and the policy for that lives
|
|
213
187
|
// here rather than in the pure function — see
|
|
214
188
|
// {@link formatUnreadableModule}.
|
|
189
|
+
// The layer FIRST, because its registry is what says which specifiers
|
|
190
|
+
// to scan for. The load is once per build and cached on the closure.
|
|
191
|
+
table ??= await loadLayer(this, id);
|
|
215
192
|
let scanned;
|
|
216
193
|
try {
|
|
217
|
-
scanned = scanNamedImports(code, id,
|
|
194
|
+
scanned = scanNamedImports(code, id, watchedSpecifiers(table.SURFACES));
|
|
218
195
|
}
|
|
219
196
|
catch (error) {
|
|
220
197
|
if (!(error instanceof ImportScanParseError))
|
|
@@ -225,7 +202,6 @@ export function reactNativeSupportGatePlugin(options = {}) {
|
|
|
225
202
|
const { named, opaque } = scanned;
|
|
226
203
|
if (named.length === 0 && opaque.length === 0)
|
|
227
204
|
return null;
|
|
228
|
-
table ??= await loadSupportTable(this, id);
|
|
229
205
|
const violations = findSupportViolations(named, table);
|
|
230
206
|
if (violations.length > 0)
|
|
231
207
|
throw new ReactNativeUnsupportedImportError(id, violations);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
],
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@gjsify/console": "^0.
|
|
64
|
-
"@gjsify/resolve-npm": "^0.
|
|
65
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
66
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
67
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
63
|
+
"@gjsify/console": "^0.47.0",
|
|
64
|
+
"@gjsify/resolve-npm": "^0.47.0",
|
|
65
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.47.0",
|
|
66
|
+
"@gjsify/rolldown-plugin-pnp": "^0.47.0",
|
|
67
|
+
"@gjsify/vite-plugin-blueprint": "^0.47.0",
|
|
68
68
|
"@rollup/pluginutils": "^5.4.0",
|
|
69
69
|
"acorn": "^8.17.0",
|
|
70
70
|
"acorn-typescript": "^1.4.13",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"sass": "^1.101.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@gjsify/lightningcss-native": "^0.
|
|
77
|
+
"@gjsify/lightningcss-native": "^0.47.0",
|
|
78
78
|
"rolldown": "^1.1.4"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@gjsify/cli": "^0.
|
|
90
|
-
"@gjsify/unit": "^0.
|
|
89
|
+
"@gjsify/cli": "^0.47.0",
|
|
90
|
+
"@gjsify/unit": "^0.47.0",
|
|
91
91
|
"@types/node": "^25.9.2",
|
|
92
92
|
"rolldown": "^1.1.4",
|
|
93
93
|
"typescript": "^6.0.3"
|