one 1.24.8 → 1.25.2

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.
Files changed (133) hide show
  1. package/dist/cjs/cli/build.cjs +10 -0
  2. package/dist/cjs/cli/build.native.js +107 -96
  3. package/dist/cjs/cli/build.native.js.map +1 -1
  4. package/dist/cjs/headless-server.cjs +1 -0
  5. package/dist/cjs/headless-server.native.js +5 -4
  6. package/dist/cjs/headless-server.native.js.map +1 -1
  7. package/dist/cjs/server/setupBuildOptions.cjs +8 -0
  8. package/dist/cjs/server/setupBuildOptions.native.js +30 -0
  9. package/dist/cjs/server/setupBuildOptions.native.js.map +1 -1
  10. package/dist/cjs/vite/one.cjs +77 -46
  11. package/dist/cjs/vite/one.native.js +129 -92
  12. package/dist/cjs/vite/one.native.js.map +1 -1
  13. package/dist/cjs/vite/plugins/bundledDevPlugin.cjs +16 -7
  14. package/dist/cjs/vite/plugins/bundledDevPlugin.native.js +16 -7
  15. package/dist/cjs/vite/plugins/bundledDevPlugin.native.js.map +1 -1
  16. package/dist/cjs/vite/plugins/criticalCSSPlugin.cjs +15 -11
  17. package/dist/cjs/vite/plugins/criticalCSSPlugin.native.js +15 -11
  18. package/dist/cjs/vite/plugins/criticalCSSPlugin.native.js.map +1 -1
  19. package/dist/cjs/vite/plugins/criticalCSSPlugin.test.cjs +9 -25
  20. package/dist/cjs/vite/plugins/criticalCSSPlugin.test.native.js +9 -25
  21. package/dist/cjs/vite/plugins/criticalCSSPlugin.test.native.js.map +1 -1
  22. package/dist/cjs/vite/plugins/environmentGuardPlugin.cjs +22 -7
  23. package/dist/cjs/vite/plugins/environmentGuardPlugin.native.js +23 -8
  24. package/dist/cjs/vite/plugins/environmentGuardPlugin.native.js.map +1 -1
  25. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +1 -1
  26. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +1 -1
  27. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  28. package/dist/cjs/vite/plugins/imageDataPlugin.cjs +42 -32
  29. package/dist/cjs/vite/plugins/imageDataPlugin.native.js +42 -32
  30. package/dist/cjs/vite/plugins/imageDataPlugin.native.js.map +1 -1
  31. package/dist/cjs/vite/plugins/imageDataPlugin.test.cjs +18 -38
  32. package/dist/cjs/vite/plugins/imageDataPlugin.test.native.js +18 -38
  33. package/dist/cjs/vite/plugins/imageDataPlugin.test.native.js.map +1 -1
  34. package/dist/cjs/vite/plugins/virtualEntryPlugin.cjs +58 -43
  35. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js +93 -76
  36. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  37. package/dist/cjs/vite/plugins/virtualEntryPlugin.test.cjs +1 -1
  38. package/dist/cjs/vite/plugins/virtualEntryPlugin.test.native.js +1 -1
  39. package/dist/cjs/vite/plugins/virtualEntryPlugin.test.native.js.map +1 -1
  40. package/dist/cjs/vite/plugins/warmRoutesPlugin.cjs +31 -7
  41. package/dist/cjs/vite/plugins/warmRoutesPlugin.native.js +35 -10
  42. package/dist/cjs/vite/plugins/warmRoutesPlugin.native.js.map +1 -1
  43. package/dist/esm/cli/build.mjs +10 -0
  44. package/dist/esm/cli/build.mjs.map +1 -1
  45. package/dist/esm/cli/build.native.js +107 -96
  46. package/dist/esm/cli/build.native.js.map +1 -1
  47. package/dist/esm/getURL.native.test.mjs +1 -1
  48. package/dist/esm/headless-server.mjs +1 -0
  49. package/dist/esm/headless-server.mjs.map +1 -1
  50. package/dist/esm/headless-server.native.js +5 -4
  51. package/dist/esm/headless-server.native.js.map +1 -1
  52. package/dist/esm/router/hmrImport.native.test.mjs +1 -1
  53. package/dist/esm/router/routeHmr.native.test.mjs +1 -1
  54. package/dist/esm/router/routeHmr.native.test.mjs.map +1 -1
  55. package/dist/esm/server/setupBuildOptions.mjs +8 -0
  56. package/dist/esm/server/setupBuildOptions.mjs.map +1 -1
  57. package/dist/esm/server/setupBuildOptions.native.js +30 -0
  58. package/dist/esm/server/setupBuildOptions.native.js.map +1 -1
  59. package/dist/esm/useLoader.native.js +0 -66
  60. package/dist/esm/useLoader.native.js.map +1 -1
  61. package/dist/esm/vite/one.mjs +77 -46
  62. package/dist/esm/vite/one.mjs.map +1 -1
  63. package/dist/esm/vite/one.native.js +129 -92
  64. package/dist/esm/vite/one.native.js.map +1 -1
  65. package/dist/esm/vite/plugins/bundledDevPlugin.mjs +16 -7
  66. package/dist/esm/vite/plugins/bundledDevPlugin.mjs.map +1 -1
  67. package/dist/esm/vite/plugins/bundledDevPlugin.native.js +16 -7
  68. package/dist/esm/vite/plugins/bundledDevPlugin.native.js.map +1 -1
  69. package/dist/esm/vite/plugins/criticalCSSPlugin.mjs +15 -11
  70. package/dist/esm/vite/plugins/criticalCSSPlugin.mjs.map +1 -1
  71. package/dist/esm/vite/plugins/criticalCSSPlugin.native.js +15 -11
  72. package/dist/esm/vite/plugins/criticalCSSPlugin.native.js.map +1 -1
  73. package/dist/esm/vite/plugins/criticalCSSPlugin.test.mjs +9 -25
  74. package/dist/esm/vite/plugins/criticalCSSPlugin.test.mjs.map +1 -1
  75. package/dist/esm/vite/plugins/criticalCSSPlugin.test.native.js +9 -25
  76. package/dist/esm/vite/plugins/criticalCSSPlugin.test.native.js.map +1 -1
  77. package/dist/esm/vite/plugins/environmentGuardPlugin.mjs +22 -7
  78. package/dist/esm/vite/plugins/environmentGuardPlugin.mjs.map +1 -1
  79. package/dist/esm/vite/plugins/environmentGuardPlugin.native.js +23 -8
  80. package/dist/esm/vite/plugins/environmentGuardPlugin.native.js.map +1 -1
  81. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +1 -1
  82. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
  83. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +1 -1
  84. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  85. package/dist/esm/vite/plugins/imageDataPlugin.mjs +42 -32
  86. package/dist/esm/vite/plugins/imageDataPlugin.mjs.map +1 -1
  87. package/dist/esm/vite/plugins/imageDataPlugin.native.js +42 -32
  88. package/dist/esm/vite/plugins/imageDataPlugin.native.js.map +1 -1
  89. package/dist/esm/vite/plugins/imageDataPlugin.test.mjs +18 -38
  90. package/dist/esm/vite/plugins/imageDataPlugin.test.mjs.map +1 -1
  91. package/dist/esm/vite/plugins/imageDataPlugin.test.native.js +18 -38
  92. package/dist/esm/vite/plugins/imageDataPlugin.test.native.js.map +1 -1
  93. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs +58 -43
  94. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs.map +1 -1
  95. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js +93 -76
  96. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  97. package/dist/esm/vite/plugins/virtualEntryPlugin.test.mjs +1 -1
  98. package/dist/esm/vite/plugins/virtualEntryPlugin.test.mjs.map +1 -1
  99. package/dist/esm/vite/plugins/virtualEntryPlugin.test.native.js +1 -1
  100. package/dist/esm/vite/plugins/virtualEntryPlugin.test.native.js.map +1 -1
  101. package/dist/esm/vite/plugins/warmRoutesPlugin.mjs +31 -7
  102. package/dist/esm/vite/plugins/warmRoutesPlugin.mjs.map +1 -1
  103. package/dist/esm/vite/plugins/warmRoutesPlugin.native.js +35 -10
  104. package/dist/esm/vite/plugins/warmRoutesPlugin.native.js.map +1 -1
  105. package/package.json +12 -12
  106. package/src/cli/build.ts +19 -0
  107. package/src/headless-server.ts +1 -0
  108. package/src/server/setupBuildOptions.ts +12 -0
  109. package/src/vite/one.ts +121 -66
  110. package/src/vite/plugins/bundledDevPlugin.ts +17 -7
  111. package/src/vite/plugins/criticalCSSPlugin.test.ts +11 -27
  112. package/src/vite/plugins/criticalCSSPlugin.ts +15 -11
  113. package/src/vite/plugins/environmentGuardPlugin.ts +20 -7
  114. package/src/vite/plugins/fileSystemRouterPlugin.tsx +7 -3
  115. package/src/vite/plugins/imageDataPlugin.test.ts +20 -39
  116. package/src/vite/plugins/imageDataPlugin.ts +49 -44
  117. package/src/vite/plugins/virtualEntryPlugin.test.ts +1 -1
  118. package/src/vite/plugins/virtualEntryPlugin.ts +139 -122
  119. package/src/vite/plugins/warmRoutesPlugin.ts +36 -10
  120. package/src/vite/types.ts +19 -2
  121. package/types/cli/build.d.ts.map +1 -1
  122. package/types/headless-server.d.ts.map +1 -1
  123. package/types/server/setupBuildOptions.d.ts.map +1 -1
  124. package/types/vite/one.d.ts.map +1 -1
  125. package/types/vite/plugins/bundledDevPlugin.d.ts.map +1 -1
  126. package/types/vite/plugins/criticalCSSPlugin.d.ts.map +1 -1
  127. package/types/vite/plugins/environmentGuardPlugin.d.ts.map +1 -1
  128. package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
  129. package/types/vite/plugins/imageDataPlugin.d.ts.map +1 -1
  130. package/types/vite/plugins/virtualEntryPlugin.d.ts.map +1 -1
  131. package/types/vite/plugins/warmRoutesPlugin.d.ts.map +1 -1
  132. package/types/vite/types.d.ts +18 -2
  133. package/types/vite/types.d.ts.map +1 -1
