@uniflowed/react-native 0.0.0-alpha.40
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/index.js +21 -0
- package/metro-transformer.cjs +333 -0
- package/metro.cjs +205 -0
- package/package.json +36 -0
package/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/react-native`: React Native, re-exported.
|
|
4
|
+
//
|
|
5
|
+
// This is the real `react-native` package, not a declaration of it. uf owns the
|
|
6
|
+
// Flow-first compiler, router, lints and target selection around React Native;
|
|
7
|
+
// the host components, Platform object, StyleSheet runtime and native module
|
|
8
|
+
// bridge stay with React Native itself.
|
|
9
|
+
//
|
|
10
|
+
// `export *` rather than a list of names: React Native's public JavaScript
|
|
11
|
+
// surface moves with its release, and a hand-maintained copy here would turn
|
|
12
|
+
// every upstream addition into a uf runtime gap.
|
|
13
|
+
//
|
|
14
|
+
// React Native is a peer dependency, so the application chooses the exact
|
|
15
|
+
// version. uf's own release does not decide when an app moves its native host.
|
|
16
|
+
|
|
17
|
+
export * from "react-native";
|
|
18
|
+
|
|
19
|
+
import * as ReactNative from "react-native";
|
|
20
|
+
|
|
21
|
+
export { ReactNative };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/react-native/metro-transformer.cjs`: the Babel transformer Metro
|
|
4
|
+
// runs for every module in a native bundle.
|
|
5
|
+
//
|
|
6
|
+
// Metro loads `transformer.babelTransformerPath` with `require`, in its own
|
|
7
|
+
// workers, whatever the app's config is written in — so this is CommonJS, and
|
|
8
|
+
// plain JavaScript for the reason `metro.cjs` is: no process that loads it has
|
|
9
|
+
// uf's hooks.
|
|
10
|
+
//
|
|
11
|
+
// For a module uf owns (`isFlowModule`), it runs `uf transform` — Flow's own
|
|
12
|
+
// lowering, the React Compiler, the StyleX policy — and hands the result to the
|
|
13
|
+
// transformer the project's Metro config already had, so Expo's preset or React
|
|
14
|
+
// Native's still does everything it did before: the module system, React
|
|
15
|
+
// Refresh, platform inlining. Anything else goes to that transformer untouched.
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const fs = require("node:fs");
|
|
20
|
+
const os = require("node:os");
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An upstream transformer a person names on purpose. It wins over everything.
|
|
25
|
+
*/
|
|
26
|
+
const UPSTREAM_OVERRIDE_ENV = "UF_METRO_UPSTREAM_TRANSFORMER";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The upstream transformer `withUniflowedMetro()` found in the config it
|
|
30
|
+
* composed.
|
|
31
|
+
*
|
|
32
|
+
* # Why the environment
|
|
33
|
+
*
|
|
34
|
+
* Metro calls a Babel transformer with `{ src, filename, options, plugins }`
|
|
35
|
+
* and nothing from the config it was loaded from — `getBabelTransformArgs` in
|
|
36
|
+
* `metro-transform-worker` builds `options` from the bundle request and five
|
|
37
|
+
* named config values — so there is no key on `transformer` this module could
|
|
38
|
+
* read the upstream from. The environment is the one channel that reaches it:
|
|
39
|
+
* the config is evaluated before Metro starts its workers, and a worker, child
|
|
40
|
+
* process or thread, starts with a copy of the environment it was created from.
|
|
41
|
+
*
|
|
42
|
+
* A name of its own, rather than reusing `UF_METRO_UPSTREAM_TRANSFORMER`,
|
|
43
|
+
* because the two say different things. That one is a person overriding the
|
|
44
|
+
* upstream, and it wins; this one is what the project's config already said,
|
|
45
|
+
* and it is written again every time a config is composed.
|
|
46
|
+
*/
|
|
47
|
+
const CONFIGURED_UPSTREAM_ENV = "UF_METRO_CONFIGURED_UPSTREAM";
|
|
48
|
+
|
|
49
|
+
let host = null;
|
|
50
|
+
let upstream = null;
|
|
51
|
+
|
|
52
|
+
async function transform(args) {
|
|
53
|
+
const { src, filename, options } = args;
|
|
54
|
+
const projectRoot = projectRootOf(options);
|
|
55
|
+
const flow = loadHost();
|
|
56
|
+
const absolute = path.isAbsolute(filename) ? filename : path.join(projectRoot, filename);
|
|
57
|
+
let next = src;
|
|
58
|
+
|
|
59
|
+
if (flow.isFlowModule(absolute)) {
|
|
60
|
+
findUfBinary(flow);
|
|
61
|
+
const out = await flow.transformFlow(src, absolute, {
|
|
62
|
+
root: projectRoot,
|
|
63
|
+
development: options?.dev === true,
|
|
64
|
+
// Off, because the upstream transformer registers components for React
|
|
65
|
+
// Refresh itself — Expo's preset and React Native's both add
|
|
66
|
+
// `react-refresh/babel` to a development bundle — and a component
|
|
67
|
+
// registered twice is two families to the runtime, and a refresh that
|
|
68
|
+
// remounts instead of preserving state.
|
|
69
|
+
refresh: false,
|
|
70
|
+
sourceMap: true,
|
|
71
|
+
});
|
|
72
|
+
if (out != null) {
|
|
73
|
+
if (out.css != null && out.css !== "") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`@uniflowed/react-native/metro: ${filename} produced StyleX CSS, but the ` +
|
|
76
|
+
"native Metro contract cannot attach CSS to a React Native bundle yet. " +
|
|
77
|
+
"Use native style props or keep that StyleX module out of the native route.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
next = out.code;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Everything Metro passed, and not a chosen subset of it. `plugins` carries
|
|
85
|
+
// Metro's `functionMapBabelPlugin` and `importLocationsPlugin`, and
|
|
86
|
+
// forwarding `{ src, filename, options }` alone dropped both for every Flow
|
|
87
|
+
// module in the bundle — function names in stack traces among them.
|
|
88
|
+
return loadUpstream(projectRoot).transformer.transform({
|
|
89
|
+
...args,
|
|
90
|
+
src: next,
|
|
91
|
+
// RN's Babel transformer still gates React Refresh on `hot`, although
|
|
92
|
+
// recent Metro versions omit it. Preserve an explicit caller decision.
|
|
93
|
+
options: { ...options, hot: options?.hot ?? options?.dev === true },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The cache key Metro files every transformed module under.
|
|
99
|
+
*
|
|
100
|
+
* Four identities, because four things decide what a module compiles to: this
|
|
101
|
+
* file, the JavaScript side of `uf transform`, the `uf` binary that does the
|
|
102
|
+
* compiling, and the upstream transformer with its own key. The binary is the
|
|
103
|
+
* one that was missing. A rebuilt or upgraded `uf` changes the output of every
|
|
104
|
+
* Flow module without touching any file this key used to hash, so Metro went on
|
|
105
|
+
* serving the old transforms until somebody thought to pass `--reset-cache` —
|
|
106
|
+
* the defect `@uniflowed/host`'s own cache keys on `ufBinaryIdentity()` to
|
|
107
|
+
* avoid.
|
|
108
|
+
*/
|
|
109
|
+
function getCacheKey(options) {
|
|
110
|
+
const projectRoot = projectRootOf(options);
|
|
111
|
+
const flow = loadHost();
|
|
112
|
+
const binary = findUfBinary(flow);
|
|
113
|
+
const loaded = loadUpstream(projectRoot);
|
|
114
|
+
const parts = [
|
|
115
|
+
"uniflowed-react-native-metro-transformer-v2",
|
|
116
|
+
fileIdentity(__filename),
|
|
117
|
+
fileIdentity(require.resolve("@uniflowed/host/transform")),
|
|
118
|
+
flow.ufBinaryIdentity(binary) ?? binary,
|
|
119
|
+
loaded.path,
|
|
120
|
+
];
|
|
121
|
+
if (typeof loaded.transformer.getCacheKey === "function") {
|
|
122
|
+
parts.push(String(loaded.transformer.getCacheKey(options)));
|
|
123
|
+
}
|
|
124
|
+
return parts.join("\0");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `@uniflowed/host/transform`, loaded synchronously.
|
|
129
|
+
*
|
|
130
|
+
* It is an ES module and this file is not, and `getCacheKey` has to answer
|
|
131
|
+
* synchronously: Metro asks it once, on its main thread, before the first
|
|
132
|
+
* transform. `require` of an ES module is what Node 20.19 and 22.12 added, and
|
|
133
|
+
* on an older Node the error says that rather than failing on a name.
|
|
134
|
+
*/
|
|
135
|
+
function loadHost() {
|
|
136
|
+
if (host != null) return host;
|
|
137
|
+
try {
|
|
138
|
+
host = require("@uniflowed/host/transform");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error?.code === "ERR_REQUIRE_ESM") {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`@uniflowed/react-native/metro: Node ${process.version} cannot require @uniflowed/host, ` +
|
|
143
|
+
"which is an ES module. Run Metro on Node 20.19, 22.12 or later.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return host;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The `uf` that compiles this bundle, found the way its installer arranged and
|
|
153
|
+
* handed to `@uniflowed/host` through `UF_BINARY`.
|
|
154
|
+
*
|
|
155
|
+
* In order: `UF_BINARY`, which `uf dev` sets to itself so a bundle compiles
|
|
156
|
+
* with exactly the `uf` that started it; `uf` on `PATH`; and the installer's
|
|
157
|
+
* own directory, `UF_BIN_DIR` or `~/.local/bin`. The last is there for the
|
|
158
|
+
* processes that bundle an app without a person's shell in front of them —
|
|
159
|
+
* Xcode's "Bundle React Native code and images" phase runs with a minimal
|
|
160
|
+
* `PATH`, and a `uf` the installer put in `~/.local/bin` is invisible to it.
|
|
161
|
+
*
|
|
162
|
+
* Found once and written back to `UF_BINARY`, so that the binary this key
|
|
163
|
+
* identifies and the binary `@uniflowed/host` spawns cannot be two different
|
|
164
|
+
* files.
|
|
165
|
+
*/
|
|
166
|
+
function findUfBinary(flow) {
|
|
167
|
+
const named = process.env.UF_BINARY;
|
|
168
|
+
if (named != null && named !== "") return named;
|
|
169
|
+
if (flow.ufBinaryIdentity("uf") != null) return "uf";
|
|
170
|
+
const directory = process.env.UF_BIN_DIR || path.join(os.homedir(), ".local", "bin");
|
|
171
|
+
const installed = path.join(directory, "uf");
|
|
172
|
+
if (flow.ufBinaryIdentity(installed) != null) {
|
|
173
|
+
process.env.UF_BINARY = installed;
|
|
174
|
+
return installed;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(
|
|
177
|
+
"@uniflowed/react-native/metro: cannot find the `uf` binary that compiles Flow for this bundle. " +
|
|
178
|
+
`UF_BINARY is not set, \`uf\` is not on PATH (${process.env.PATH ?? ""}), ` +
|
|
179
|
+
`and there is no executable at ${installed}. ` +
|
|
180
|
+
"Install uf with `curl -fsSL https://setup.uniflowed.dev | sh`, or set UF_BINARY to its path — " +
|
|
181
|
+
"for Xcode's bundle phase, export it from ios/.xcode.env.local.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The Babel transformer that runs after uf's, loaded once per worker.
|
|
187
|
+
*
|
|
188
|
+
* An upstream named by a person or by the composed config is the only
|
|
189
|
+
* candidate: if it does not load, that is an error, not an invitation to
|
|
190
|
+
* guess. Only a config that never went through `withUniflowedMetro()` falls
|
|
191
|
+
* back to looking for the transformer the project's own Metro config would
|
|
192
|
+
* have named.
|
|
193
|
+
*/
|
|
194
|
+
function loadUpstream(projectRoot) {
|
|
195
|
+
if (upstream != null) return upstream;
|
|
196
|
+
const override = process.env[UPSTREAM_OVERRIDE_ENV];
|
|
197
|
+
const configured = process.env[CONFIGURED_UPSTREAM_ENV];
|
|
198
|
+
let candidates;
|
|
199
|
+
if (override != null && override !== "") {
|
|
200
|
+
candidates = [{ request: override, from: projectRoot, why: UPSTREAM_OVERRIDE_ENV }];
|
|
201
|
+
} else if (configured != null && configured !== "") {
|
|
202
|
+
candidates = [
|
|
203
|
+
{
|
|
204
|
+
request: configured,
|
|
205
|
+
from: projectRoot,
|
|
206
|
+
why: "transformer.babelTransformerPath, composed by withUniflowedMetro()",
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
} else {
|
|
210
|
+
candidates = installedCandidates(projectRoot);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const tried = [];
|
|
214
|
+
for (const candidate of candidates) {
|
|
215
|
+
let resolved;
|
|
216
|
+
try {
|
|
217
|
+
resolved = require.resolve(candidate.request, { paths: [candidate.from] });
|
|
218
|
+
} catch {
|
|
219
|
+
tried.push(`${candidate.request} (${candidate.why}): not found from ${candidate.from}`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const loaded = require(resolved);
|
|
223
|
+
const transformer =
|
|
224
|
+
loaded?.__esModule === true && loaded.default != null ? loaded.default : loaded;
|
|
225
|
+
if (typeof transformer?.transform !== "function") {
|
|
226
|
+
tried.push(`${resolved} (${candidate.why}): does not export transform()`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (transformer.isUniflowedMetroTransformer === true) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`@uniflowed/react-native/metro: ${resolved} is uf's own Metro transformer, named as the ` +
|
|
232
|
+
"transformer to run after uf's. Two copies of @uniflowed/react-native have composed the " +
|
|
233
|
+
"same config; install one.",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
upstream = { transformer, path: resolved };
|
|
237
|
+
return upstream;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new Error(
|
|
241
|
+
"@uniflowed/react-native/metro: there is no React Native Babel transformer to run after uf's. " +
|
|
242
|
+
"Compose the project's Metro config so the transformer it already has keeps running — " +
|
|
243
|
+
"`module.exports = withUniflowedMetro(getDefaultConfig(__dirname))`, with getDefaultConfig " +
|
|
244
|
+
"from expo/metro-config or @react-native/metro-config — or set UF_METRO_UPSTREAM_TRANSFORMER " +
|
|
245
|
+
"to a transformer's path.\n" +
|
|
246
|
+
tried.map((line) => ` ${line}`).join("\n"),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The transformers a project's own Metro config would have named, most likely
|
|
252
|
+
* first.
|
|
253
|
+
*
|
|
254
|
+
* Looked up from the project, not from this package. A package manager is free
|
|
255
|
+
* to nest `@expo/metro-config` inside `expo` — npm does, for the SDK 57
|
|
256
|
+
* template — and a `require` from here sees only what was hoisted, which is why
|
|
257
|
+
* this list could not find Expo's transformer in a freshly created Expo app.
|
|
258
|
+
*
|
|
259
|
+
* Expo's first when the project has Expo, because in an Expo project it is the
|
|
260
|
+
* one `getDefaultConfig()` names and React Native's would bundle without
|
|
261
|
+
* `babel-preset-expo`. `metro-babel-transformer`, which this list used to end
|
|
262
|
+
* with, is gone from it: it applies no React Native preset, so reaching it meant
|
|
263
|
+
* React Native's own Flow source arriving at Babel with nothing to strip it — a
|
|
264
|
+
* failure much further from its cause than this refusal.
|
|
265
|
+
*/
|
|
266
|
+
function installedCandidates(projectRoot) {
|
|
267
|
+
const candidates = [];
|
|
268
|
+
const expo = packageDirectory("expo", projectRoot);
|
|
269
|
+
if (expo != null) {
|
|
270
|
+
candidates.push({
|
|
271
|
+
request: "@expo/metro-config/babel-transformer",
|
|
272
|
+
from: expo,
|
|
273
|
+
why: "Expo's transformer, beside the installed expo",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
candidates.push({
|
|
277
|
+
request: "@react-native/metro-babel-transformer",
|
|
278
|
+
from: projectRoot,
|
|
279
|
+
why: "React Native's transformer",
|
|
280
|
+
});
|
|
281
|
+
const reactNative = packageDirectory("react-native", projectRoot);
|
|
282
|
+
if (reactNative != null) {
|
|
283
|
+
candidates.push({
|
|
284
|
+
request: "@react-native/metro-babel-transformer",
|
|
285
|
+
from: reactNative,
|
|
286
|
+
why: "React Native's transformer, beside the installed react-native",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
candidates.push({
|
|
290
|
+
request: "metro-react-native-babel-transformer",
|
|
291
|
+
from: projectRoot,
|
|
292
|
+
why: "React Native's transformer before 0.73",
|
|
293
|
+
});
|
|
294
|
+
return candidates;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The directory `name` is installed in, looked up the way Node looks up a
|
|
299
|
+
* package — `node_modules` in the starting directory and then in each parent.
|
|
300
|
+
*
|
|
301
|
+
* Walked rather than resolved, because `require.resolve("expo/package.json")`
|
|
302
|
+
* depends on the package's `exports` allowing it, and the question here is only
|
|
303
|
+
* whether the package is installed.
|
|
304
|
+
*/
|
|
305
|
+
function packageDirectory(name, from) {
|
|
306
|
+
let directory = from;
|
|
307
|
+
for (;;) {
|
|
308
|
+
const candidate = path.join(directory, "node_modules", name);
|
|
309
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
310
|
+
const parent = path.dirname(directory);
|
|
311
|
+
if (parent === directory) return null;
|
|
312
|
+
directory = parent;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function projectRootOf(options) {
|
|
317
|
+
return typeof options?.projectRoot === "string" ? options.projectRoot : process.cwd();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function fileIdentity(file) {
|
|
321
|
+
const stat = fs.statSync(file);
|
|
322
|
+
return `${file}:${stat.size}:${stat.mtimeMs}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
transform,
|
|
327
|
+
getCacheKey,
|
|
328
|
+
// Read by `loadUpstream`, so a config composed by two copies of this package
|
|
329
|
+
// is refused instead of recursing into itself.
|
|
330
|
+
isUniflowedMetroTransformer: true,
|
|
331
|
+
// The name `metro.cjs` writes the composed upstream under, kept in one place.
|
|
332
|
+
configuredUpstreamEnv: CONFIGURED_UPSTREAM_ENV,
|
|
333
|
+
};
|
package/metro.cjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/react-native/metro`: the Metro half of uf's native target.
|
|
4
|
+
//
|
|
5
|
+
// # Why this module is CommonJS rather than Flow
|
|
6
|
+
//
|
|
7
|
+
// Every other module in `@uniflowed/*` is Flow, because `uf` is the process
|
|
8
|
+
// that loads it and `uf` installs the hooks that make Flow run. This one is
|
|
9
|
+
// loaded by somebody else's process. Metro evaluates `metro.config.js` in
|
|
10
|
+
// plain Node — under `expo start`, `react-native start`, the bundle phases
|
|
11
|
+
// Xcode and Gradle run, and on EAS Build's machines — and none of those has
|
|
12
|
+
// heard of uf's loader. Until ubugeeei-prod/uf#988 this file was Flow, and
|
|
13
|
+
// outside `uf test` importing it was `SyntaxError: Unexpected token 'export'`.
|
|
14
|
+
// The suite never saw that, because the suite is the one place the hooks are
|
|
15
|
+
// always installed.
|
|
16
|
+
//
|
|
17
|
+
// CommonJS rather than an ES module, because every template that writes a
|
|
18
|
+
// Metro config writes CommonJS — `require("expo/metro-config")`,
|
|
19
|
+
// `require("@react-native/metro-config")` — and an ES module config can still
|
|
20
|
+
// `import { withUniflowedMetro } from "@uniflowed/react-native/metro"`: Node
|
|
21
|
+
// reads a CommonJS module's named exports. The other direction, `require` of
|
|
22
|
+
// an ES module, needs Node 20.19 or 22.12.
|
|
23
|
+
//
|
|
24
|
+
// `uf build --target native` writes the same extension list and pipeline into
|
|
25
|
+
// the build manifest's target contract, so what a finished build reports and
|
|
26
|
+
// what a project's Metro config does are one answer rather than two.
|
|
27
|
+
|
|
28
|
+
"use strict";
|
|
29
|
+
|
|
30
|
+
const path = require("node:path");
|
|
31
|
+
|
|
32
|
+
const { configuredUpstreamEnv } = require("./metro-transformer.cjs");
|
|
33
|
+
|
|
34
|
+
/** Source extensions uf's native target needs, added to the project's own. */
|
|
35
|
+
const metroSourceExts = Object.freeze(["js", "jsx", "mjs", "cjs"]);
|
|
36
|
+
|
|
37
|
+
/** The `package.json` fields Metro reads for an entry point, in order. */
|
|
38
|
+
const metroResolverMainFields = Object.freeze(["react-native", "browser", "main"]);
|
|
39
|
+
|
|
40
|
+
/** What a module goes through, in order, on its way into a native bundle. */
|
|
41
|
+
const metroTransformPipeline = Object.freeze([
|
|
42
|
+
"flow",
|
|
43
|
+
"react-compiler",
|
|
44
|
+
"stylex-css-refusal",
|
|
45
|
+
"metro-babel",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** The Babel transformer Metro runs, as the absolute path Metro wants. */
|
|
49
|
+
const metroTransformerPath = path.join(__dirname, "metro-transformer.cjs");
|
|
50
|
+
|
|
51
|
+
const nativePackages = Object.freeze([
|
|
52
|
+
"brand",
|
|
53
|
+
"cell",
|
|
54
|
+
"core",
|
|
55
|
+
"effect",
|
|
56
|
+
"fetch",
|
|
57
|
+
"form",
|
|
58
|
+
"graphql",
|
|
59
|
+
"hooks",
|
|
60
|
+
"i18n",
|
|
61
|
+
"immer",
|
|
62
|
+
"query",
|
|
63
|
+
"react",
|
|
64
|
+
"react-native",
|
|
65
|
+
"relay",
|
|
66
|
+
"state",
|
|
67
|
+
"temporal",
|
|
68
|
+
"validator",
|
|
69
|
+
]);
|
|
70
|
+
const domPackages = new Set([
|
|
71
|
+
"ui",
|
|
72
|
+
"web",
|
|
73
|
+
"browser",
|
|
74
|
+
"react-testing",
|
|
75
|
+
"story",
|
|
76
|
+
"vrt",
|
|
77
|
+
"pwa",
|
|
78
|
+
"motion",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
function nativeResolution(previous) {
|
|
82
|
+
return (context, moduleName, platform) => {
|
|
83
|
+
const name = moduleName.startsWith("@uniflowed/") ? moduleName.split("/")[1] : null;
|
|
84
|
+
if (
|
|
85
|
+
domPackages.has(name) ||
|
|
86
|
+
(name === "router" &&
|
|
87
|
+
moduleName !== "@uniflowed/router/native" &&
|
|
88
|
+
moduleName !== "@uniflowed/router/routing")
|
|
89
|
+
) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${moduleName} needs a browser DOM and cannot be bundled for ${platform}. ` +
|
|
92
|
+
`Native packages: ${nativePackages.map((item) => `@uniflowed/${item}`).join(", ")}, ` +
|
|
93
|
+
"@uniflowed/router/native and @uniflowed/router/routing. Keep DOM components in $page.web.js.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return (previous ?? context.resolveRequest)(context, moduleName, platform);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Add uf's source extensions, resolver fields and transformer to a Metro config.
|
|
102
|
+
*
|
|
103
|
+
* Metro already owns platform suffix resolution (`.ios.js`, `.android.js`,
|
|
104
|
+
* `.native.js`), and uf's route scanner uses the same suffixes, so nothing
|
|
105
|
+
* about that is declared again here.
|
|
106
|
+
*
|
|
107
|
+
* # Composing with the transformer that is already there
|
|
108
|
+
*
|
|
109
|
+
* Metro accepts one `babelTransformerPath`, and every real config already has
|
|
110
|
+
* one: `getDefaultConfig()` names Expo's in `expo/metro-config` and React
|
|
111
|
+
* Native's in `@react-native/metro-config`. This function used to refuse such a
|
|
112
|
+
* config, which left a hand-written object as the only input it accepted —
|
|
113
|
+
* `withUniflowedMetro(getDefaultConfig(__dirname))` threw in every project that
|
|
114
|
+
* had ever been generated.
|
|
115
|
+
*
|
|
116
|
+
* Replacing that transformer would be worse than refusing it. Expo's is how
|
|
117
|
+
* `babel-preset-expo`, Expo Router and React Refresh reach a module, and a
|
|
118
|
+
* custom one — an SVG transformer, say — is a decision the project made. So the
|
|
119
|
+
* transformer that was there keeps running, after uf's and on uf's output, and
|
|
120
|
+
* uf's takes its place in the config.
|
|
121
|
+
*
|
|
122
|
+
* Metro gives a Babel transformer nothing from the config it came from, so the
|
|
123
|
+
* one being composed travels to Metro's workers in the environment; see
|
|
124
|
+
* `configuredUpstreamEnv` in `metro-transformer.cjs` for why that is the only
|
|
125
|
+
* channel there is. A transformer the config names by package is resolved here,
|
|
126
|
+
* from the project root, where Metro would have resolved it — so a transformer
|
|
127
|
+
* that is not installed is an error while the config loads, not in a worker
|
|
128
|
+
* halfway through the first bundle.
|
|
129
|
+
*
|
|
130
|
+
* Composing twice is composing once: a config whose transformer is already
|
|
131
|
+
* uf's is returned with nothing recorded, so wrapping a config a library has
|
|
132
|
+
* already wrapped cannot make uf's transformer its own upstream.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} [config] a Metro config — usually `getDefaultConfig(__dirname)`
|
|
135
|
+
* @returns {object} the same config, with uf's extensions, fields and transformer
|
|
136
|
+
*/
|
|
137
|
+
function withUniflowedMetro(config = {}) {
|
|
138
|
+
const resolver = config.resolver ?? {};
|
|
139
|
+
const transformer = config.transformer ?? {};
|
|
140
|
+
const existing = transformer.babelTransformerPath;
|
|
141
|
+
if (existing != null && existing !== metroTransformerPath) {
|
|
142
|
+
process.env[configuredUpstreamEnv] = resolveTransformer(existing, config.projectRoot);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
...config,
|
|
146
|
+
resolver: {
|
|
147
|
+
...resolver,
|
|
148
|
+
sourceExts: mergeUnique(resolver.sourceExts, metroSourceExts),
|
|
149
|
+
resolverMainFields: mergeUnique(resolver.resolverMainFields, metroResolverMainFields),
|
|
150
|
+
resolveRequest: nativeResolution(resolver.resolveRequest),
|
|
151
|
+
},
|
|
152
|
+
transformer: {
|
|
153
|
+
...transformer,
|
|
154
|
+
babelTransformerPath: metroTransformerPath,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The absolute path of the transformer a config names.
|
|
161
|
+
*
|
|
162
|
+
* An absolute path is taken as given, because that is what Metro does with it.
|
|
163
|
+
* A package name or a relative path is resolved from the project root, which is
|
|
164
|
+
* what `getDefaultConfig(projectRoot)` records on the config, and from the
|
|
165
|
+
* working directory for a config that does not say.
|
|
166
|
+
*/
|
|
167
|
+
function resolveTransformer(specifier, projectRoot) {
|
|
168
|
+
if (typeof specifier !== "string" || specifier === "") {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`@uniflowed/react-native/metro: transformer.babelTransformerPath is ${JSON.stringify(specifier)}, ` +
|
|
171
|
+
"and the path of a Babel transformer module was expected.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (path.isAbsolute(specifier)) return specifier;
|
|
175
|
+
const from = typeof projectRoot === "string" ? projectRoot : process.cwd();
|
|
176
|
+
try {
|
|
177
|
+
return require.resolve(specifier, { paths: [from] });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`@uniflowed/react-native/metro: transformer.babelTransformerPath names ${specifier}, ` +
|
|
181
|
+
`which does not resolve from ${from}: ${String(error.message).split("\n")[0]}. ` +
|
|
182
|
+
"Install it, or name it by its absolute path.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mergeUnique(existing, required) {
|
|
188
|
+
const merged = [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
for (const value of [...(existing ?? []), ...required]) {
|
|
191
|
+
if (seen.has(value)) continue;
|
|
192
|
+
seen.add(value);
|
|
193
|
+
merged.push(value);
|
|
194
|
+
}
|
|
195
|
+
return merged;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
metroResolverMainFields,
|
|
200
|
+
metroSourceExts,
|
|
201
|
+
metroTransformPipeline,
|
|
202
|
+
metroTransformerPath,
|
|
203
|
+
nativePackages,
|
|
204
|
+
withUniflowedMetro,
|
|
205
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uniflowed/react-native",
|
|
3
|
+
"version": "0.0.0-alpha.40",
|
|
4
|
+
"description": "React Native, re-exported for the Unified Toolchain for Flow.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ubugeeei-prod/uf.git",
|
|
11
|
+
"directory": "packages/react-native"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./metro": "./metro.cjs",
|
|
16
|
+
"./metro-transformer.cjs": "./metro-transformer.cjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"metro.cjs",
|
|
21
|
+
"metro-transformer.cjs",
|
|
22
|
+
"!*.test.js"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@uniflowed/host": "0.0.0-alpha.40"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=19.2.3",
|
|
29
|
+
"react-native": ">=0.86"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"react-native": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|