vxrn 1.26.0 → 1.26.1
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/dist/plugins/reactNativeDevServer.mjs +123 -42
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +209 -44
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.mjs +29 -10
- package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.native.js +29 -10
- package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.mjs +220 -83
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +257 -84
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.test.mjs +285 -23
- package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.test.native.js +319 -23
- package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
- package/dist/utils/patches.test.mjs.map +1 -1
- package/dist/utils/patches.test.native.js.map +1 -1
- package/dist/utils/symbolicateNativeStack.mjs +66 -0
- package/dist/utils/symbolicateNativeStack.mjs.map +1 -0
- package/dist/utils/symbolicateNativeStack.native.js +70 -0
- package/dist/utils/symbolicateNativeStack.native.js.map +1 -0
- package/dist/utils/symbolicateNativeStack.test.mjs +96 -0
- package/dist/utils/symbolicateNativeStack.test.mjs.map +1 -0
- package/dist/utils/symbolicateNativeStack.test.native.js +96 -0
- package/dist/utils/symbolicateNativeStack.test.native.js.map +1 -0
- package/package.json +12 -11
- package/src/plugins/reactNativeDevServer.ts +162 -50
- package/src/rn-commands/bundle/buildBundle.ts +28 -9
- package/src/utils/createNativeDevEngine.test.ts +581 -193
- package/src/utils/createNativeDevEngine.ts +434 -115
- package/src/utils/patches.test.ts +1 -4
- package/src/utils/symbolicateNativeStack.test.ts +78 -0
- package/src/utils/symbolicateNativeStack.ts +129 -0
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
- package/types/utils/createNativeDevEngine.d.ts +23 -4
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
- package/types/utils/symbolicateNativeStack.d.ts +46 -0
- package/types/utils/symbolicateNativeStack.d.ts.map +1 -0
- package/types/utils/symbolicateNativeStack.test.d.ts +2 -0
- package/types/utils/symbolicateNativeStack.test.d.ts.map +1 -0
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
mkdirSync,
|
|
15
15
|
realpathSync,
|
|
16
16
|
readdirSync,
|
|
17
|
+
statSync,
|
|
17
18
|
readFileSync,
|
|
18
19
|
} from 'node:fs'
|
|
19
20
|
import { pathToFileURL } from 'node:url'
|
|
21
|
+
import micromatch from 'micromatch'
|
|
20
22
|
import type { InputOptions, OutputOptions, Plugin, RolldownOutput } from 'rolldown'
|
|
21
23
|
import type { DevEngine } from 'rolldown/experimental'
|
|
22
24
|
import { loadEnv as loadViteEnv, normalizePath } from 'vite'
|
|
@@ -52,6 +54,12 @@ export function getHermesSWCIncludes(dev: boolean): string[] {
|
|
|
52
54
|
return [...HERMES_CLASS_TRANSFORMS, ...HERMES_ASYNC_TRANSFORMS]
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
export interface NativePluginContext {
|
|
58
|
+
root: string
|
|
59
|
+
platform: 'ios' | 'android'
|
|
60
|
+
dev: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
interface NativeDevEngineOptions {
|
|
56
64
|
root: string
|
|
57
65
|
port: number
|
|
@@ -70,14 +78,27 @@ export type NativeHmrUpdate =
|
|
|
70
78
|
changedIds: string[]
|
|
71
79
|
seq: number
|
|
72
80
|
}
|
|
73
|
-
|
|
81
|
+
// a reload with no clientId goes to every client on the platform: a full
|
|
82
|
+
// build re-expands the route globs for all of them at once
|
|
83
|
+
| { type: 'hmr:reload'; clientId?: string }
|
|
74
84
|
| { type: 'hmr:error' }
|
|
75
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The served dev bundle and the map that resolves a frame in it back to the
|
|
88
|
+
* authored file. `map` is the serialized JSON, parsed only when a symbolicate
|
|
89
|
+
* request arrives.
|
|
90
|
+
*/
|
|
91
|
+
export interface NativeDevBundle {
|
|
92
|
+
code: string
|
|
93
|
+
map: string
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
interface NativeDevEngineResult {
|
|
77
97
|
engine: DevEngine
|
|
78
|
-
getBundle: () => Promise<
|
|
98
|
+
getBundle: () => Promise<NativeDevBundle>
|
|
79
99
|
getAsset: (pathname: string, hash?: string) => NativeDevAsset | undefined
|
|
80
100
|
close: () => Promise<void>
|
|
101
|
+
handleAddedFile: (file: string) => Promise<void>
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
// shared resolve extensions for native builds
|
|
@@ -221,13 +242,15 @@ export function getNativeTransformConfig(
|
|
|
221
242
|
// shared plugins used by both dev and prod native builds
|
|
222
243
|
function getNativePlugins(
|
|
223
244
|
root: string,
|
|
224
|
-
platform:
|
|
245
|
+
platform: NativePluginContext['platform'],
|
|
225
246
|
viteImportGlobPlugin: any,
|
|
226
247
|
dev: boolean,
|
|
227
248
|
assetsDest?: string,
|
|
228
249
|
onAsset?: (asset: NativeAssetData) => void,
|
|
229
|
-
sourceMaps = false
|
|
250
|
+
sourceMaps = false,
|
|
251
|
+
userPlugins: Plugin[] = []
|
|
230
252
|
): Plugin[] {
|
|
253
|
+
const context: NativePluginContext = { root, platform, dev }
|
|
231
254
|
return [
|
|
232
255
|
// plugins provided by One (clientTreeShakePlugin for loader removal, etc.)
|
|
233
256
|
...(globalThis.__vxrnAddNativePlugins || []),
|
|
@@ -264,17 +287,31 @@ function getNativePlugins(
|
|
|
264
287
|
// hermes compat: per-iteration loop bindings. runs last so it also covers
|
|
265
288
|
// loops the earlier lowering steps emit.
|
|
266
289
|
hermesLoopsPlugin(sourceMaps),
|
|
267
|
-
|
|
290
|
+
...userPlugins,
|
|
291
|
+
].map((plugin: Plugin) =>
|
|
292
|
+
plugin.api?.vxrnNative ? plugin.api.vxrnNative(context) : plugin
|
|
293
|
+
)
|
|
268
294
|
}
|
|
269
295
|
|
|
270
|
-
// shared output options for native builds
|
|
271
|
-
|
|
296
|
+
// shared output options for native builds. `minify: false` keeps rolldown's
|
|
297
|
+
// default `'dce-only'`, which is what native builds have always run with.
|
|
298
|
+
function getNativeOutputOptions(
|
|
299
|
+
prelude: string,
|
|
300
|
+
sourcemap: boolean,
|
|
301
|
+
minify: boolean
|
|
302
|
+
): OutputOptions {
|
|
272
303
|
return {
|
|
273
304
|
format: 'esm',
|
|
274
305
|
sourcemap,
|
|
306
|
+
// emit absolute source paths, the way Metro does. a relative path is only
|
|
307
|
+
// meaningful next to the map file, and neither /symbolicate nor a crash
|
|
308
|
+
// reporter reading the shipped .map has that directory.
|
|
309
|
+
sourcemapPathTransform: (relativeSourcePath, sourcemapPath) =>
|
|
310
|
+
normalizePath(resolve(dirname(sourcemapPath), relativeSourcePath)),
|
|
275
311
|
intro: prelude,
|
|
276
312
|
codeSplitting: false,
|
|
277
313
|
strictExecutionOrder: true,
|
|
314
|
+
minify: minify || 'dce-only',
|
|
278
315
|
}
|
|
279
316
|
}
|
|
280
317
|
|
|
@@ -293,7 +330,40 @@ export function normalizeNativeCommonJSInterop(code: string): string {
|
|
|
293
330
|
)
|
|
294
331
|
}
|
|
295
332
|
|
|
296
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Every post-processing pass below rewrites the bundle after Rolldown has
|
|
335
|
+
* already emitted its source map, so a pass that changes the number of lines
|
|
336
|
+
* shifts every later frame away from the source it maps to. Replacing a removed
|
|
337
|
+
* span with its own newlines keeps each surviving statement on the line the map
|
|
338
|
+
* recorded for it.
|
|
339
|
+
*/
|
|
340
|
+
function blankPreservingLines(match: string): string {
|
|
341
|
+
return '\n'.repeat(match.match(/\n/g)?.length ?? 0)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function countLines(code: string): number {
|
|
345
|
+
return (code.match(/\n/g)?.length ?? 0) + 1
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Guard the source map's only invariant: application code has to stay on the
|
|
350
|
+
* line Rolldown mapped it to. A pass that gains or loses a line is a bug in that
|
|
351
|
+
* pass, and a symbolicated frame that silently points a few lines off is worse
|
|
352
|
+
* than a build that stops and says so.
|
|
353
|
+
*/
|
|
354
|
+
function assertBundleLinesPreserved(before: string, after: string, stage: string): void {
|
|
355
|
+
const beforeLines = countLines(before)
|
|
356
|
+
const afterLines = countLines(after)
|
|
357
|
+
if (beforeLines !== afterLines) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`[vxrn] native bundle post-processing (${stage}) changed the line count ` +
|
|
360
|
+
`(${beforeLines} -> ${afterLines}), which invalidates the source map. ` +
|
|
361
|
+
`Every pass after Rolldown emits the map must preserve lines.`
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function postProcessNativeBundle(code: string): string {
|
|
297
367
|
code = normalizeNativeCommonJSInterop(code)
|
|
298
368
|
|
|
299
369
|
// Rolldown replaces import.meta.env reads but can leave a guarding
|
|
@@ -302,9 +372,17 @@ function postProcessNativeBundle(code: string): string {
|
|
|
302
372
|
code = code.replace(/\btypeof\s+import\.meta\b/g, '"object"')
|
|
303
373
|
|
|
304
374
|
// rolldown devMode still emits ESM export statements that hermes can't parse.
|
|
305
|
-
// this is a rolldown behavior we can't configure away yet.
|
|
306
|
-
|
|
307
|
-
|
|
375
|
+
// this is a rolldown behavior we can't configure away yet. both patterns match
|
|
376
|
+
// horizontal whitespace only: a plain `\s` swallows the blank line above the
|
|
377
|
+
// statement, and a swallowed line moves every frame below it.
|
|
378
|
+
code = code.replace(
|
|
379
|
+
/^[^\S\n]*export[^\S\n]*\{[^}]*\}[^\S\n]*;?[^\S\n]*$/gm,
|
|
380
|
+
blankPreservingLines
|
|
381
|
+
)
|
|
382
|
+
code = code.replace(
|
|
383
|
+
/^([^\S\n]*)export[^\S\n]+default[^\S\n]+([^;\n]+);?[^\S\n]*$/gm,
|
|
384
|
+
'$1$2;'
|
|
385
|
+
)
|
|
308
386
|
// rolldown devMode runtime leaves some raw import.meta.hot references
|
|
309
387
|
// that aren't compiled through the normal plugin pipeline.
|
|
310
388
|
code = code.replace(/^if \(import\.meta\.hot\).*$/gm, '')
|
|
@@ -322,9 +400,11 @@ function postProcessNativeBundle(code: string): string {
|
|
|
322
400
|
const afterMarker = code.indexOf('})();', idx)
|
|
323
401
|
if (afterMarker !== -1) {
|
|
324
402
|
const end = afterMarker + '})();'.length
|
|
403
|
+
const removed = code.slice(beforeMarker, end)
|
|
325
404
|
code =
|
|
326
405
|
code.slice(0, beforeMarker) +
|
|
327
406
|
'NativeAnimatedModule = NativeAnimatedModule_default ?? NativeAnimatedTurboModule_default;' +
|
|
407
|
+
blankPreservingLines(removed) +
|
|
328
408
|
code.slice(end)
|
|
329
409
|
}
|
|
330
410
|
}
|
|
@@ -365,7 +445,10 @@ export function wrapNativeBundleModuleScope(code: string): string {
|
|
|
365
445
|
const idx = code.indexOf(marker)
|
|
366
446
|
if (idx === -1) return code
|
|
367
447
|
|
|
368
|
-
|
|
448
|
+
// the opener shares the marker's line (the rest of that line is a comment) so
|
|
449
|
+
// the wrap costs no lines, and the closer only appends past the last mapped
|
|
450
|
+
// line. the served bundle therefore stays aligned with Rolldown's source map.
|
|
451
|
+
return code.slice(0, idx) + ';(function() {' + code.slice(idx) + '\n})();\n'
|
|
369
452
|
}
|
|
370
453
|
|
|
371
454
|
/**
|
|
@@ -415,6 +498,33 @@ async function downlevelClassFieldsInBundle(code: string): Promise<string> {
|
|
|
415
498
|
return code.slice(0, startIdx) + linePreservingRuntime + code.slice(runtimeEnd)
|
|
416
499
|
}
|
|
417
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Run the native post-processing passes inside the bundle, where rolldown still
|
|
503
|
+
* hands them unminified output.
|
|
504
|
+
*
|
|
505
|
+
* Rolldown minifies a chunk *after* plugin `renderChunk` hooks, and every pass
|
|
506
|
+
* below reads the shape rolldown generates: `normalizeNativeCommonJSInterop`
|
|
507
|
+
* matches `__toESM(require_x(), 1)` by name, and `downlevelClassFieldsInBundle`
|
|
508
|
+
* locates the runtime by its `//#region` comment. Minification mangles the
|
|
509
|
+
* first and strips the second, so running these on the finished chunk would
|
|
510
|
+
* silently stop applying them as soon as `--minify` is on — a production bundle
|
|
511
|
+
* whose CommonJS interop differs from the dev one. This hook is what keeps a
|
|
512
|
+
* minified and an unminified bundle the same bundle.
|
|
513
|
+
*
|
|
514
|
+
* `map: null` records what the passes guarantee: they replace syntax in place
|
|
515
|
+
* without moving a line, so rolldown's own map still describes the result.
|
|
516
|
+
*/
|
|
517
|
+
function nativeBundlePostProcessPlugin(): Plugin {
|
|
518
|
+
return {
|
|
519
|
+
name: 'vxrn:native-post-process',
|
|
520
|
+
async renderChunk(code) {
|
|
521
|
+
const processed = await downlevelClassFieldsInBundle(postProcessNativeBundle(code))
|
|
522
|
+
assertBundleLinesPreserved(code, processed, 'production')
|
|
523
|
+
return { code: processed, map: null }
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
418
528
|
export async function createNativeDevEngine(
|
|
419
529
|
options: NativeDevEngineOptions
|
|
420
530
|
): Promise<NativeDevEngineResult> {
|
|
@@ -439,13 +549,25 @@ export async function createNativeDevEngine(
|
|
|
439
549
|
})
|
|
440
550
|
const assetRegistry = createNativeDevAssetRegistry()
|
|
441
551
|
|
|
442
|
-
let currentBundle:
|
|
552
|
+
let currentBundle: NativeDevBundle | null = null
|
|
443
553
|
let firstBuildError: Error | null = null
|
|
444
|
-
let bundleResolve: ((value:
|
|
554
|
+
let bundleResolve: ((value: NativeDevBundle) => void) | null = null
|
|
445
555
|
let bundleReject: ((error: Error) => void) | null = null
|
|
446
|
-
let bundlePromise: Promise<
|
|
556
|
+
let bundlePromise: Promise<NativeDevBundle> | null = null
|
|
447
557
|
|
|
448
558
|
const resolvedHost = host === '0.0.0.0' ? 'localhost' : host
|
|
559
|
+
const virtualEntry = nativeVirtualEntryPlugin(root, { dev: true })
|
|
560
|
+
|
|
561
|
+
// one build at a time: a bundle request and a full rebuild can arrive
|
|
562
|
+
// together (a reload lands on the bundle route while a new route file is
|
|
563
|
+
// being picked up), and the engine holds a single output the second would
|
|
564
|
+
// race the first for.
|
|
565
|
+
let engineWork: Promise<unknown> = Promise.resolve()
|
|
566
|
+
const queueEngineWork = <T>(work: () => Promise<T>): Promise<T> => {
|
|
567
|
+
const result = engineWork.then(work, work)
|
|
568
|
+
engineWork = result.catch(() => {})
|
|
569
|
+
return result
|
|
570
|
+
}
|
|
449
571
|
|
|
450
572
|
const inputOptions: InputOptions = {
|
|
451
573
|
input: VIRTUAL_NATIVE_ENTRY,
|
|
@@ -475,7 +597,7 @@ export async function createNativeDevEngine(
|
|
|
475
597
|
},
|
|
476
598
|
|
|
477
599
|
plugins: [
|
|
478
|
-
|
|
600
|
+
virtualEntry.plugin,
|
|
479
601
|
...getNativePlugins(
|
|
480
602
|
root,
|
|
481
603
|
platform,
|
|
@@ -483,91 +605,78 @@ export async function createNativeDevEngine(
|
|
|
483
605
|
true,
|
|
484
606
|
undefined,
|
|
485
607
|
assetRegistry.register,
|
|
486
|
-
|
|
608
|
+
// per-module maps: without them a module's transform output has no path
|
|
609
|
+
// back to its authored file, and /symbolicate can only report the
|
|
610
|
+
// bundle offset it was handed.
|
|
611
|
+
true,
|
|
612
|
+
userPlugins
|
|
487
613
|
),
|
|
488
|
-
...userPlugins,
|
|
489
614
|
],
|
|
490
615
|
}
|
|
491
616
|
|
|
492
617
|
const outputOptions: OutputOptions = {
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
//
|
|
618
|
+
// the dev map is what /symbolicate resolves a device frame through. it is
|
|
619
|
+
// generated once per full bundle build, not per edit: rebuildStrategy is
|
|
620
|
+
// 'never', so Fast Refresh patches never re-enter this path. a dev bundle
|
|
621
|
+
// is never minified.
|
|
622
|
+
...getNativeOutputOptions(prelude, true, false),
|
|
623
|
+
// open the HMR socket with RN's WebSocket module (not the global, which is
|
|
624
|
+
// only polyfilled once InitializeCore has run) and hand it to the runtime,
|
|
625
|
+
// which owns every message it carries. handed over immediately rather than
|
|
626
|
+
// from an open handler: a host that does not honor `socket.onopen = fn`
|
|
627
|
+
// would otherwise silently never get Fast Refresh.
|
|
500
628
|
outro: `
|
|
501
629
|
try {
|
|
502
630
|
var __WS = (init_WebSocket(), __toCommonJS(WebSocket_exports)).default;
|
|
503
631
|
var __hmrUrl = 'ws://${resolvedHost}:${port}/hot?platform=${platform}&clientId=' + encodeURIComponent(__rolldown_runtime__.clientId);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
var applied = __rolldown_runtime__.applyHmrUpdate(msg.code, msg.changedIds, msg.seq);
|
|
511
|
-
if (!applied) {
|
|
512
|
-
var updateSettings = g.__turboModuleProxy ? g.__turboModuleProxy('DevSettings') : null;
|
|
513
|
-
if (updateSettings && updateSettings.reload) updateSettings.reload();
|
|
514
|
-
}
|
|
515
|
-
} else if (msg.type === 'hmr:reload') {
|
|
516
|
-
var ds = g.__turboModuleProxy ? g.__turboModuleProxy('DevSettings') : null;
|
|
517
|
-
if (ds && ds.reload) ds.reload();
|
|
518
|
-
}
|
|
519
|
-
} catch(e) { console.error('[vxrn] HMR eval error:', e); }
|
|
520
|
-
};
|
|
521
|
-
__hmrWS.onopen = function() {
|
|
522
|
-
if (typeof __rolldown_runtime__ !== 'undefined' && __rolldown_runtime__.setup) {
|
|
523
|
-
__rolldown_runtime__.setup(__hmrWS);
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
__hmrWS.onerror = function(e) { console.warn('[vxrn] HMR connection error:', e.message || e); };
|
|
527
|
-
} catch(e) {}
|
|
632
|
+
__rolldown_runtime__.setup(new __WS(__hmrUrl));
|
|
633
|
+
} catch(e) {
|
|
634
|
+
// a swallowed failure here leaves the app permanently without Fast Refresh and
|
|
635
|
+
// nothing on screen or in the terminal says so.
|
|
636
|
+
console.error('[vxrn] HMR client failed to start:', (e && e.message) || e);
|
|
637
|
+
}
|
|
528
638
|
`,
|
|
529
639
|
}
|
|
530
640
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
641
|
+
// rolldown does not await this callback, so `ensureLatestBuildOutput()`
|
|
642
|
+
// resolves while the emitted chunk is still being post-processed and
|
|
643
|
+
// `currentBundle` still holds the previous build. anything that has to see
|
|
644
|
+
// the finished bundle waits on this too.
|
|
645
|
+
let outputProcessed: Promise<void> = Promise.resolve()
|
|
646
|
+
|
|
647
|
+
let engine: Awaited<ReturnType<typeof dev>>
|
|
648
|
+
|
|
649
|
+
const rebuildIfNewRoute = async (files: string[]): Promise<boolean> => {
|
|
650
|
+
const { routeRoot, files: knownRoutes, isRouteFile } = virtualEntry.routes
|
|
651
|
+
const routeRootPrefix = `${normalizePath(routeRoot)}/`
|
|
652
|
+
const routeAdded = files.some((file) => {
|
|
653
|
+
const normalized = normalizePath(file)
|
|
654
|
+
if (isRouteFile(normalized)) return !knownRoutes.has(normalized)
|
|
655
|
+
return (
|
|
656
|
+
normalized.startsWith(routeRootPrefix) &&
|
|
657
|
+
statSync(file, { throwIfNoEntry: false })?.isDirectory() === true
|
|
658
|
+
)
|
|
659
|
+
})
|
|
660
|
+
if (!routeAdded) return false
|
|
661
|
+
await queueEngineWork(async () => {
|
|
662
|
+
engine.triggerFullBuild()
|
|
663
|
+
await engine.ensureLatestBuildOutput()
|
|
664
|
+
await outputProcessed
|
|
665
|
+
})
|
|
666
|
+
onHmrUpdate?.({ type: 'hmr:reload' })
|
|
667
|
+
return true
|
|
668
|
+
}
|
|
547
669
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
// wrap module code in a function scope so top-level `var`s (e.g. RN
|
|
559
|
-
// fetch.js's `Headers`/`Request`) don't leak as non-configurable
|
|
560
|
-
// globals and break RN's polyfillGlobal (dev-only redbox). see fn doc.
|
|
561
|
-
code = wrapNativeBundleModuleScope(code)
|
|
562
|
-
|
|
563
|
-
currentBundle = { code }
|
|
564
|
-
console.info(`[vxrn] native bundle ready (${Math.round(code.length / 1024)}KB)`)
|
|
565
|
-
if (bundleResolve) {
|
|
566
|
-
bundleResolve(currentBundle)
|
|
567
|
-
bundleResolve = null
|
|
568
|
-
bundleReject = null
|
|
569
|
-
bundlePromise = null
|
|
570
|
-
}
|
|
670
|
+
engine = await dev(inputOptions, outputOptions, {
|
|
671
|
+
onOutput: async (result) => {
|
|
672
|
+
let finishOutput = () => {}
|
|
673
|
+
outputProcessed = new Promise<void>((resolve) => {
|
|
674
|
+
finishOutput = resolve
|
|
675
|
+
})
|
|
676
|
+
try {
|
|
677
|
+
await handleOutput(result)
|
|
678
|
+
} finally {
|
|
679
|
+
finishOutput()
|
|
571
680
|
}
|
|
572
681
|
},
|
|
573
682
|
|
|
@@ -577,6 +686,14 @@ try {
|
|
|
577
686
|
onHmrUpdate?.({ type: 'hmr:error' })
|
|
578
687
|
return
|
|
579
688
|
}
|
|
689
|
+
|
|
690
|
+
// a route file that was just created has no module, so rolldown reports
|
|
691
|
+
// the change and stops: in devMode it never re-scans, and the entry's
|
|
692
|
+
// `import.meta.glob` was expanded back when it transformed the entry.
|
|
693
|
+
// only a full build re-expands it, and rolldown tells no client that
|
|
694
|
+
// happened, so the reload is sent from here.
|
|
695
|
+
if (await rebuildIfNewRoute(result.changedFiles)) return
|
|
696
|
+
|
|
580
697
|
for (const { clientId, update } of result.updates) {
|
|
581
698
|
if (update.type === 'Patch' && update.code) {
|
|
582
699
|
onHmrUpdate?.({
|
|
@@ -598,6 +715,67 @@ try {
|
|
|
598
715
|
watch: {},
|
|
599
716
|
})
|
|
600
717
|
|
|
718
|
+
async function handleOutput(result: RolldownOutput | Error) {
|
|
719
|
+
if (result instanceof Error) {
|
|
720
|
+
console.error('[vxrn] native bundle error:', result.message)
|
|
721
|
+
if (/panic/i.test(result.message)) {
|
|
722
|
+
// a panicked rolldown worker stops producing HMR patches for the rest
|
|
723
|
+
// of the process, so every later edit reloads the whole app. nothing
|
|
724
|
+
// else says so, and the app looks fine.
|
|
725
|
+
console.error(
|
|
726
|
+
'[vxrn] rolldown itself panicked. Fast Refresh will full-reload until you restart the dev server.'
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
if (!currentBundle) {
|
|
730
|
+
firstBuildError ||= result
|
|
731
|
+
if (bundleReject) {
|
|
732
|
+
const reject = bundleReject
|
|
733
|
+
bundleResolve = null
|
|
734
|
+
bundleReject = null
|
|
735
|
+
bundlePromise = null
|
|
736
|
+
reject(firstBuildError)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const output = result as RolldownOutput
|
|
743
|
+
const chunk = output.output.find((o) => o.type === 'chunk' && o.isEntry)
|
|
744
|
+
if (chunk && 'code' in chunk) {
|
|
745
|
+
firstBuildError = null
|
|
746
|
+
let code = postProcessNativeBundle(chunk.code)
|
|
747
|
+
|
|
748
|
+
// downlevel class fields from the rolldown runtime (virtual module
|
|
749
|
+
// skipped by the per-file SWC plugin) so old Hermes can parse them
|
|
750
|
+
code = await downlevelClassFieldsInBundle(code)
|
|
751
|
+
assertBundleLinesPreserved(chunk.code, code, 'dev')
|
|
752
|
+
|
|
753
|
+
// wrap module code in a function scope so top-level `var`s (e.g. RN
|
|
754
|
+
// fetch.js's `Headers`/`Request`) don't leak as non-configurable
|
|
755
|
+
// globals and break RN's polyfillGlobal (dev-only redbox). see fn doc.
|
|
756
|
+
// it only appends past the last mapped line, so it needs no assertion.
|
|
757
|
+
code = wrapNativeBundleModuleScope(code)
|
|
758
|
+
|
|
759
|
+
if (!chunk.map) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
'[vxrn] rolldown produced no source map for the dev bundle, so /symbolicate cannot resolve a frame'
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
// hold the serialized map, not a parsed one: parsing a map for a bundle
|
|
765
|
+
// this size costs hundreds of megabytes that would stay resident for the
|
|
766
|
+
// life of the dev server, and /symbolicate runs only when a frame needs
|
|
767
|
+
// resolving.
|
|
768
|
+
currentBundle = { code, map: chunk.map.toString() }
|
|
769
|
+
console.info(`[vxrn] native bundle ready (${Math.round(code.length / 1024)}KB)`)
|
|
770
|
+
if (bundleResolve) {
|
|
771
|
+
bundleResolve(currentBundle)
|
|
772
|
+
bundleResolve = null
|
|
773
|
+
bundleReject = null
|
|
774
|
+
bundlePromise = null
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
601
779
|
await engine.run()
|
|
602
780
|
|
|
603
781
|
return {
|
|
@@ -606,7 +784,10 @@ try {
|
|
|
606
784
|
async getBundle() {
|
|
607
785
|
// a runtime invalidation marks the full output stale before reloading. pull
|
|
608
786
|
// that output before serving the cached bundle to the restarted app.
|
|
609
|
-
await
|
|
787
|
+
await queueEngineWork(async () => {
|
|
788
|
+
await engine.ensureLatestBuildOutput()
|
|
789
|
+
await outputProcessed
|
|
790
|
+
})
|
|
610
791
|
if (currentBundle) return currentBundle
|
|
611
792
|
if (firstBuildError) throw firstBuildError
|
|
612
793
|
if (!bundlePromise) {
|
|
@@ -638,6 +819,14 @@ try {
|
|
|
638
819
|
async close() {
|
|
639
820
|
await engine.close()
|
|
640
821
|
},
|
|
822
|
+
|
|
823
|
+
// vite's watcher sees a created file even when rolldown's addWatchFile
|
|
824
|
+
// directory watches do not, which is what happens on github's macos
|
|
825
|
+
// runners. scoped to route files so a random write in the project is not
|
|
826
|
+
// a full rebuild.
|
|
827
|
+
async handleAddedFile(file: string) {
|
|
828
|
+
await rebuildIfNewRoute([file])
|
|
829
|
+
},
|
|
641
830
|
}
|
|
642
831
|
}
|
|
643
832
|
|
|
@@ -653,6 +842,11 @@ interface NativeBuildOptions {
|
|
|
653
842
|
plugins?: Plugin[]
|
|
654
843
|
/** only pass when the map is written somewhere — it costs a second copy of the bundle */
|
|
655
844
|
sourcemap?: boolean
|
|
845
|
+
/**
|
|
846
|
+
* Compress and mangle the output. Defaults to React Native's own rule for a
|
|
847
|
+
* bundle: on unless the build is a dev build.
|
|
848
|
+
*/
|
|
849
|
+
minify?: boolean
|
|
656
850
|
}
|
|
657
851
|
|
|
658
852
|
export async function buildNativeBundle(
|
|
@@ -667,6 +861,7 @@ export async function buildNativeBundle(
|
|
|
667
861
|
assetsDest,
|
|
668
862
|
plugins: userPlugins = [],
|
|
669
863
|
sourcemap = false,
|
|
864
|
+
minify = !dev,
|
|
670
865
|
} = options
|
|
671
866
|
|
|
672
867
|
const { build } = await import('rolldown')
|
|
@@ -699,7 +894,7 @@ export async function buildNativeBundle(
|
|
|
699
894
|
shimMissingExports: true,
|
|
700
895
|
moduleTypes: { '.js': 'jsx' },
|
|
701
896
|
plugins: [
|
|
702
|
-
...(entryFile ? [] : [nativeVirtualEntryPlugin(root, { dev })]),
|
|
897
|
+
...(entryFile ? [] : [nativeVirtualEntryPlugin(root, { dev }).plugin]),
|
|
703
898
|
...getNativePlugins(
|
|
704
899
|
root,
|
|
705
900
|
platform,
|
|
@@ -707,11 +902,13 @@ export async function buildNativeBundle(
|
|
|
707
902
|
dev,
|
|
708
903
|
assetsDest,
|
|
709
904
|
undefined,
|
|
710
|
-
sourcemap
|
|
905
|
+
sourcemap,
|
|
906
|
+
userPlugins
|
|
711
907
|
),
|
|
712
|
-
|
|
908
|
+
// last, so it sees what every other plugin produced
|
|
909
|
+
nativeBundlePostProcessPlugin(),
|
|
713
910
|
],
|
|
714
|
-
output: getNativeOutputOptions(prelude, sourcemap),
|
|
911
|
+
output: getNativeOutputOptions(prelude, sourcemap, minify),
|
|
715
912
|
})
|
|
716
913
|
const chunk = result.output.find((o) => o.type === 'chunk' && o.isEntry)
|
|
717
914
|
|
|
@@ -719,18 +916,38 @@ export async function buildNativeBundle(
|
|
|
719
916
|
throw new Error('[vxrn] production build produced no output')
|
|
720
917
|
}
|
|
721
918
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
//
|
|
728
|
-
|
|
919
|
+
if (sourcemap && !chunk.map) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
'[vxrn] a source map was requested but rolldown produced none for the production bundle'
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
// Per-module Babel/SWC transforms return maps when requested, and
|
|
925
|
+
// nativeBundlePostProcessPlugin runs inside the bundle, so Rolldown's map
|
|
926
|
+
// already describes the emitted chunk and composes back to original source.
|
|
927
|
+
return {
|
|
928
|
+
code: chunk.code,
|
|
929
|
+
map: sourcemap ? chunk.map?.toString() : undefined,
|
|
930
|
+
}
|
|
729
931
|
}
|
|
730
932
|
|
|
731
933
|
const VIRTUAL_NATIVE_ENTRY = 'virtual:native-entry'
|
|
732
934
|
|
|
733
|
-
|
|
935
|
+
/**
|
|
936
|
+
* The route files the last build expanded `import.meta.glob` over. rolldown's
|
|
937
|
+
* dev engine reports a change to a watched file it holds no module for, but in
|
|
938
|
+
* devMode it never re-scans, so this is what tells a created route apart from
|
|
939
|
+
* an edit to one that already exists.
|
|
940
|
+
*/
|
|
941
|
+
interface NativeRouteRegistry {
|
|
942
|
+
routeRoot: string
|
|
943
|
+
files: Set<string>
|
|
944
|
+
isRouteFile: (file: string) => boolean
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function nativeVirtualEntryPlugin(
|
|
948
|
+
root: string,
|
|
949
|
+
opts?: { dev?: boolean }
|
|
950
|
+
): { plugin: Plugin; routes: NativeRouteRegistry } {
|
|
734
951
|
const isDev = opts?.dev !== false
|
|
735
952
|
// absolute for import.meta.glob resolution; forward-slash for module-graph convention
|
|
736
953
|
const resolvedId = normalizePath(resolve(root, '__virtual-native-entry.tsx'))
|
|
@@ -810,17 +1027,71 @@ createApp({
|
|
|
810
1027
|
});
|
|
811
1028
|
`
|
|
812
1029
|
|
|
1030
|
+
const routeRoot = resolve(root, routerRoot)
|
|
1031
|
+
// the entry's globs are written for `import.meta.glob`, which reads a leading
|
|
1032
|
+
// `!` as an exclusion and a leading `./` as the project root. micromatch
|
|
1033
|
+
// reads neither: `!p` matches everything except p, so a negation would match
|
|
1034
|
+
// every ordinary file, and `./app/x` never matches `./app/**`. split the
|
|
1035
|
+
// exclusions out and drop the `./` from both sides to keep the same meaning.
|
|
1036
|
+
const stripDot = (pattern: string) => pattern.replace(/^\.\//, '')
|
|
1037
|
+
const includeGlobs = routeGlobs.filter((p) => !p.startsWith('!')).map(stripDot)
|
|
1038
|
+
const excludeGlobs = routeGlobs
|
|
1039
|
+
.filter((p) => p.startsWith('!'))
|
|
1040
|
+
.map((p) => stripDot(p.slice(1)))
|
|
1041
|
+
const isRouteFile = (file: string) => {
|
|
1042
|
+
const relativeToRoot = normalizePath(relative(root, file))
|
|
1043
|
+
return (
|
|
1044
|
+
micromatch.isMatch(relativeToRoot, includeGlobs) &&
|
|
1045
|
+
!micromatch.isMatch(relativeToRoot, excludeGlobs)
|
|
1046
|
+
)
|
|
1047
|
+
}
|
|
1048
|
+
const routes: NativeRouteRegistry = {
|
|
1049
|
+
routeRoot,
|
|
1050
|
+
files: new Set(),
|
|
1051
|
+
isRouteFile,
|
|
1052
|
+
}
|
|
1053
|
+
|
|
813
1054
|
return {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1055
|
+
routes,
|
|
1056
|
+
plugin: {
|
|
1057
|
+
name: 'vxrn:native-virtual-entry',
|
|
1058
|
+
resolveId(id) {
|
|
1059
|
+
if (id === VIRTUAL_NATIVE_ENTRY) {
|
|
1060
|
+
return resolvedId
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
load(id) {
|
|
1064
|
+
if (id !== resolvedId) return
|
|
1065
|
+
// rolldown expands `import.meta.glob` during transform and records a
|
|
1066
|
+
// dependency on the files it matched, never on the directories it
|
|
1067
|
+
// searched. registering those directories is what makes its own watcher
|
|
1068
|
+
// report a route file appearing, and walking them is what lets the
|
|
1069
|
+
// engine tell that report apart from an edit.
|
|
1070
|
+
//
|
|
1071
|
+
// watching a directory is not recursive, so every directory under the
|
|
1072
|
+
// route root is registered. a new subdirectory is itself a change to
|
|
1073
|
+
// its watched parent, and the full build that follows re-runs this
|
|
1074
|
+
// hook, so the build that picks up `app/foo/index.tsx` is also what
|
|
1075
|
+
// starts watching `app/foo`.
|
|
1076
|
+
routes.files.clear()
|
|
1077
|
+
const walkRouteRoot = (dir: string) => {
|
|
1078
|
+
this.addWatchFile(dir)
|
|
1079
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1080
|
+
const child = resolve(dir, entry.name)
|
|
1081
|
+
if (entry.isDirectory()) walkRouteRoot(child)
|
|
1082
|
+
else if (isRouteFile(child)) routes.files.add(normalizePath(child))
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// a project can have no route root at all: `import.meta.glob` then
|
|
1086
|
+
// expands to nothing and the app builds fine. watch the directory that
|
|
1087
|
+
// would hold it, so creating it is the change that starts the walk.
|
|
1088
|
+
if (statSync(routeRoot, { throwIfNoEntry: false })?.isDirectory()) {
|
|
1089
|
+
walkRouteRoot(routeRoot)
|
|
1090
|
+
} else {
|
|
1091
|
+
this.addWatchFile(dirname(routeRoot))
|
|
1092
|
+
}
|
|
822
1093
|
return entryCode
|
|
823
|
-
}
|
|
1094
|
+
},
|
|
824
1095
|
},
|
|
825
1096
|
}
|
|
826
1097
|
}
|
|
@@ -1029,7 +1300,9 @@ export function vxrnCompilerPlugin(
|
|
|
1029
1300
|
)
|
|
1030
1301
|
}
|
|
1031
1302
|
const workletEntry = babelOptions?.plugins?.find(isWorkletPlugin)
|
|
1032
|
-
const workletPluginOptions = Array.isArray(workletEntry)
|
|
1303
|
+
const workletPluginOptions = Array.isArray(workletEntry)
|
|
1304
|
+
? workletEntry[1]
|
|
1305
|
+
: undefined
|
|
1033
1306
|
const useWorklets =
|
|
1034
1307
|
compiler.isNativeWorkletsEnabled() &&
|
|
1035
1308
|
(Boolean(workletEntry) || compiler.shouldTransformWorklets({ id, code }))
|
|
@@ -1041,7 +1314,11 @@ export function vxrnCompilerPlugin(
|
|
|
1041
1314
|
if (compilerPluginIndex !== -1) {
|
|
1042
1315
|
// preserve automatic worklet candidates before react compiler hoists them.
|
|
1043
1316
|
if (useWorklets) {
|
|
1044
|
-
const prepared = compiler.prepareWorkletsForReactCompiler(
|
|
1317
|
+
const prepared = compiler.prepareWorkletsForReactCompiler(
|
|
1318
|
+
id,
|
|
1319
|
+
curCode,
|
|
1320
|
+
sourceMaps
|
|
1321
|
+
)
|
|
1045
1322
|
if (prepared) {
|
|
1046
1323
|
curCode = prepared.code
|
|
1047
1324
|
if (prepared.map) intermediateMaps.push(prepared.map)
|
|
@@ -1050,7 +1327,10 @@ export function vxrnCompilerPlugin(
|
|
|
1050
1327
|
const compilerTarget =
|
|
1051
1328
|
(babelOptions!.plugins![compilerPluginIndex] as any[])[1]?.target ?? '19'
|
|
1052
1329
|
const compilerOut = await compiler.transformOxcReactCompiler(
|
|
1053
|
-
id,
|
|
1330
|
+
id,
|
|
1331
|
+
curCode,
|
|
1332
|
+
compilerTarget,
|
|
1333
|
+
sourceMaps
|
|
1054
1334
|
)
|
|
1055
1335
|
if (compilerOut?.code) {
|
|
1056
1336
|
curCode = compilerOut.code
|
|
@@ -1541,7 +1821,10 @@ export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin
|
|
|
1541
1821
|
const err = result.errors[0]
|
|
1542
1822
|
throw new Error(err.message + (err.codeframe ? `\n${err.codeframe}` : ''))
|
|
1543
1823
|
}
|
|
1544
|
-
output = {
|
|
1824
|
+
output = {
|
|
1825
|
+
code: result.code,
|
|
1826
|
+
map: sourceMaps ? result.map : undefined,
|
|
1827
|
+
}
|
|
1545
1828
|
}
|
|
1546
1829
|
const lowered = await transformHermesAsync(output?.code ?? code, id, sourceMaps)
|
|
1547
1830
|
if (!lowered) return output
|
|
@@ -1581,7 +1864,8 @@ export function hermesLoopsPlugin(sourceMaps = false): Plugin {
|
|
|
1581
1864
|
if (!result) return
|
|
1582
1865
|
|
|
1583
1866
|
if (!sourceMaps) return { code: result.code, map: null }
|
|
1584
|
-
if (result.maps.length === 1)
|
|
1867
|
+
if (result.maps.length === 1)
|
|
1868
|
+
return { code: result.code, map: result.maps[0] as any }
|
|
1585
1869
|
const remapping = (await import('@jridgewell/remapping')).default
|
|
1586
1870
|
return {
|
|
1587
1871
|
code: result.code,
|
|
@@ -1785,9 +2069,44 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
|
1785
2069
|
}
|
|
1786
2070
|
}
|
|
1787
2071
|
|
|
2072
|
+
reload() {
|
|
2073
|
+
var proxy = globalThis.__turboModuleProxy
|
|
2074
|
+
? globalThis.__turboModuleProxy('DevSettings')
|
|
2075
|
+
: globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.DevSettings;
|
|
2076
|
+
if (proxy && proxy.reload) proxy.reload();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1788
2079
|
setup(socket) {
|
|
1789
2080
|
if (this._socket) return;
|
|
1790
2081
|
this._socket = socket;
|
|
2082
|
+
var runtime = this;
|
|
2083
|
+
// addEventListener rather than socket.onmessage: the bundle's WebSocket is
|
|
2084
|
+
// whatever the host platform provides, and a host that does not honor on*
|
|
2085
|
+
// property assignment would otherwise leave the app permanently without
|
|
2086
|
+
// Fast Refresh and say nothing.
|
|
2087
|
+
socket.addEventListener('message', function(event) {
|
|
2088
|
+
var message;
|
|
2089
|
+
try {
|
|
2090
|
+
message = JSON.parse(event.data);
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
try {
|
|
2095
|
+
if (message.type === 'hmr:update') {
|
|
2096
|
+
// a patch that cannot be applied in place is the reload signal
|
|
2097
|
+
if (!runtime.applyHmrUpdate(message.code, message.changedIds, message.seq)) {
|
|
2098
|
+
runtime.reload();
|
|
2099
|
+
}
|
|
2100
|
+
} else if (message.type === 'hmr:reload') {
|
|
2101
|
+
runtime.reload();
|
|
2102
|
+
}
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
console.error('[vxrn HMR]: failed to apply update', error);
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
socket.addEventListener('error', function(event) {
|
|
2108
|
+
console.warn('[vxrn HMR]: connection error', (event && event.message) || event);
|
|
2109
|
+
});
|
|
1791
2110
|
}
|
|
1792
2111
|
}
|
|
1793
2112
|
|