package/src/vite/one.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import events from 'node:events'
9
9
  import { existsSync, readFileSync } from 'node:fs'
10
10
  import path from 'node:path'
11
+ import { exactRegex } from 'rolldown/filter'
11
12
  import { normalizePath, type Plugin, type PluginOption } from 'vite'
12
13
  import { autoDepOptimizePlugin, getOptionsFilled, loadEnv } from 'vxrn'
13
14
  import vxrnVitePlugin from 'vxrn/vite-plugin'
@@ -37,6 +38,11 @@ import type { One } from './types'
37
38
 
38
39
  type MetroOptions = MetroPluginOptions
39
40
 
41
+ // deep react-native Libraries/* paths that don't exist in react-native-web.
42
+ // used as a rust-side resolveId filter so ordinary imports never reach js.
43
+ const RN_DEEP_IMPORT_RE =
44
+ /^react-native\/Libraries\/|react-native-web(-lite)?\/.*\/Libraries\//
45
+
40
46
  /**
41
47
  * This needs a big refactor!
42
48
  * I guess these plugins are all being loaded by native??
@@ -388,6 +394,28 @@ export function one(options: One.PluginOptions = {}): PluginOption {
388
394
  } catch {}
389
395
  }
390
396
 
397
+ // load eagerly so the rust-side hook filter below can be built from
398
+ // the actual tsconfig prefixes. hook filters are fixed at
399
+ // registration time, so waiting for configResolved would mean no
400
+ // filter at all and a napi round trip for every import in the graph
401
+ loadMappings(root)
402
+
403
+ // only ids that could match a mapping reach js. if the eager load
404
+ // found nothing we can't narrow to prefixes, so fall back to "any
405
+ // bare specifier" — tsconfig paths are never relative or absolute,
406
+ // so this still drops every ./ and ../ import while staying correct
407
+ // if configResolved loads mappings later from a different root.
408
+ const tsconfigPathsFilter = mappings.length
409
+ ? new RegExp(
410
+ mappings
411
+ .map(
412
+ (m) =>
413
+ `^${m.prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}${m.wildcard ? '' : '$'}`
414
+ )
415
+ .join('|')
416
+ )
417
+ : /^[^./]/
418
+
391
419
  return {
392
420
  name: 'one:tsconfig-paths',
393
421
  enforce: 'pre',
@@ -404,40 +432,43 @@ export function one(options: One.PluginOptions = {}): PluginOption {
404
432
  }
405
433
  },
406
434
 
407
- resolveId(source: string) {
408
- const jsExts = [
409
- '.ts',
410
- '.tsx',
411
- '.js',
412
- '.jsx',
413
- '.mts',
414
- '.mjs',
415
- '.cjs',
416
- '.cts',
417
- ]
418
- for (const m of mappings) {
419
- let candidate: string | undefined
420
- if (m.wildcard) {
421
- if (source.startsWith(m.prefix)) {
422
- candidate = m.replacement + source.slice(m.prefix.length)
435
+ resolveId: {
436
+ filter: { id: tsconfigPathsFilter },
437
+ handler(source: string) {
438
+ const jsExts = [
439
+ '.ts',
440
+ '.tsx',
441
+ '.js',
442
+ '.jsx',
443
+ '.mts',
444
+ '.mjs',
445
+ '.cjs',
446
+ '.cts',
447
+ ]
448
+ for (const m of mappings) {
449
+ let candidate: string | undefined
450
+ if (m.wildcard) {
451
+ if (source.startsWith(m.prefix)) {
452
+ candidate = m.replacement + source.slice(m.prefix.length)
453
+ }
454
+ } else if (source === m.prefix) {
455
+ candidate = m.replacement
423
456
  }
424
- } else if (source === m.prefix) {
425
- candidate = m.replacement
426
- }
427
- if (!candidate) continue
428
- // already has a js/ts extension
429
- if (jsExts.includes(path.extname(candidate))) return candidate
430
- // try appending extensions
431
- for (const e of jsExts) {
432
- if (existsSync(candidate + e)) return candidate + e
433
- }
434
- // try /index
435
- for (const e of jsExts) {
436
- if (existsSync(candidate + '/index' + e))
437
- return candidate + '/index' + e
457
+ if (!candidate) continue
458
+ // already has a js/ts extension
459
+ if (jsExts.includes(path.extname(candidate))) return candidate
460
+ // try appending extensions
461
+ for (const e of jsExts) {
462
+ if (existsSync(candidate + e)) return candidate + e
463
+ }
464
+ // try /index
465
+ for (const e of jsExts) {
466
+ if (existsSync(candidate + '/index' + e))
467
+ return candidate + '/index' + e
468
+ }
469
+ return candidate
438
470
  }
439
- return candidate
440
- }
471
+ },
441
472
  },
442
473
  } satisfies Plugin
443
474
  })(),
@@ -471,24 +502,40 @@ export function one(options: One.PluginOptions = {}): PluginOption {
471
502
  android: resolveMap(a.android),
472
503
  }
473
504
 
505
+ // every alias is an exact source match, so the union of all keys is
506
+ // an exact rust-side filter. anything else never enters js.
507
+ const aliasKeys = [
508
+ ...new Set(
509
+ Object.values(resolved).flatMap((m) => (m ? Object.keys(m) : []))
510
+ ),
511
+ ]
512
+ const aliasFilter = aliasKeys.length
513
+ ? new RegExp(
514
+ `^(?:${aliasKeys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})$`
515
+ )
516
+ : /(?!)/
517
+
474
518
  return {
475
519
  name: 'one:alias',
476
520
  enforce: 'pre',
477
- resolveId(source) {
478
- const env = this.environment?.name
479
-
480
- // specific env wins over general
481
- const specific = env ? resolved[env as keyof typeof resolved] : null
482
- if (specific && source in specific) {
483
- return { id: specific[source], external: false }
484
- }
521
+ resolveId: {
522
+ filter: { id: aliasFilter },
523
+ handler(source) {
524
+ const env = this.environment?.name
525
+
526
+ // specific env wins over general
527
+ const specific = env ? resolved[env as keyof typeof resolved] : null
528
+ if (specific && source in specific) {
529
+ return { id: specific[source], external: false }
530
+ }
485
531
 
486
- // fall back to general (web/native)
487
- const isWeb = !env || env === 'client' || env === 'ssr'
488
- const general = isWeb ? resolved.web : resolved.native
489
- if (general && source in general) {
490
- return { id: general[source], external: false }
491
- }
532
+ // fall back to general (web/native)
533
+ const isWeb = !env || env === 'client' || env === 'ssr'
534
+ const general = isWeb ? resolved.web : resolved.native
535
+ if (general && source in general) {
536
+ return { id: general[source], external: false }
537
+ }
538
+ },
492
539
  },
493
540
  } satisfies Plugin
494
541
  })(),
@@ -501,21 +548,22 @@ export function one(options: One.PluginOptions = {}): PluginOption {
501
548
  name: 'one:redirect-rn-deep-imports',
502
549
  enforce: 'pre',
503
550
 
504
- resolveId(source) {
505
- if (this.environment?.name === 'client' || this.environment?.name === 'ssr') {
506
- if (
507
- source.startsWith('react-native/Libraries/') ||
508
- /react-native-web(-lite)?\/.*\/Libraries\//.test(source)
509
- ) {
551
+ // rust-side filter matching the two source patterns below, so the ~24k
552
+ // non-matching imports in a build never enter js at all
553
+ resolveId: {
554
+ filter: { id: RN_DEEP_IMPORT_RE },
555
+ handler(source) {
556
+ if (this.environment?.name === 'client' || this.environment?.name === 'ssr') {
510
557
  return '\0rn-empty-module'
511
558
  }
512
- }
559
+ },
513
560
  },
514
561
 
515
- load(id) {
516
- if (id === '\0rn-empty-module') {
562
+ load: {
563
+ filter: { id: exactRegex('\0rn-empty-module') },
564
+ handler() {
517
565
  return 'export default {}; export {};'
518
- }
566
+ },
519
567
  },
520
568
  } satisfies Plugin,
521
569
 
@@ -734,15 +782,20 @@ export function one(options: One.PluginOptions = {}): PluginOption {
734
782
  name: 'one:remove-server-from-client',
735
783
  enforce: 'pre',
736
784
 
737
- transform(code, id) {
738
- if (this.environment.name === 'client') {
739
- if (id.includes(`one-server-only`)) {
740
- return code.replace(
741
- `import { AsyncLocalStorage } from "node:async_hooks"`,
742
- `class AsyncLocalStorage {}`
743
- )
744
- }
745
- }
785
+ // both conditions are rust-side: only one-server-only modules that
786
+ // actually contain the import cross into js
787
+ transform: {
788
+ filter: {
789
+ id: /one-server-only/,
790
+ code: 'import { AsyncLocalStorage } from "node:async_hooks"',
791
+ },
792
+ handler(code) {
793
+ if (this.environment.name !== 'client') return
794
+ return code.replace(
795
+ `import { AsyncLocalStorage } from "node:async_hooks"`,
796
+ `class AsyncLocalStorage {}`
797
+ )
798
+ },
746
799
  },
747
800
  },
748
801
 
@@ -773,7 +826,9 @@ export function one(options: One.PluginOptions = {}): PluginOption {
773
826
  // (source) path. the optimizer doesn't recognize the real path, so it
774
827
  // loads from source — creating a duplicate instance.
775
828
  // this plugin forces optimized SSR deps to resolve via node_modules.
776
- ssrSymlinkDedupPlugin,
829
+ // off by default, and every hook bails on `!dedupeSymlinks`, so skip
830
+ // registering it entirely rather than paying a napi round trip per import
831
+ ...(dedupeSymlinks ? [ssrSymlinkDedupPlugin] : []),
777
832
  ] satisfies Plugin[]
778
833
 
779
834
  // TODO move to single config and through environments
@@ -38,11 +38,13 @@ const MIME: Record<string, string> = {
38
38
  // FullBundleDev and diverges from the client's inlined data-uri → React hydration
39
39
  // mismatch. So the inliner runs on client (rolldown plugin) and ssr (top-level
40
40
  // transform) alike — see bundledDevPlugin().
41
+ const ASSET_EXT_RE = /\.(png|jpe?g|gif|webp|avif|bmp|ico)/
42
+
41
43
  function inlineAssetImports(
42
44
  code: string,
43
45
  id: string
44
46
  ): { code: string; map: null } | null {
45
- if (!/\.(png|jpe?g|gif|webp|avif|bmp|ico)/.test(code)) return null
47
+ if (!ASSET_EXT_RE.test(code)) return null
46
48
  const importRe =
47
49
  /import\s+(\w+)\s+from\s*['"]([^'"?]+\.(?:png|jpe?g|gif|webp|avif|bmp|ico))['"]\s*;?/g
48
50
  const base = id.split('?')[0]
@@ -78,12 +80,20 @@ export function bundledDevPlugin(enabled: boolean): Plugin {
78
80
 
79
81
  // mirror the client's asset inlining on the server env so SSR emits the same
80
82
  // data-uris (the rolldown client plugin above can't reach the ssr pipeline).
81
- transform(code, id) {
82
- if (!enabled) return
83
- const env = (this as any).environment
84
- if (!env || env.name !== 'ssr' || env.mode !== 'dev') return
85
- return inlineAssetImports(code, id)
86
- },
83
+ // bundledDev is off by default, so only register the hook when it's on, and
84
+ // let rust pre-filter on the asset extensions inlineAssetImports looks for.
85
+ ...(enabled
86
+ ? {
87
+ transform: {
88
+ filter: { code: ASSET_EXT_RE },
89
+ handler(code: string, id: string) {
90
+ const env = (this as any).environment
91
+ if (!env || env.name !== 'ssr' || env.mode !== 'dev') return
92
+ return inlineAssetImports(code, id)
93
+ },
94
+ },
95
+ }
96
+ : {}),
87
97
 
88
98
  config(_userConfig, env) {
89
99
  if (!enabled || env.command !== 'serve') return
@@ -13,32 +13,16 @@ describe('criticalCSSPlugin', () => {
13
13
  })
14
14
 
15
15
  describe('resolveId', () => {
16
- it('should return null for non-inline CSS imports', async () => {
16
+ // non-matching ids are now rejected by the rust-side hook filter, so the
17
+ // handler never runs for them. assert the filter itself does that.
18
+ it('filters out non-inline CSS imports before the handler runs', async () => {
17
19
  const { criticalCSSPlugin } = await import('./criticalCSSPlugin')
18
20
  const plugin = criticalCSSPlugin()
21
+ const filter = (plugin.resolveId as any).filter.id as RegExp
19
22
 
20
- ;(plugin.configResolved as any)({ root: PROJECT_ROOT })
21
-
22
- const result = await (plugin.resolveId as any).call(
23
- { resolve: vi.fn() },
24
- './styles.css',
25
- resolve('/project/src/App.tsx')
26
- )
27
- expect(result).toBeNull()
28
- })
29
-
30
- it('should return null for non-css imports', async () => {
31
- const { criticalCSSPlugin } = await import('./criticalCSSPlugin')
32
- const plugin = criticalCSSPlugin()
33
-
34
- ;(plugin.configResolved as any)({ root: PROJECT_ROOT })
35
-
36
- const result = await (plugin.resolveId as any).call(
37
- { resolve: vi.fn() },
38
- './data.json',
39
- resolve('/project/src/App.tsx')
40
- )
41
- expect(result).toBeNull()
23
+ expect(filter.test('./styles.css')).toBe(false)
24
+ expect(filter.test('./data.json')).toBe(false)
25
+ expect(filter.test('./styles.inline.css')).toBe(true)
42
26
  })
43
27
 
44
28
  it('should resolve .inline.css and track it', async () => {
@@ -53,7 +37,7 @@ describe('criticalCSSPlugin', () => {
53
37
  })
54
38
 
55
39
  const importerPath = resolve('/project/src/App.tsx')
56
- const result = await (plugin.resolveId as any).call(
40
+ const result = await (plugin.resolveId as any).handler.call(
57
41
  { resolve: mockResolve },
58
42
  './styles.inline.css',
59
43
  importerPath
@@ -77,7 +61,7 @@ describe('criticalCSSPlugin', () => {
77
61
  id: resolve('/project/src/layout.inline.css'),
78
62
  })
79
63
 
80
- await (plugin.resolveId as any).call(
64
+ await (plugin.resolveId as any).handler.call(
81
65
  { resolve: mockResolve },
82
66
  './layout.inline.css',
83
67
  resolve('/project/src/App.tsx')
@@ -98,7 +82,7 @@ describe('criticalCSSPlugin', () => {
98
82
 
99
83
  const mockResolve = vi.fn().mockResolvedValue(null)
100
84
 
101
- const result = await (plugin.resolveId as any).call(
85
+ const result = await (plugin.resolveId as any).handler.call(
102
86
  { resolve: mockResolve },
103
87
  './nonexistent.inline.css',
104
88
  resolve('/project/src/App.tsx')
@@ -119,7 +103,7 @@ describe('criticalCSSPlugin', () => {
119
103
  const mockResolve = vi.fn().mockResolvedValue({
120
104
  id: resolve('/project/app/layout.inline.css'),
121
105
  })
122
- await (plugin.resolveId as any).call(
106
+ await (plugin.resolveId as any).handler.call(
123
107
  { resolve: mockResolve },
124
108
  './layout.inline.css',
125
109
  resolve('/project/app/_layout.tsx')
@@ -15,6 +15,9 @@ import type { Plugin } from 'vite'
15
15
  */
