Skip to content

Commit bf0ba5f

Browse files
committed
update
1 parent fc089a7 commit bf0ba5f

2 files changed

Lines changed: 118 additions & 8 deletions

File tree

‎lib/import.react_native/README.md‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Each file in this directory loads one optional React Native dependency (a peer dependency the app may not have installed) and tells the rest of the SDK whether it is available.
44

5-
The shims are not bundled into the main SDK bundle. Rollup builds each one to the same path under `dist/import.react_native/`, and the main React Native bundles `require` it by relative path (see `rollup.config.mjs`). The rest of this section explains why.
5+
The shims are not bundled into the main SDK bundle. Rolldown builds each one to the same path under `dist/import.react_native/`, and the main React Native bundles `require` it by relative path (see `rolldown.config.mjs`). The rest of this section explains why.
66

77
## Why the shims are separate modules
88

@@ -78,18 +78,17 @@ A test goes next to its shim as `<name>.spec.ts`. Spec files are not built as sh
7878
- **The package's exports, under the same names:** if the package isn't installed they are `undefined`. A shim may adapt a value as long as it keeps the package's shape. For example, the async-storage shim adds `multiGet` on AsyncStorage v3.
7979
- **Only the exports the SDK uses:** this isn't required for correctness. Re-exporting more would still be one `require`. It just keeps the shims small.
8080
- **Packages loaded only for side effects:** polyfills like `fast-text-encoding` only need `isAvailable`.
81-
- **A shim can use `export default` when the package has a default export,** as the async-storage shim does. This works because of a setting in `rollup.config.mjs`:
81+
- **A shim can use `export default` when the package has a default export,** as the async-storage shim does:
8282
- Shims are built as CommonJS. A default export becomes `exports.default`, and the file is marked with `__esModule`.
83-
- In the main CJS bundles, Rollup by default treats a default import from an external CommonJS module as the whole `module.exports`. `import AsyncStorage from '…/async-storage'` would then get the shim's exports object instead of the storage.
84-
- `rollup.config.mjs` therefore sets `interop: 'esModule'` for shim imports, so the CJS bundles read `exports.default`.
85-
- Shims are only built as CommonJS; the ESM main bundle imports the same CommonJS files. It doesn't need the setting: the app's bundler (Metro, or webpack) reads `exports.default` for a default import from a CommonJS module marked with `__esModule`.
83+
- In the main CJS bundles, Rolldown wraps a `require` of an external CommonJS module in its `__toESM` helper, which reads `exports.default` for a default import when the module is marked with `__esModule`. `import AsyncStorage from '…/async-storage'` therefore gets the storage, not the shim's exports object. (Rollup needed `interop: 'esModule'` for this; Rolldown needs no setting.)
84+
- Shims are only built as CommonJS; the ESM main bundle imports the same CommonJS files. The app's bundler (Metro, or webpack) reads `exports.default` for a default import from a CommonJS module marked with `__esModule`.
8685
- **Never throw.** A shim catches the `require` error, sets `isAvailable = false`, and leaves its exports `undefined`. The importer decides what a missing package means. For example, `index.react_native.ts` throws if `TextEncoder` is still missing after the polyfill shims run, and `AsyncStorageCache` throws "Module not found" when there's no AsyncStorage.
8786

8887
### Exactly one `require` in the built file
8988

9089
The optional `require` must be the only `require` in the built shim (`dist/import.react_native/<name>.js`). Any other `require` in the module could shift when the optional package is missing.
9190

92-
**The build enforces this.** The `rn-import-shim-check` plugin in `rollup.config.mjs` fails the build if a built shim contains any `require` other than its own package's. The package is taken from the shim's name, so the check also enforces the naming convention.
91+
**The build enforces this.** The `rn-import-shim-check` plugin in `rolldown.config.mjs` fails the build if a built shim contains any `require` other than its own package's. The package is taken from the shim's name, so the check also enforces the naming convention.
9392

