vxrn 1.25.8 → 1.25.9
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/config/getViteServerConfig.mjs +6 -0
- package/dist/config/getViteServerConfig.mjs.map +1 -1
- package/dist/config/getViteServerConfig.native.js +6 -0
- package/dist/config/getViteServerConfig.native.js.map +1 -1
- package/dist/config/getViteServerConfig.test.mjs +26 -0
- package/dist/config/getViteServerConfig.test.mjs.map +1 -0
- package/dist/config/getViteServerConfig.test.native.js +26 -0
- package/dist/config/getViteServerConfig.test.native.js.map +1 -0
- package/dist/exports/dev.mjs +26 -1
- package/dist/exports/dev.mjs.map +1 -1
- package/dist/exports/dev.native.js +26 -1
- package/dist/exports/dev.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +80 -7
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +80 -7
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/runtime/native-prelude.mjs +7 -0
- package/dist/runtime/native-prelude.mjs.map +1 -1
- package/dist/runtime/native-prelude.native.js +7 -0
- package/dist/runtime/native-prelude.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.mjs +302 -197
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +410 -221
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.test.mjs +536 -6
- package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.test.native.js +559 -6
- package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
- package/package.json +11 -11
- package/src/config/getViteServerConfig.test.ts +29 -0
- package/src/config/getViteServerConfig.ts +11 -0
- package/src/exports/dev.ts +34 -3
- package/src/plugins/reactNativeDevServer.ts +89 -12
- package/src/runtime/native-prelude.ts +7 -0
- package/src/utils/createNativeDevEngine.test.ts +682 -5
- package/src/utils/createNativeDevEngine.ts +516 -229
- package/types/config/getViteServerConfig.d.ts.map +1 -1
- package/types/config/getViteServerConfig.test.d.ts +2 -0
- package/types/config/getViteServerConfig.test.d.ts.map +1 -0
- package/types/exports/dev.d.ts.map +1 -1
- package/types/plugins/reactNativeDevServer.d.ts +1 -0
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/runtime/native-prelude.d.ts.map +1 -1
- package/types/utils/createNativeDevEngine.d.ts +40 -2
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
|
@@ -6,12 +6,20 @@
|
|
|
6
6
|
* https://github.com/leegeunhyeok/rollipop
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from 'node:crypto'
|
|
9
10
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
copyFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
} from 'node:fs'
|
|
11
19
|
import { pathToFileURL } from 'node:url'
|
|
12
20
|
import type { InputOptions, OutputOptions, Plugin, RolldownOutput } from 'rolldown'
|
|
13
21
|
import type { DevEngine } from 'rolldown/experimental'
|
|
14
|
-
import { normalizePath } from 'vite'
|
|
22
|
+
import { loadEnv as loadViteEnv, normalizePath } from 'vite'
|
|
15
23
|
import { DEFAULT_ASSET_EXTS } from '../constants/defaults'
|
|
16
24
|
import { getNativePrelude } from '../runtime/native-prelude'
|
|
17
25
|
|
|
@@ -28,18 +36,21 @@ const FLOW_FILE_PATTERN = /node_modules[\\/](?:react-native|@react-native)[\\/].
|
|
|
28
36
|
const HERMES_CLASS_TRANSFORMS = [
|
|
29
37
|
'transform-classes',
|
|
30
38
|
'transform-parameters',
|
|
39
|
+
'transform-block-scoping',
|
|
31
40
|
'transform-class-properties',
|
|
32
41
|
'transform-class-static-block',
|
|
33
42
|
'transform-private-methods',
|
|
34
43
|
'transform-private-property-in-object',
|
|
35
44
|
] as const
|
|
36
45
|
|
|
37
|
-
//
|
|
38
|
-
|
|
46
|
+
// Hermes V1 rejects async generators in both the dev interpreter and AOT
|
|
47
|
+
// compilation. Keep the async lowering identical across modes: a dev bundle
|
|
48
|
+
// that parses in Rolldown but redboxes in Hermes is not a usable build.
|
|
49
|
+
const HERMES_ASYNC_TRANSFORMS = ['transform-async-to-generator'] as const
|
|
39
50
|
|
|
40
51
|
/** SWC `env.include` for Hermes-compatible downleveling; see HERMES_CLASS_TRANSFORMS. */
|
|
41
52
|
export function getHermesSWCIncludes(dev: boolean): string[] {
|
|
42
|
-
return [...HERMES_CLASS_TRANSFORMS, ...
|
|
53
|
+
return [...HERMES_CLASS_TRANSFORMS, ...HERMES_ASYNC_TRANSFORMS]
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
interface NativeDevEngineOptions {
|
|
@@ -66,6 +77,7 @@ export type NativeHmrUpdate =
|
|
|
66
77
|
interface NativeDevEngineResult {
|
|
67
78
|
engine: DevEngine
|
|
68
79
|
getBundle: () => Promise<{ code: string }>
|
|
80
|
+
getAsset: (pathname: string, hash?: string) => NativeDevAsset | undefined
|
|
69
81
|
close: () => Promise<void>
|
|
70
82
|
}
|
|
71
83
|
|
|
@@ -84,7 +96,11 @@ function getResolveExtensions(platform: 'ios' | 'android'): string[] {
|
|
|
84
96
|
function getNativeResolveConfig(platform: 'ios' | 'android') {
|
|
85
97
|
return {
|
|
86
98
|
extensions: getResolveExtensions(platform),
|
|
87
|
-
|
|
99
|
+
// Rolldown supplies `import`, `require`, and `default` contextually. Adding
|
|
100
|
+
// both here makes a CommonJS require eligible for an export map's ESM
|
|
101
|
+
// `import` target (for example @babel/runtime), which turns callable CJS
|
|
102
|
+
// helpers into namespace objects at runtime.
|
|
103
|
+
conditionNames: ['react-native'],
|
|
88
104
|
mainFields: ['react-native', 'module', 'main'],
|
|
89
105
|
}
|
|
90
106
|
}
|
|
@@ -120,37 +136,19 @@ export function getNativeTransformConfig(
|
|
|
120
136
|
}
|
|
121
137
|
})()
|
|
122
138
|
|
|
123
|
-
// load .env files for VITE_* variables (mirrors what Vite does)
|
|
124
|
-
const envDefines = (() => {
|
|
125
|
-
const defines: Record<string, string> = {}
|
|
126
|
-
try {
|
|
127
|
-
const mode = dev ? 'development' : 'production'
|
|
128
|
-
// load .env, .env.local, .env.[mode], .env.[mode].local (same order as Vite)
|
|
129
|
-
for (const envFile of [
|
|
130
|
-
'.env',
|
|
131
|
-
'.env.local',
|
|
132
|
-
`.env.${mode}`,
|
|
133
|
-
`.env.${mode}.local`,
|
|
134
|
-
]) {
|
|
135
|
-
const envPath = join(root, envFile)
|
|
136
|
-
if (!existsSync(envPath)) continue
|
|
137
|
-
const content = readFileSync(envPath, 'utf8')
|
|
138
|
-
for (const line of content.split('\n')) {
|
|
139
|
-
const match = line.match(/^\s*(VITE_\w+)\s*=\s*(.*)$/)
|
|
140
|
-
if (match) {
|
|
141
|
-
const [, key, rawVal] = match
|
|
142
|
-
const val = rawVal.replace(/^['"]|['"]$/g, '').trim()
|
|
143
|
-
defines[`import.meta.env.${key}`] = JSON.stringify(val)
|
|
144
|
-
defines[`process.env.${key}`] = JSON.stringify(val)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} catch {}
|
|
149
|
-
return defines
|
|
150
|
-
})()
|
|
151
|
-
|
|
152
139
|
const mode = dev ? 'development' : 'production'
|
|
153
140
|
|
|
141
|
+
// Match One's Vite client contract: load public values from process.env and
|
|
142
|
+
// the mode-specific env files, with shell values taking precedence. Native
|
|
143
|
+
// apps commonly use Expo's EXPO_PUBLIC_ prefix; accepting only VITE_ here
|
|
144
|
+
// made the same source silently receive `undefined` after leaving Metro.
|
|
145
|
+
const publicEnv = loadViteEnv(mode, root, ['VITE_', 'EXPO_PUBLIC_'])
|
|
146
|
+
const envDefines: Record<string, string> = {}
|
|
147
|
+
for (const [key, value] of Object.entries(publicEnv)) {
|
|
148
|
+
envDefines[`import.meta.env.${key}`] = JSON.stringify(value)
|
|
149
|
+
envDefines[`process.env.${key}`] = JSON.stringify(value)
|
|
150
|
+
}
|
|
151
|
+
|
|
154
152
|
// build the full import.meta.env object for when it's used as a whole (e.g. JSON.stringify(import.meta.env))
|
|
155
153
|
const envObject: Record<string, any> = {
|
|
156
154
|
MODE: mode,
|
|
@@ -163,7 +161,7 @@ export function getNativeTransformConfig(
|
|
|
163
161
|
TAMAGUI_TARGET: 'native',
|
|
164
162
|
TAMAGUI_ENVIRONMENT: platform,
|
|
165
163
|
}
|
|
166
|
-
// add
|
|
164
|
+
// add public values from the shell and mode-specific env files
|
|
167
165
|
for (const [key, val] of Object.entries(envDefines)) {
|
|
168
166
|
const match = key.match(/^import\.meta\.env\.(.+)$/)
|
|
169
167
|
if (match) {
|
|
@@ -218,7 +216,9 @@ function getNativePlugins(
|
|
|
218
216
|
platform: string,
|
|
219
217
|
viteImportGlobPlugin: any,
|
|
220
218
|
dev: boolean,
|
|
221
|
-
assetsDest?: string
|
|
219
|
+
assetsDest?: string,
|
|
220
|
+
onAsset?: (asset: NativeAssetData) => void,
|
|
221
|
+
sourceMaps = false
|
|
222
222
|
): Plugin[] {
|
|
223
223
|
return [
|
|
224
224
|
// plugins provided by One (clientTreeShakePlugin for loader removal, etc.)
|
|
@@ -240,7 +240,7 @@ function getNativePlugins(
|
|
|
240
240
|
// flowStripPlugin so react-native's Flow `.js` specs reach codegen with their
|
|
241
241
|
// type argument intact. stripping Flow first would erase it (which is why the
|
|
242
242
|
// codegen "didn't run for <Component>" warning fired).
|
|
243
|
-
vxrnCompilerPlugin(platform, dev, root),
|
|
243
|
+
vxrnCompilerPlugin(platform, dev, root, sourceMaps),
|
|
244
244
|
// strip Flow from any react-native / @react-native `.js` the compiler didn't
|
|
245
245
|
// handle, the guaranteed safety net before rolldown's oxc core parse (which
|
|
246
246
|
// can't parse Flow). now downstream of the compiler, so codegen sees the types.
|
|
@@ -248,9 +248,9 @@ function getNativePlugins(
|
|
|
248
248
|
// guard undefined native methods in NativeAnimatedHelper
|
|
249
249
|
nativeAnimatedGuardPlugin(),
|
|
250
250
|
// handle asset imports (.png, .jpg, .ttf, etc.)
|
|
251
|
-
assetPlugin({ root, platform, assetsDest }),
|
|
251
|
+
assetPlugin({ root, platform, assetsDest, onAsset }),
|
|
252
252
|
// hermes compat: transform class properties and private fields
|
|
253
|
-
hermesCompatSWCPlugin(dev),
|
|
253
|
+
hermesCompatSWCPlugin(dev, sourceMaps),
|
|
254
254
|
]
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -271,7 +271,25 @@ function getNativeOutputOptions(prelude: string, sourcemap: boolean): OutputOpti
|
|
|
271
271
|
* - VXRN_REACT_19 → handled by define in getNativeTransformConfig
|
|
272
272
|
* - DevSettings stripping → stripDevSettingsPlugin
|
|
273
273
|
*/
|
|
274
|
+
export function normalizeNativeCommonJSInterop(code: string): string {
|
|
275
|
+
// Rolldown can mark ESM default imports from internal CommonJS modules as
|
|
276
|
+
// Node-mode conversions in both dev and production. Babel CommonJS packages
|
|
277
|
+
// expose their actual default behind `exports.default`; Node mode instead
|
|
278
|
+
// returns the whole exports object and React receives `{ default: Component }`.
|
|
279
|
+
return code.replace(
|
|
280
|
+
/(\b__toESM(?:\$\d+)?\(\s*require[\w$]*\(\)\s*),\s*1(\s*\))/g,
|
|
281
|
+
'$1$2'
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
274
285
|
function postProcessNativeBundle(code: string): string {
|
|
286
|
+
code = normalizeNativeCommonJSInterop(code)
|
|
287
|
+
|
|
288
|
+
// Rolldown replaces import.meta.env reads but can leave a guarding
|
|
289
|
+
// `typeof import.meta` expression behind. Hermes rejects import.meta syntax
|
|
290
|
+
// even when the other side of the condition has already folded to false.
|
|
291
|
+
code = code.replace(/\btypeof\s+import\.meta\b/g, '"object"')
|
|
292
|
+
|
|
275
293
|
// rolldown devMode still emits ESM export statements that hermes can't parse.
|
|
276
294
|
// this is a rolldown behavior we can't configure away yet.
|
|
277
295
|
code = code.replace(/^\s*export\s*\{[^}]*\}\s*;?\s*$/gm, '')
|
|
@@ -357,36 +375,51 @@ async function downlevelClassFieldsInBundle(code: string): Promise<string> {
|
|
|
357
375
|
|
|
358
376
|
const runtimeEnd = endIdx + endMarker.length
|
|
359
377
|
const runtimeSection = code.slice(startIdx, runtimeEnd)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
378
|
+
const originalNewlines = runtimeSection.match(/\n/g)?.length ?? 0
|
|
379
|
+
|
|
380
|
+
const swc = await import('@swc/core')
|
|
381
|
+
const result = await swc.transform(runtimeSection, {
|
|
382
|
+
filename: 'rolldown-runtime.js',
|
|
383
|
+
configFile: false,
|
|
384
|
+
swcrc: false,
|
|
385
|
+
sourceMaps: false,
|
|
386
|
+
inputSourceMap: false,
|
|
387
|
+
isModule: false,
|
|
388
|
+
// Compact only Rolldown's generated runtime, then restore its original
|
|
389
|
+
// newline count below. This keeps every application module at the exact
|
|
390
|
+
// generated line recorded by Rolldown's source map while still lowering
|
|
391
|
+
// runtime class fields for Hermes.
|
|
392
|
+
minify: true,
|
|
393
|
+
env: {
|
|
394
|
+
targets: { node: 9999 },
|
|
395
|
+
// dev-only runtime prelude: the class set only, no prod bytecode transforms
|
|
396
|
+
include: [...HERMES_CLASS_TRANSFORMS],
|
|
397
|
+
},
|
|
398
|
+
jsc: {
|
|
399
|
+
parser: { syntax: 'ecmascript' },
|
|
400
|
+
transform: { react: { runtime: 'preserve' } },
|
|
401
|
+
externalHelpers: false,
|
|
402
|
+
assumptions: {
|
|
403
|
+
setPublicClassFields: true,
|
|
404
|
+
privateFieldsAsProperties: true,
|
|
383
405
|
},
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
const transformedCode = result.code.trimEnd()
|
|
409
|
+
// SWC minification strips comments, but the dev bundle scope wrapper uses
|
|
410
|
+
// this generated-runtime marker as its structural boundary.
|
|
411
|
+
const transformed = transformedCode.includes(startMarker)
|
|
412
|
+
? transformedCode
|
|
413
|
+
: `${startMarker}\n${transformedCode}`
|
|
414
|
+
const transformedNewlines = transformed.match(/\n/g)?.length ?? 0
|
|
415
|
+
if (transformedNewlines > originalNewlines) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
'[vxrn] Hermes runtime transform added lines and would invalidate the production source map'
|
|
418
|
+
)
|
|
389
419
|
}
|
|
420
|
+
const linePreservingRuntime =
|
|
421
|
+
transformed + '\n'.repeat(originalNewlines - transformedNewlines)
|
|
422
|
+
return code.slice(0, startIdx) + linePreservingRuntime + code.slice(runtimeEnd)
|
|
390
423
|
}
|
|
391
424
|
|
|
392
425
|
export async function createNativeDevEngine(
|
|
@@ -411,6 +444,7 @@ export async function createNativeDevEngine(
|
|
|
411
444
|
platform,
|
|
412
445
|
serverUrl: serverUrl || `http://${host}:${port}`,
|
|
413
446
|
})
|
|
447
|
+
const assetRegistry = createNativeDevAssetRegistry()
|
|
414
448
|
|
|
415
449
|
let currentBundle: { code: string } | null = null
|
|
416
450
|
let firstBuildError: Error | null = null
|
|
@@ -449,7 +483,15 @@ export async function createNativeDevEngine(
|
|
|
449
483
|
|
|
450
484
|
plugins: [
|
|
451
485
|
nativeVirtualEntryPlugin(root, { dev: true }),
|
|
452
|
-
...getNativePlugins(
|
|
486
|
+
...getNativePlugins(
|
|
487
|
+
root,
|
|
488
|
+
platform,
|
|
489
|
+
viteImportGlobPlugin,
|
|
490
|
+
true,
|
|
491
|
+
undefined,
|
|
492
|
+
assetRegistry.register,
|
|
493
|
+
false
|
|
494
|
+
),
|
|
453
495
|
...userPlugins,
|
|
454
496
|
],
|
|
455
497
|
}
|
|
@@ -585,20 +627,21 @@ try {
|
|
|
585
627
|
clearTimeout(timeoutId)
|
|
586
628
|
reject(error)
|
|
587
629
|
}
|
|
588
|
-
timeoutId = setTimeout(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
},
|
|
595
|
-
120_000
|
|
596
|
-
)
|
|
630
|
+
timeoutId = setTimeout(() => {
|
|
631
|
+
bundleResolve = null
|
|
632
|
+
bundleReject = null
|
|
633
|
+
bundlePromise = null
|
|
634
|
+
reject(new Error('[vxrn] bundle build timed out after 120s'))
|
|
635
|
+
}, 120_000)
|
|
597
636
|
})
|
|
598
637
|
}
|
|
599
638
|
return bundlePromise
|
|
600
639
|
},
|
|
601
640
|
|
|
641
|
+
getAsset(pathname, hash) {
|
|
642
|
+
return assetRegistry.resolve(pathname, hash)
|
|
643
|
+
},
|
|
644
|
+
|
|
602
645
|
async close() {
|
|
603
646
|
await engine.close()
|
|
604
647
|
},
|
|
@@ -664,7 +707,15 @@ export async function buildNativeBundle(
|
|
|
664
707
|
moduleTypes: { '.js': 'jsx' },
|
|
665
708
|
plugins: [
|
|
666
709
|
...(entryFile ? [] : [nativeVirtualEntryPlugin(root, { dev })]),
|
|
667
|
-
...getNativePlugins(
|
|
710
|
+
...getNativePlugins(
|
|
711
|
+
root,
|
|
712
|
+
platform,
|
|
713
|
+
viteImportGlobPlugin,
|
|
714
|
+
dev,
|
|
715
|
+
assetsDest,
|
|
716
|
+
undefined,
|
|
717
|
+
sourcemap
|
|
718
|
+
),
|
|
668
719
|
...userPlugins,
|
|
669
720
|
],
|
|
670
721
|
output: getNativeOutputOptions(prelude, sourcemap),
|
|
@@ -677,11 +728,10 @@ export async function buildNativeBundle(
|
|
|
677
728
|
|
|
678
729
|
let code = postProcessNativeBundle(chunk.code)
|
|
679
730
|
code = await downlevelClassFieldsInBundle(code)
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
// not for exact line/column symbolication.
|
|
731
|
+
// Per-module Babel/SWC transforms return maps when requested, and the
|
|
732
|
+
// generated-runtime downlevel pass preserves its input line count. The
|
|
733
|
+
// remaining post-processing replaces syntax in place, so application frames
|
|
734
|
+
// retain Rolldown's generated line and compose back to original source.
|
|
685
735
|
return { code, map: sourcemap ? chunk.map?.toString() : undefined }
|
|
686
736
|
}
|
|
687
737
|
|
|
@@ -791,18 +841,32 @@ createApp({
|
|
|
791
841
|
* nullthrows(NativeAnimatedModule)[methodName] which returns undefined, then
|
|
792
842
|
* method(...args) throws "undefined is not a function".
|
|
793
843
|
*/
|
|
794
|
-
function nativeAnimatedGuardPlugin(): Plugin {
|
|
844
|
+
export function nativeAnimatedGuardPlugin(): Plugin {
|
|
795
845
|
return {
|
|
796
846
|
name: 'vxrn:native-animated-guard',
|
|
797
847
|
transform(code, id) {
|
|
798
848
|
if (!id.includes('animated/NativeAnimatedHelper')) return
|
|
799
849
|
const target = 'const method = nullthrows(NativeAnimatedModule)[methodName];'
|
|
800
850
|
if (!code.includes(target)) return
|
|
851
|
+
const transformed = code.replace(
|
|
852
|
+
target,
|
|
853
|
+
`${target} if (typeof method !== 'function') return;`
|
|
854
|
+
)
|
|
801
855
|
return {
|
|
802
|
-
code:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
856
|
+
code: transformed,
|
|
857
|
+
// this transform deliberately preserves line count. Give Rolldown a
|
|
858
|
+
// line-identity map so it can compose the earlier compiler map instead
|
|
859
|
+
// of dropping source-map coverage for this module.
|
|
860
|
+
map: {
|
|
861
|
+
version: 3,
|
|
862
|
+
sources: [id],
|
|
863
|
+
sourcesContent: [code],
|
|
864
|
+
names: [],
|
|
865
|
+
mappings: code
|
|
866
|
+
.split('\n')
|
|
867
|
+
.map((_, index) => (index === 0 ? 'AAAA' : 'AACA'))
|
|
868
|
+
.join(';'),
|
|
869
|
+
},
|
|
806
870
|
}
|
|
807
871
|
},
|
|
808
872
|
}
|
|
@@ -925,7 +989,8 @@ function cssStubPlugin(): Plugin {
|
|
|
925
989
|
export function vxrnCompilerPlugin(
|
|
926
990
|
platform: string,
|
|
927
991
|
dev: boolean,
|
|
928
|
-
projectRoot = process.cwd()
|
|
992
|
+
projectRoot = process.cwd(),
|
|
993
|
+
sourceMaps = false
|
|
929
994
|
): Plugin {
|
|
930
995
|
let compiler: typeof import('@vxrn/compiler') | null = null
|
|
931
996
|
|
|
@@ -944,51 +1009,62 @@ export function vxrnCompilerPlugin(
|
|
|
944
1009
|
|
|
945
1010
|
const needsRefresh = isRefreshCandidate(id)
|
|
946
1011
|
|
|
947
|
-
|
|
948
|
-
if (!compiler) compiler = await import('@vxrn/compiler')
|
|
949
|
-
|
|
950
|
-
const props = {
|
|
951
|
-
id,
|
|
952
|
-
code,
|
|
953
|
-
projectRoot,
|
|
954
|
-
development: dev,
|
|
955
|
-
environment: platform as 'ios' | 'android',
|
|
956
|
-
reactForRNVersion: '19' as const,
|
|
957
|
-
}
|
|
1012
|
+
if (!compiler) compiler = await import('@vxrn/compiler')
|
|
958
1013
|
|
|
959
|
-
|
|
1014
|
+
const props = {
|
|
1015
|
+
id,
|
|
1016
|
+
code,
|
|
1017
|
+
projectRoot,
|
|
1018
|
+
development: dev,
|
|
1019
|
+
environment: platform as 'ios' | 'android',
|
|
1020
|
+
reactForRNVersion: '19' as const,
|
|
1021
|
+
}
|
|
960
1022
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1023
|
+
let babelOptions = compiler.getBabelOptions(props)
|
|
1024
|
+
|
|
1025
|
+
if (needsRefresh) {
|
|
1026
|
+
// merge react-refresh/babel into the existing plugins (or create new options)
|
|
1027
|
+
const existingPlugins = babelOptions?.plugins || []
|
|
1028
|
+
babelOptions = {
|
|
1029
|
+
...babelOptions,
|
|
1030
|
+
plugins: [
|
|
1031
|
+
...existingPlugins,
|
|
1032
|
+
[
|
|
1033
|
+
'react-refresh/babel',
|
|
1034
|
+
{
|
|
1035
|
+
skipEnvCheck: true,
|
|
1036
|
+
refreshReg: '__vxrnRefreshReg',
|
|
1037
|
+
refreshSig: '__vxrnRefreshSig',
|
|
1038
|
+
},
|
|
976
1039
|
],
|
|
977
|
-
|
|
1040
|
+
],
|
|
978
1041
|
}
|
|
1042
|
+
}
|
|
979
1043
|
|
|
980
|
-
|
|
1044
|
+
if (!babelOptions) return
|
|
1045
|
+
|
|
1046
|
+
if (sourceMaps) {
|
|
1047
|
+
// Rolldown composes plugin maps in transform order. Without this map,
|
|
1048
|
+
// every module Babel changes is attributed only to Babel's generated
|
|
1049
|
+
// output, which makes production Hermes frames miss the original
|
|
1050
|
+
// source line even though the bundle command emits a `.map` file.
|
|
1051
|
+
babelOptions = {
|
|
1052
|
+
...babelOptions,
|
|
1053
|
+
sourceMaps: true,
|
|
1054
|
+
sourceFileName: id,
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
981
1057
|
|
|
982
|
-
|
|
1058
|
+
const result = await compiler.transformBabel(id, code, babelOptions)
|
|
983
1059
|
|
|
984
|
-
|
|
985
|
-
|
|
1060
|
+
if (result?.code) {
|
|
1061
|
+
let out = result.code
|
|
986
1062
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1063
|
+
if (needsRefresh) {
|
|
1064
|
+
// wrap with per-file $RefreshReg$ that includes the file path as unique ID
|
|
1065
|
+
// and schedule performReactRefresh() after HMR patch re-execution
|
|
1066
|
+
const escapedId = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
1067
|
+
out = `
|
|
992
1068
|
var __prevRefreshReg = globalThis.$RefreshReg$;
|
|
993
1069
|
var __prevRefreshSig = globalThis.$RefreshSig$;
|
|
994
1070
|
if (globalThis.__ReactRefresh) {
|
|
@@ -1006,28 +1082,16 @@ ${out}
|
|
|
1006
1082
|
globalThis.$RefreshReg$ = __prevRefreshReg;
|
|
1007
1083
|
globalThis.$RefreshSig$ = __prevRefreshSig;
|
|
1008
1084
|
if (import.meta.hot) {
|
|
1009
|
-
import.meta.hot.
|
|
1085
|
+
import.meta.hot.acceptReactRefresh(function() {
|
|
1010
1086
|
if (globalThis.__ReactRefresh) {
|
|
1011
1087
|
setTimeout(function() { globalThis.__ReactRefresh.performReactRefresh(); }, 30);
|
|
1012
1088
|
}
|
|
1013
1089
|
});
|
|
1014
1090
|
}
|
|
1015
1091
|
`
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
return { code: out }
|
|
1019
|
-
}
|
|
1020
|
-
} catch (err: any) {
|
|
1021
|
-
// log but don't crash — fallback to rolldown's own transform
|
|
1022
|
-
if (dev) {
|
|
1023
|
-
console.warn(`[vxrn:compiler] ${id}: ${err.message || err}`)
|
|
1024
|
-
}
|
|
1025
|
-
// if babel transform fails for a refresh candidate, still add accept boundary
|
|
1026
|
-
if (needsRefresh) {
|
|
1027
|
-
return {
|
|
1028
|
-
code: code + `\nif (import.meta.hot) { import.meta.hot.accept(); }\n`,
|
|
1029
|
-
}
|
|
1030
1092
|
}
|
|
1093
|
+
|
|
1094
|
+
return { code: out, map: sourceMaps ? result.map : undefined }
|
|
1031
1095
|
}
|
|
1032
1096
|
},
|
|
1033
1097
|
}
|
|
@@ -1044,20 +1108,16 @@ function flowStripPlugin(): Plugin {
|
|
|
1044
1108
|
async handler(code, id) {
|
|
1045
1109
|
if (!FLOW_FILE_PATTERN.test(id)) return
|
|
1046
1110
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
return { code: result.code, map: result.map }
|
|
1058
|
-
} catch (err: any) {
|
|
1059
|
-
console.warn(`[vxrn:flow-strip] ${id}: ${err.message}`)
|
|
1060
|
-
}
|
|
1111
|
+
const fft = await import('fast-flow-transform')
|
|
1112
|
+
const result = await fft.default({
|
|
1113
|
+
filename: id,
|
|
1114
|
+
source: code,
|
|
1115
|
+
sourcemap: true,
|
|
1116
|
+
dialect: 'flow',
|
|
1117
|
+
format: 'pretty',
|
|
1118
|
+
})
|
|
1119
|
+
// don't set moduleType - let rolldown's global moduleTypes config handle it
|
|
1120
|
+
return { code: result.code, map: result.map }
|
|
1061
1121
|
},
|
|
1062
1122
|
},
|
|
1063
1123
|
}
|
|
@@ -1071,6 +1131,7 @@ function assetPlugin(opts: {
|
|
|
1071
1131
|
root: string
|
|
1072
1132
|
platform: string
|
|
1073
1133
|
assetsDest?: string
|
|
1134
|
+
onAsset?: (asset: NativeAssetData) => void
|
|
1074
1135
|
}): Plugin {
|
|
1075
1136
|
const assetRegex = new RegExp(`\\.(?:${DEFAULT_ASSET_EXTS.join('|')})$`)
|
|
1076
1137
|
|
|
@@ -1080,41 +1141,11 @@ function assetPlugin(opts: {
|
|
|
1080
1141
|
async handler(id) {
|
|
1081
1142
|
if (!assetRegex.test(id)) return
|
|
1082
1143
|
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
const dir = dirname(id)
|
|
1086
|
-
const relativePath = relative(opts.root, id)
|
|
1087
|
-
// On Windows, change backslashes to slashes to get proper URL path from file path.
|
|
1088
|
-
const httpLocation = '/assets/' + dirname(relativePath).replace(/\\/g, '/')
|
|
1089
|
-
|
|
1090
|
-
// simple asset registration (TODO: scale detection like rollipop)
|
|
1091
|
-
const assetData = {
|
|
1092
|
-
__packager_asset: true,
|
|
1093
|
-
name,
|
|
1094
|
-
type: ext,
|
|
1095
|
-
scales: [1],
|
|
1096
|
-
httpServerLocation: httpLocation,
|
|
1097
|
-
fileSystemLocation: dir,
|
|
1098
|
-
hash: '',
|
|
1099
|
-
width: undefined as number | undefined,
|
|
1100
|
-
height: undefined as number | undefined,
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// try to get image dimensions
|
|
1104
|
-
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(ext)) {
|
|
1105
|
-
try {
|
|
1106
|
-
const { imageSize } = await import('image-size')
|
|
1107
|
-
const dims = imageSize(id)
|
|
1108
|
-
assetData.width = dims.width
|
|
1109
|
-
assetData.height = dims.height
|
|
1110
|
-
} catch {}
|
|
1111
|
-
}
|
|
1144
|
+
const assetData = await getNativeAssetData(id, opts.root, opts.platform)
|
|
1145
|
+
opts.onAsset?.(assetData)
|
|
1112
1146
|
|
|
1113
1147
|
if (opts.assetsDest) {
|
|
1114
|
-
|
|
1115
|
-
const assetDestDir = join(opts.assetsDest, 'assets', relativeAssetDir)
|
|
1116
|
-
mkdirSync(assetDestDir, { recursive: true })
|
|
1117
|
-
copyFileSync(id, join(assetDestDir, `${name}.${ext}`))
|
|
1148
|
+
copyNativeAssetFiles(assetData, opts.assetsDest, opts.platform)
|
|
1118
1149
|
}
|
|
1119
1150
|
|
|
1120
1151
|
const code = `module.exports = require('react-native/Libraries/Image/AssetRegistry').registerAsset(${JSON.stringify(assetData)});`
|
|
@@ -1125,12 +1156,243 @@ function assetPlugin(opts: {
|
|
|
1125
1156
|
}
|
|
1126
1157
|
}
|
|
1127
1158
|
|
|
1159
|
+
type NativeAssetData = {
|
|
1160
|
+
__packager_asset: true
|
|
1161
|
+
name: string
|
|
1162
|
+
type: string
|
|
1163
|
+
scales: number[]
|
|
1164
|
+
files: string[]
|
|
1165
|
+
httpServerLocation: string
|
|
1166
|
+
fileSystemLocation: string
|
|
1167
|
+
hash: string
|
|
1168
|
+
width?: number
|
|
1169
|
+
height?: number
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
export type NativeDevAsset = {
|
|
1173
|
+
filePath: string
|
|
1174
|
+
hash: string
|
|
1175
|
+
type: string
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
export function createNativeDevAssetRegistry(): {
|
|
1179
|
+
register: (asset: NativeAssetData) => void
|
|
1180
|
+
resolve: (pathname: string, hash?: string) => NativeDevAsset | undefined
|
|
1181
|
+
} {
|
|
1182
|
+
const assets = new Map<string, NativeDevAsset>()
|
|
1183
|
+
|
|
1184
|
+
return {
|
|
1185
|
+
register(asset) {
|
|
1186
|
+
for (const [index, scale] of asset.scales.entries()) {
|
|
1187
|
+
const filePath = asset.files[index]
|
|
1188
|
+
if (!filePath) continue
|
|
1189
|
+
const fileName = `${asset.name}${scale === 1 ? '' : `@${scale}x`}.${asset.type}`
|
|
1190
|
+
assets.set(`${asset.httpServerLocation}/${fileName}`, {
|
|
1191
|
+
filePath,
|
|
1192
|
+
hash: asset.hash,
|
|
1193
|
+
type: asset.type,
|
|
1194
|
+
})
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
|
|
1198
|
+
resolve(pathname, hash) {
|
|
1199
|
+
const asset = assets.get(pathname)
|
|
1200
|
+
if (!asset || (hash !== undefined && hash !== asset.hash)) return
|
|
1201
|
+
return asset
|
|
1202
|
+
},
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
type ParsedNativeAssetName = {
|
|
1207
|
+
name: string
|
|
1208
|
+
scale: number
|
|
1209
|
+
platform: string | undefined
|
|
1210
|
+
type: string
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const NATIVE_IMAGE_EXTS = new Set([
|
|
1214
|
+
'png',
|
|
1215
|
+
'jpg',
|
|
1216
|
+
'jpeg',
|
|
1217
|
+
'gif',
|
|
1218
|
+
'webp',
|
|
1219
|
+
'bmp',
|
|
1220
|
+
'psd',
|
|
1221
|
+
'svg',
|
|
1222
|
+
'tiff',
|
|
1223
|
+
'ktx',
|
|
1224
|
+
])
|
|
1225
|
+
|
|
1226
|
+
function parseNativeAssetName(filePath: string): ParsedNativeAssetName | undefined {
|
|
1227
|
+
const type = extname(filePath).slice(1)
|
|
1228
|
+
if (!type) return
|
|
1229
|
+
|
|
1230
|
+
const stem = basename(filePath, `.${type}`)
|
|
1231
|
+
const platformMatch = stem.match(/^(.*)\.(ios|android)$/)
|
|
1232
|
+
const platform = platformMatch?.[2]
|
|
1233
|
+
const unqualifiedStem = platformMatch?.[1] ?? stem
|
|
1234
|
+
const scaleMatch = unqualifiedStem.match(/^(.+?)(?:@([\d.]+)x)?$/)
|
|
1235
|
+
if (!scaleMatch) return
|
|
1236
|
+
|
|
1237
|
+
const scale = scaleMatch[2] === undefined ? 1 : Number.parseFloat(scaleMatch[2])
|
|
1238
|
+
if (!Number.isFinite(scale) || scale <= 0) return
|
|
1239
|
+
|
|
1240
|
+
return {
|
|
1241
|
+
name: scaleMatch[1],
|
|
1242
|
+
scale,
|
|
1243
|
+
platform,
|
|
1244
|
+
type,
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function getNativeAssetUrlDirectory(root: string, assetDirectory: string): string {
|
|
1249
|
+
const relativeDirectory = relative(
|
|
1250
|
+
realpathSync(root),
|
|
1251
|
+
realpathSync(assetDirectory)
|
|
1252
|
+
).replace(/\\/g, '/')
|
|
1253
|
+
const safeDirectory = relativeDirectory
|
|
1254
|
+
.split('/')
|
|
1255
|
+
.filter((segment) => segment && segment !== '.')
|
|
1256
|
+
.map((segment) => (segment === '..' ? '_' : segment))
|
|
1257
|
+
.join('/')
|
|
1258
|
+
return safeDirectory ? `/assets/${safeDirectory}` : '/assets'
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
export async function getNativeAssetData(
|
|
1262
|
+
id: string,
|
|
1263
|
+
root: string,
|
|
1264
|
+
platform: string
|
|
1265
|
+
): Promise<NativeAssetData> {
|
|
1266
|
+
const requested = parseNativeAssetName(id)
|
|
1267
|
+
if (!requested) {
|
|
1268
|
+
throw new Error(`[vxrn] invalid native asset path: ${id}`)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const assetDirectory = dirname(id)
|
|
1272
|
+
const filesByScale = new Map<number, { file: string; platformSpecific: boolean }>()
|
|
1273
|
+
|
|
1274
|
+
for (const fileName of readdirSync(assetDirectory)) {
|
|
1275
|
+
const candidate = parseNativeAssetName(fileName)
|
|
1276
|
+
if (
|
|
1277
|
+
!candidate ||
|
|
1278
|
+
candidate.name !== requested.name ||
|
|
1279
|
+
candidate.type !== requested.type ||
|
|
1280
|
+
(candidate.platform !== undefined && candidate.platform !== platform)
|
|
1281
|
+
) {
|
|
1282
|
+
continue
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const platformSpecific = candidate.platform === platform
|
|
1286
|
+
const existing = filesByScale.get(candidate.scale)
|
|
1287
|
+
if (!existing || (platformSpecific && !existing.platformSpecific)) {
|
|
1288
|
+
filesByScale.set(candidate.scale, {
|
|
1289
|
+
file: join(assetDirectory, fileName),
|
|
1290
|
+
platformSpecific,
|
|
1291
|
+
})
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const scales = [...filesByScale.keys()].sort((a, b) => a - b)
|
|
1296
|
+
const files = scales.map((scale) => filesByScale.get(scale)!.file)
|
|
1297
|
+
if (files.length === 0) {
|
|
1298
|
+
throw new Error(`[vxrn] native asset has no files for ${platform}: ${id}`)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const hash = createHash('md5')
|
|
1302
|
+
for (const file of files) hash.update(readFileSync(file))
|
|
1303
|
+
|
|
1304
|
+
const assetData: NativeAssetData = {
|
|
1305
|
+
__packager_asset: true,
|
|
1306
|
+
name: requested.name,
|
|
1307
|
+
type: requested.type,
|
|
1308
|
+
scales,
|
|
1309
|
+
files,
|
|
1310
|
+
httpServerLocation: getNativeAssetUrlDirectory(root, assetDirectory),
|
|
1311
|
+
fileSystemLocation: assetDirectory,
|
|
1312
|
+
hash: hash.digest('hex'),
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (NATIVE_IMAGE_EXTS.has(requested.type)) {
|
|
1316
|
+
try {
|
|
1317
|
+
const { imageSize } = await import('image-size')
|
|
1318
|
+
const dims = imageSize(files[0])
|
|
1319
|
+
assetData.width = dims.width === undefined ? undefined : dims.width / scales[0]
|
|
1320
|
+
assetData.height = dims.height === undefined ? undefined : dims.height / scales[0]
|
|
1321
|
+
} catch {}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return assetData
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function getIOSAssetScales(scales: number[]): number[] {
|
|
1328
|
+
const supported = scales.filter((scale) => scale === 1 || scale === 2 || scale === 3)
|
|
1329
|
+
if (supported.length > 0 || scales.length === 0) return supported
|
|
1330
|
+
return [scales.find((scale) => scale > 3) ?? scales[scales.length - 1]]
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function getAndroidAssetDirectory(type: string, scale: number): string {
|
|
1334
|
+
if (
|
|
1335
|
+
!new Set(['gif', 'heic', 'heif', 'jpeg', 'jpg', 'ktx', 'png', 'webp', 'xml']).has(
|
|
1336
|
+
type
|
|
1337
|
+
)
|
|
1338
|
+
) {
|
|
1339
|
+
return 'raw'
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const density = new Map([
|
|
1343
|
+
[0.75, 'ldpi'],
|
|
1344
|
+
[1, 'mdpi'],
|
|
1345
|
+
[1.5, 'hdpi'],
|
|
1346
|
+
[2, 'xhdpi'],
|
|
1347
|
+
[3, 'xxhdpi'],
|
|
1348
|
+
[4, 'xxxhdpi'],
|
|
1349
|
+
]).get(scale)
|
|
1350
|
+
if (density) return `drawable-${density}`
|
|
1351
|
+
return `drawable-${Math.round(scale * 160)}dpi`
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function getAndroidAssetName(asset: NativeAssetData): string {
|
|
1355
|
+
return `${asset.httpServerLocation}/${asset.name}`
|
|
1356
|
+
.toLowerCase()
|
|
1357
|
+
.replace(/^\//, '')
|
|
1358
|
+
.replace(/\//g, '_')
|
|
1359
|
+
.replace(/([^a-z0-9_])/g, '')
|
|
1360
|
+
.replace(/^assets_/, '')
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function copyNativeAssetFiles(
|
|
1364
|
+
asset: NativeAssetData,
|
|
1365
|
+
assetsDest: string,
|
|
1366
|
+
platform: string
|
|
1367
|
+
): void {
|
|
1368
|
+
const validIOSScales = new Set(getIOSAssetScales(asset.scales))
|
|
1369
|
+
|
|
1370
|
+
for (const [index, scale] of asset.scales.entries()) {
|
|
1371
|
+
if (platform === 'ios' && !validIOSScales.has(scale)) continue
|
|
1372
|
+
|
|
1373
|
+
const destination =
|
|
1374
|
+
platform === 'android'
|
|
1375
|
+
? join(
|
|
1376
|
+
assetsDest,
|
|
1377
|
+
getAndroidAssetDirectory(asset.type, scale),
|
|
1378
|
+
`${getAndroidAssetName(asset)}.${asset.type}`
|
|
1379
|
+
)
|
|
1380
|
+
: join(
|
|
1381
|
+
assetsDest,
|
|
1382
|
+
asset.httpServerLocation.slice(1),
|
|
1383
|
+
`${asset.name}${scale === 1 ? '' : `@${scale}x`}.${asset.type}`
|
|
1384
|
+
)
|
|
1385
|
+
mkdirSync(dirname(destination), { recursive: true })
|
|
1386
|
+
copyFileSync(asset.files[index], destination)
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1128
1390
|
/**
|
|
1129
1391
|
* SWC transform for Hermes compatibility.
|
|
1130
1392
|
* Transforms class properties and private fields that Hermes doesn't support.
|
|
1131
1393
|
* Inspired by rollipop's swc-plugin.ts.
|
|
1132
1394
|
*/
|
|
1133
|
-
export function hermesCompatSWCPlugin(dev: boolean): Plugin {
|
|
1395
|
+
export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin {
|
|
1134
1396
|
let swc: typeof import('@swc/core') | null = null
|
|
1135
1397
|
|
|
1136
1398
|
return {
|
|
@@ -1140,43 +1402,43 @@ export function hermesCompatSWCPlugin(dev: boolean): Plugin {
|
|
|
1140
1402
|
if (id.includes('\0') || id.includes('virtual:')) return
|
|
1141
1403
|
// skip files that don't need transformation
|
|
1142
1404
|
const hasClass = code.includes('class ') || code.includes('class{')
|
|
1143
|
-
const hasAsync =
|
|
1144
|
-
|
|
1405
|
+
const hasAsync = code.includes('async ')
|
|
1406
|
+
const hasBlockScopedLoop = /\bfor\s*\(\s*(?:const|let)\b/.test(code)
|
|
1407
|
+
if (!hasClass && !hasAsync && !hasBlockScopedLoop) return
|
|
1145
1408
|
// skip very large prebuilt files
|
|
1146
1409
|
if (code.length > 500_000) return
|
|
1147
1410
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
},
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1411
|
+
if (!swc) swc = await import('@swc/core')
|
|
1412
|
+
|
|
1413
|
+
// app modules: the Hermes class and async sets in both modes.
|
|
1414
|
+
const envIncludes = getHermesSWCIncludes(dev)
|
|
1415
|
+
|
|
1416
|
+
const result = await swc.transform(code, {
|
|
1417
|
+
filename: id,
|
|
1418
|
+
configFile: false,
|
|
1419
|
+
swcrc: false,
|
|
1420
|
+
// Return the transform-to-input map and let Rolldown compose it with
|
|
1421
|
+
// earlier plugin maps. `inputSourceMap` stays false deliberately:
|
|
1422
|
+
// feeding the prior map to SWC as well would compose it twice.
|
|
1423
|
+
sourceMaps,
|
|
1424
|
+
sourceFileName: sourceMaps ? id : undefined,
|
|
1425
|
+
inputSourceMap: false,
|
|
1426
|
+
env: {
|
|
1427
|
+
targets: { node: 9999 },
|
|
1428
|
+
include: envIncludes,
|
|
1429
|
+
},
|
|
1430
|
+
jsc: {
|
|
1431
|
+
parser: { syntax: 'typescript', tsx: true },
|
|
1432
|
+
transform: { react: { runtime: 'preserve' } },
|
|
1433
|
+
externalHelpers: false,
|
|
1434
|
+
assumptions: {
|
|
1435
|
+
setPublicClassFields: true,
|
|
1436
|
+
privateFieldsAsProperties: true,
|
|
1173
1437
|
},
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
// don't crash on SWC transform errors (eg static blocks not supported)
|
|
1179
|
-
}
|
|
1438
|
+
},
|
|
1439
|
+
isModule: !id.endsWith('.cjs'),
|
|
1440
|
+
})
|
|
1441
|
+
return { code: result.code, map: sourceMaps ? result.map : undefined }
|
|
1180
1442
|
},
|
|
1181
1443
|
}
|
|
1182
1444
|
}
|
|
@@ -1201,6 +1463,7 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
|
1201
1463
|
var runtime = this;
|
|
1202
1464
|
var ctx = {
|
|
1203
1465
|
acceptCallbacks: [],
|
|
1466
|
+
reactRefreshAcceptCallback: null,
|
|
1204
1467
|
accept: function(deps, callback) {
|
|
1205
1468
|
if (typeof deps === 'function' || !deps) {
|
|
1206
1469
|
ctx.acceptCallbacks.push({
|
|
@@ -1216,6 +1479,9 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
|
1216
1479
|
ctx.acceptCallbacks.push({ deps: deps, fn: callback || function() {} });
|
|
1217
1480
|
}
|
|
1218
1481
|
},
|
|
1482
|
+
acceptReactRefresh: function(callback) {
|
|
1483
|
+
ctx.reactRefreshAcceptCallback = callback;
|
|
1484
|
+
},
|
|
1219
1485
|
invalidate: function() { runtime.requestReload('module invalidated: ' + moduleId); },
|
|
1220
1486
|
on: function() {},
|
|
1221
1487
|
off: function() {},
|
|
@@ -1249,9 +1515,18 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
|
1249
1515
|
|
|
1250
1516
|
isSelfAccepted(moduleId) {
|
|
1251
1517
|
var ctx = this.moduleHotContexts[moduleId];
|
|
1252
|
-
|
|
1518
|
+
var explicitlyAccepted = !!(ctx && ctx.acceptCallbacks.some(function(callback) {
|
|
1253
1519
|
return callback.deps.indexOf(moduleId) !== -1;
|
|
1254
1520
|
}));
|
|
1521
|
+
if (explicitlyAccepted) return { accepted: true, reactRefresh: false };
|
|
1522
|
+
if (!ctx || !ctx.reactRefreshAcceptCallback || !globalThis.__ReactRefresh) {
|
|
1523
|
+
return { accepted: false, reactRefresh: false };
|
|
1524
|
+
}
|
|
1525
|
+
var exports = this.loadExports(moduleId);
|
|
1526
|
+
return {
|
|
1527
|
+
accepted: ctx.refreshUtils.isReactRefreshBoundary(exports),
|
|
1528
|
+
reactRefresh: true
|
|
1529
|
+
};
|
|
1255
1530
|
}
|
|
1256
1531
|
|
|
1257
1532
|
acceptsDependency(parentId, moduleId) {
|
|
@@ -1266,8 +1541,13 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
|
1266
1541
|
traversed.add(moduleId);
|
|
1267
1542
|
updateSet.add(moduleId);
|
|
1268
1543
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1544
|
+
var selfAcceptance = this.isSelfAccepted(moduleId);
|
|
1545
|
+
if (selfAcceptance.accepted) {
|
|
1546
|
+
boundaries.push({
|
|
1547
|
+
boundary: moduleId,
|
|
1548
|
+
acceptedVia: moduleId,
|
|
1549
|
+
reactRefresh: selfAcceptance.reactRefresh
|
|
1550
|
+
});
|
|
1271
1551
|
return true;
|
|
1272
1552
|
}
|
|
1273
1553
|
|
|
@@ -1303,12 +1583,19 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
|
|
|
1303
1583
|
|
|
1304
1584
|
var callbacks = boundaries.map(function(item) {
|
|
1305
1585
|
var ctx = this.moduleHotContexts[item.boundary];
|
|
1586
|
+
var selectedCallbacks = ctx ? ctx.acceptCallbacks.filter(function(callback) {
|
|
1587
|
+
return callback.deps.indexOf(item.acceptedVia) !== -1;
|
|
1588
|
+
}) : [];
|
|
1589
|
+
if (item.reactRefresh && ctx && ctx.reactRefreshAcceptCallback) {
|
|
1590
|
+
selectedCallbacks.push({
|
|
1591
|
+
deps: [item.acceptedVia],
|
|
1592
|
+
fn: function() { ctx.reactRefreshAcceptCallback(); }
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1306
1595
|
return {
|
|
1307
1596
|
boundary: item.boundary,
|
|
1308
1597
|
acceptedVia: item.acceptedVia,
|
|
1309
|
-
callbacks:
|
|
1310
|
-
return callback.deps.indexOf(item.acceptedVia) !== -1;
|
|
1311
|
-
}) : []
|
|
1598
|
+
callbacks: selectedCallbacks
|
|
1312
1599
|
};
|
|
1313
1600
|
}, this);
|
|
1314
1601
|
|