16
16
 
17
17
  const INLINE_CSS_EXT = '.inline.css'
18
+ // rust-side hook filter, so non-css ids never cross into js. exactly equivalent
19
+ // to the `id.endsWith(INLINE_CSS_EXT)` guard it replaced.
20
+ const INLINE_CSS_RE = /\.inline\.css$/
18
21
 
19
22
  // source paths of CSS files with .inline.css extension (relative to root)
20
23
  const criticalCSSSources = new Set<string>()
@@ -57,19 +60,20 @@ export function criticalCSSPlugin(): Plugin {
57
60
  root = config.root
58
61
  },
59
62
 
60
- async resolveId(id, importer) {
61
- if (!id.endsWith(INLINE_CSS_EXT)) return null
63
+ resolveId: {
64
+ filter: { id: INLINE_CSS_RE },
65
+ async handler(id, importer) {
66
+ const resolved = await this.resolve(id, importer, { skipSelf: true })
62
67
 
63
- const resolved = await this.resolve(id, importer, { skipSelf: true })
68
+ if (resolved) {
69
+ // store as relative path to match manifest keys (forward slashes to match Vite)
70
+ const relativePath = normalizePath(relative(root, resolved.id))
71
+ criticalCSSSources.add(relativePath)
72
+ return resolved
73
+ }
64
74
 
65
- if (resolved) {
66
- // store as relative path to match manifest keys (forward slashes to match Vite)
67
- const relativePath = normalizePath(relative(root, resolved.id))
68
- criticalCSSSources.add(relativePath)
69
- return resolved
70
- }
71
-
72
- return null
75
+ return null
76
+ },
73
77
  },
74
78
  }