9493
- **Wrap the `require` directly in `try/catch`.** Metro only treats a `require` as optional when it is directly inside a `try` block. Without that, a missing package fails the app's build.
9594
- **Run it at module load, not later in a function.** Only then is a failed `require` thrown to the shim's `catch`. Later, Metro before 0.82.5 reports it as a fatal error instead (see "The bug in Metro before 0.82.5").
@@ -122,4 +121,4 @@ try {
122121
export const __platforms: Platform[] = ['react_native'];
123122
```
124123

125-
Importers use a shim through a normal relative import, for example `import { isAvailable, someExport } from './import.react_native/some-package'`. The Rollup plugin turns that into the relative `require` of the built shim.
124+
Importers use a shim through a normal relative import, for example `import { isAvailable, someExport } from './import.react_native/some-package'`. The `rn-import-shim-external` plugin in `rolldown.config.mjs` turns that into the relative `require` of the built shim.

‎rolldown.config.mjs‎

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
import { createRequire } from 'module';
1818
import { fileURLToPath } from 'url';
19-
import { dirname, join } from 'path';
19+
import { readdirSync } from 'fs';
20+
import { dirname, join, relative, resolve } from 'path';
2021

2122
const __filename = fileURLToPath(import.meta.url);
2223
const __dirname = dirname(__filename);
@@ -36,6 +37,111 @@ const externalDeps = ['https', 'http', 'url'].concat(Object.keys({ ...dependenci
3637
// into the CJS output instead of leaving it as an external dependency.
3738
const externalDepsWithoutUuid = externalDeps.filter(dep => dep !== 'uuid');
3839

40+
// React Native optional import shims (lib/import.react_native) are not bundled into the main bundles.
41+
// Each is shipped as its own module at the same relative location in dist, because Metro before 0.82.5
42+
// shifts every later require in a module when an optional dependency is missing.
43+
// See lib/import.react_native/README.md.
44+
const BUILD_DIR = join(__dirname, '.build');
45+
const RN_IMPORT_SHIM_DIR = 'import.react_native';
46+
47+
// Shim paths relative to the shim directory, without extension. Shims are named after their package, so
48+
// a scoped package's shim is in a subdirectory, e.g. '@react-native-async-storage/async-storage'.
49+
const listShims = (subdir = '') =>
50+
readdirSync(join(__dirname, 'lib', RN_IMPORT_SHIM_DIR, subdir), { withFileTypes: true }).flatMap(entry => {
51+
const path = subdir ? `${subdir}/${entry.name}` : entry.name;
52+
if (entry.isDirectory()) return listShims(path);
53+
return entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts') ? [path.replace(/\.ts$/, '')] : [];
54+
});
55+
const RN_IMPORT_SHIMS = listShims();
56+
57+
// Leaves shim imports out of the main bundles and writes them as a path relative to the bundle, e.g.
58+
// import '../../../import.react_native/@react-native-async-storage/async-storage' (from a file deep in .build)
59+
// -> require("./import.react_native/@react-native-async-storage/async-storage.js") (in dist/index.react_native.min.js)
60+
// The bundles are at the root of dist and the shims are built to dist/import.react_native,
61+
// so that path points to the built shim.
62+
// Marking the imports external via the `external` option is not enough: the bundler would resolve them to
63+
// the file in .build and write a path from the bundle back into .build (../.build/import.react_native/...).
64+
// Returning a relative id from resolveId makes the bundler write it exactly as given.
65+
//
66+
// Shims are built as CommonJS with __esModule set, so a default import of a shim must read its `default`
67+
// export. Rolldown's CJS interop for externals (__toESM) already does that for modules with __esModule set,
68+
// so no per-import interop override is needed (Rollup needed `output.interop: 'esModule'` for these).
69+
const rnImportShimExternalPlugin = {
70+
name: 'rn-import-shim-external',
71+
resolveId(source, importer) {
72+
if (!importer || !source.startsWith('.')) return null;
73+
const pathInBuild = relative(BUILD_DIR, resolve(dirname(importer), source)).replace(/\.js$/, '');
74+
if (!pathInBuild.startsWith(`${RN_IMPORT_SHIM_DIR}/`)) return null;
75+
return { id: `./${pathInBuild}.js`, external: true };
76+
},
77+
};
78+
79+
// Fails the build unless every built shim contains exactly one require(), of its own package (the shim's
80+
// name). Any other require in a shim could be shifted by the Metro bug when the package is missing. This
81+
// catches imports of other packages, tslib helpers, and code shared between shims (the bundler moves it into
82+
// a shared chunk that the shims require). The shim's own require() is plain code, so it isn't listed in
83+
// chunk.imports; the generated code is parsed instead, with the bundler's parser.
84+
const rnImportShimCheckPlugin = {
85+
name: 'rn-import-shim-check',
86+
generateBundle(_, bundle) {
87+
// Module names of every require(...) call in an ESTree AST; a call whose argument is not a string is
88+
// listed as '<non-literal>'.
89+
const findRequireCalls = (node, found = []) => {
90+
if (Array.isArray(node)) {
91+
node.forEach(child => findRequireCalls(child, found));
92+
} else if (node && typeof node.type === 'string') {
93+
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require') {
94+
const [arg] = node.arguments;
95+
found.push(arg && arg.type === 'Literal' && typeof arg.value === 'string' ? arg.value : '<non-literal>');
96+
}
97+
for (const value of Object.values(node)) {
98+
if (value && typeof value === 'object') findRequireCalls(value, found);
99+
}
100+
}
101+
return found;
102+
};
103+
104+
for (const chunk of Object.values(bundle)) {
105+
if (chunk.type !== 'chunk') continue;
106+
const required = findRequireCalls(this.parse(chunk.code));
107+
if (!chunk.isEntry || required.length !== 1 || required[0] !== chunk.name) {
108+
this.error(
109+
`${RN_IMPORT_SHIM_DIR}/${chunk.fileName}: a shim must contain exactly one require(), of its own package` +
110+
` '${chunk.name}' (found: ${required.join(', ') || 'none'}). See lib/${RN_IMPORT_SHIM_DIR}/README.md.`
111+
);
112+
}
113+
}
114+
},
115+
};
116+
117+
const rnImportShimsBundle = {
118+
input: Object.fromEntries(
119+
RN_IMPORT_SHIMS.map(name => [name, join(BUILD_DIR, RN_IMPORT_SHIM_DIR, `${name}.js`)])
120+
),
121+
output: {
122+
exports: 'named',
123+
format: 'cjs',
124+
dir: join('dist', RN_IMPORT_SHIM_DIR),
125+
entryFileNames: '[name].js',
126+
},
127+
plugins: [rnImportShimCheckPlugin],
128+
external: externalDeps,
129+
};
130+
131+
// Rolldown defaults `platform` to 'node' for cjs output, which adds the 'node' export condition and would
132+
// pull Node-only builds of bundled deps (e.g. uuid's dist-node, which requires node:crypto) into
133+
// non-node bundles. Set it explicitly so only the node bundles resolve the 'node' condition.
134+
const rolldownPlatformFor = (platform) => {
135+
switch (platform) {
136+
case 'node':
137+
return 'node';
138+
case 'browser':
139+
return 'browser';
140+
default:
141+
return 'neutral';
142+
}
143+
};
144+
39145
const resolveForPlatform = (platform) => {
40146
const base = { alias: aliasEntries };
41147
switch (platform) {
@@ -71,6 +177,8 @@ const cjsBundleFor = (platform, opt = {}) => {
71177
sourcemap: true,
72178
minify,
73179
},
180+
plugins: [rnImportShimExternalPlugin],
181+
platform: rolldownPlatformFor(platform),
74182
resolve: resolveForPlatform(platform),
75183
external: externalDepsWithoutUuid,
76184
};
@@ -88,6 +196,8 @@ const esmBundleFor = (platform, opt = {}) => {
88196
sourcemap: true,
89197
minify,
90198
},
199+
plugins: [rnImportShimExternalPlugin],
200+
platform: rolldownPlatformFor(platform),
91201
resolve: resolveForPlatform(platform),
92202
external: externalDeps,
93203
};
@@ -174,6 +284,7 @@ const bundles = {
174284
'esm-node-min': esmBundleFor('node', { ext: '.mjs' }),
175285
'esm-react-native-min': esmBundleFor('react_native'),
176286
'esm-universal': esmBundleFor('universal'),
287+
'cjs-react-native-import-shims': rnImportShimsBundle,
177288
'json-schema': jsonSchemaBundle,
178289
'cjs-ua-parser-min': cjsBundleForUAParser(),
179290
'esm-ua-parser-min': esmBundleForUAParser(),

0 commit comments

Comments
 (0)