75
79
  }
@@ -19,6 +19,7 @@
19
19
  * platform, not by consumer type.
20
20
  */
21
21
 
22
+ import { prefixRegex } from 'rolldown/filter'
22
23
  import type { Plugin } from 'vite'
23
24
 
24
25
  const VIRTUAL_PREFIX = '\0one-env-guard:'
@@ -30,6 +31,9 @@ const GUARD_SPECIFIERS = [
30
31
  'web-only',
31
32
  ] as const
32
33
 
34
+ // derived so the hook filter can never drift from the list above
35
+ const GUARD_SPECIFIERS_RE = new RegExp(`^(?:${GUARD_SPECIFIERS.join('|')})$`)
36
+
33
37
  type GuardSpecifier = (typeof GUARD_SPECIFIERS)[number]
34
38
  type ViteEnvironment = 'client' | 'ssr' | 'ios' | 'android'
35
39
 
@@ -141,15 +145,24 @@ export function environmentGuardPlugin(options?: EnvironmentGuardOptions): Plugi
141
145
  name: 'one:environment-guard',
142
146
  enforce: 'pre',
143
147
 
144
- resolveId(source) {
145
- const envName = this.environment?.name
146
- if (!envName) return null
147
- const consumer = this.environment?.config?.consumer
148
- return resolveEnvironmentGuard(source, envName, consumer, options)
148
+ // filters are evaluated in rust, so non-guard ids never cross into js.
149
+ // without them every import in the graph paid a napi round trip to be told
150
+ // it wasn't one of these four specifiers.
151
+ resolveId: {
152
+ filter: { id: GUARD_SPECIFIERS_RE },
153
+ handler(source) {
154
+ const envName = this.environment?.name
155
+ if (!envName) return null
156
+ const consumer = this.environment?.config?.consumer
157
+ return resolveEnvironmentGuard(source, envName, consumer, options)
158
+ },
149
159
  },
150
160
 
151
- load(id) {
152
- return loadEnvironmentGuard(id)
161
+ load: {
162
+ filter: { id: prefixRegex(VIRTUAL_PREFIX) },
163
+ handler(id) {
164
+ return loadEnvironmentGuard(id)
165
+ },
153
166
  },
154
167
  }
155
168
  }
@@ -699,9 +699,13 @@ export function createFileSystemRouterPlugin(
699
699
  // 404s in this mode, so point preloads (and bootstrap) at the bundled url.
700
700
  const clientEnv = server.environments?.client as any
701
701
  if (clientEnv?.config?.isBundled || clientEnv?.bundledDev) {
702
- // bundled mode: the HMR client + refresh runtime are bundled into the
703
- // entry; /@vite/client doesn't exist as a standalone module (404s).
704
- preloads = ['/assets/_virtual_one-entry.js']
702
+ // bundled mode: /@vite/client doesn't exist as a standalone module
703
+ // (404s). vite 8.2 moved the bundled-dev client out of the entry bundle
704
+ // and serves it standalone at /bundledDevClient.mjs; it defines
705
+ // __rolldown_runtime__, which the entry calls on nearly every module, so
706
+ // it has to load first or nothing hydrates. bootstrapModules emits these
707
+ // in order, and module scripts execute in order.
708
+ preloads = ['/bundledDevClient.mjs', '/assets/_virtual_one-entry.js']
705
709
  }
706
710
 
707
711
  // change this to .server to test using the indepedently scoped env
@@ -35,29 +35,17 @@ describe('imageDataPlugin', () => {
35
35
  })
36
36
 
37
37
  describe('resolveId', () => {
38
- it('should return null for non-imagedata imports', async () => {
38
+ // these ids are now rejected by the rust-side hook filter, so the handler
39
+ // never runs for them. assert the filter itself does that.
40
+ it('filters out ids without a trailing ?imagedata', async () => {
39
41
  const { imageDataPlugin } = await import('./imageDataPlugin')
40
42
  const plugin = imageDataPlugin()
43
+ const filter = (plugin.resolveId as any).filter.id as RegExp
41
44
 
42
- if (plugin.configResolved) {
43
- ;(plugin.configResolved as any)(mockConfig())
44
- }
45
-
46
- const result = await (plugin.resolveId as any)('./image.jpg', undefined)
47
- expect(result).toBeNull()
48
- })
49
-
50
- it('should return null for imports with imagedata not as suffix', async () => {
51
- const { imageDataPlugin } = await import('./imageDataPlugin')
52
- const plugin = imageDataPlugin()
53
-
54
- if (plugin.configResolved) {
55
- ;(plugin.configResolved as any)(mockConfig())
56
- }
57
-
45
+ expect(filter.test('./image.jpg')).toBe(false)
58
46
  // ?imagedata in the middle should not match
59
- const result = await (plugin.resolveId as any)('./image?imagedata.jpg', undefined)
60
- expect(result).toBeNull()
47
+ expect(filter.test('./image?imagedata.jpg')).toBe(false)
48
+ expect(filter.test('./image.jpg?imagedata')).toBe(true)
61
49
  })
62
50
 
63
51
  it('should resolve public dir paths starting with /', async () => {
@@ -68,7 +56,7 @@ describe('imageDataPlugin', () => {
68
56
  ;(plugin.configResolved as any)(mockConfig())
69
57
  }
70
58
 
71
- const result = await (plugin.resolveId as any)(
59
+ const result = await (plugin.resolveId as any).handler(
72
60
  '/test-image.jpg?imagedata',
73
61
  undefined
74
62
  )
@@ -83,7 +71,7 @@ describe('imageDataPlugin', () => {
83
71
  ;(plugin.configResolved as any)(mockConfig())
84
72
  }
85
73
 
86
- const result = await (plugin.resolveId as any)(
74
+ const result = await (plugin.resolveId as any).handler(
87
75
  './test-image.jpg?imagedata',
88
76
  HERO_FILE
89
77
  )
@@ -98,7 +86,7 @@ describe('imageDataPlugin', () => {
98
86
  ;(plugin.configResolved as any)(mockConfig())
99
87
  }
100
88
 
101
- const result = await (plugin.resolveId as any)(
89
+ const result = await (plugin.resolveId as any).handler(
102
90
  '/nonexistent.jpg?imagedata',
103
91
  undefined
104
92
  )
@@ -115,7 +103,7 @@ describe('imageDataPlugin', () => {
115
103
  ;(plugin.configResolved as any)(mockConfig())
116
104
  }
117
105
 
118
- const result = await (plugin.resolveId as any)(
106
+ const result = await (plugin.resolveId as any).handler(
119
107
  '/../../../etc/passwd?imagedata',
120
108
  undefined
121
109
  )
@@ -130,7 +118,7 @@ describe('imageDataPlugin', () => {
130
118
  ;(plugin.configResolved as any)(mockConfig())
131
119
  }
132
120
 
133
- const result = await (plugin.resolveId as any)(
121
+ const result = await (plugin.resolveId as any).handler(
134
122
  '../../../../etc/passwd?imagedata',
135
123
  HERO_FILE
136
124
  )
@@ -145,7 +133,7 @@ describe('imageDataPlugin', () => {
145
133
  ;(plugin.configResolved as any)(mockConfig())
146
134
  }
147
135
 
148
- const result = await (plugin.resolveId as any)(
136
+ const result = await (plugin.resolveId as any).handler(
149
137
  '../../../etc/passwd?imagedata',
150
138
  undefined
151
139
  )
@@ -162,7 +150,7 @@ describe('imageDataPlugin', () => {
162
150
 
163
151
  // Going up and back down should still work if within bounds
164
152
  // From /project/src/components, ../.. goes to /project, then src/test-image.jpg
165
- const result = await (plugin.resolveId as any)(
153
+ const result = await (plugin.resolveId as any).handler(
166
154
  '../../src/test-image.jpg?imagedata',
167
155
  HERO_FILE
168
156
  )
@@ -171,20 +159,13 @@ describe('imageDataPlugin', () => {
171
159
  })
172
160
 
173
161
  describe('load', () => {
174
- it('should return null for non-virtual modules', async () => {
162
+ it('filters out non-virtual modules before the handler runs', async () => {
175
163
  const { imageDataPlugin } = await import('./imageDataPlugin')
176
164
  const plugin = imageDataPlugin()
165
+ const filter = (plugin.load as any).filter.id as RegExp
177
166
 
178
- if (plugin.configResolved) {
179
- ;(plugin.configResolved as any)(mockConfig())
180
- }
181
-
182
- const mockContext = {
183
- addWatchFile: vi.fn(),
184
- }
185
-
186
- const result = await (plugin.load as any).call(mockContext, './image.jpg')
187
- expect(result).toBeNull()
167
+ expect(filter.test('./image.jpg')).toBe(false)
168
+ expect(filter.test('\0imagedata:/abs/path/image.jpg')).toBe(true)
188
169
  })
189
170
 
190
171
  it('should return fallback data when sharp fails', async () => {
@@ -203,7 +184,7 @@ describe('imageDataPlugin', () => {
203
184
 
204
185
  // This will fail because the file doesn't actually exist
205
186
  // But it should gracefully fallback
206
- const result = await (plugin.load as any).call(
187
+ const result = await (plugin.load as any).handler.call(
207
188
  mockContext,
208
189
  '\0imagedata:' + testFilePath
209
190
  )
@@ -248,7 +229,7 @@ describe('imageDataPlugin output format', () => {
248
229
 
249
230
  const testFilePath = resolve(PUBLIC_DIR, 'test-image.jpg')
250
231
 
251
- const result = await (plugin.load as any).call(
232
+ const result = await (plugin.load as any).handler.call(
252
233
  mockContext,
253
234
  '\0imagedata:' + testFilePath
254
235
  )