one 1.6.16 → 1.6.18

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 (112) hide show
  1. package/dist/cjs/babel-plugins/environment-guard.cjs +51 -0
  2. package/dist/cjs/babel-plugins/environment-guard.js +53 -0
  3. package/dist/cjs/babel-plugins/environment-guard.js.map +6 -0
  4. package/dist/cjs/babel-plugins/environment-guard.native.js +54 -0
  5. package/dist/cjs/babel-plugins/environment-guard.native.js.map +1 -0
  6. package/dist/cjs/cli/daemon.cjs +7 -1
  7. package/dist/cjs/cli/daemon.js +10 -1
  8. package/dist/cjs/cli/daemon.js.map +1 -1
  9. package/dist/cjs/cli/daemon.native.js +11 -1
  10. package/dist/cjs/cli/daemon.native.js.map +1 -1
  11. package/dist/cjs/daemon/server.cjs +38 -2
  12. package/dist/cjs/daemon/server.js +28 -2
  13. package/dist/cjs/daemon/server.js.map +1 -1
  14. package/dist/cjs/daemon/server.native.js +59 -6
  15. package/dist/cjs/daemon/server.native.js.map +1 -1
  16. package/dist/cjs/metro-config/getViteMetroPluginOptions.cjs +2 -0
  17. package/dist/cjs/metro-config/getViteMetroPluginOptions.js +2 -0
  18. package/dist/cjs/metro-config/getViteMetroPluginOptions.js.map +1 -1
  19. package/dist/cjs/metro-config/getViteMetroPluginOptions.native.js +2 -0
  20. package/dist/cjs/metro-config/getViteMetroPluginOptions.native.js.map +1 -1
  21. package/dist/cjs/vite/one.cjs +3 -2
  22. package/dist/cjs/vite/one.js +2 -1
  23. package/dist/cjs/vite/one.js.map +1 -1
  24. package/dist/cjs/vite/one.native.js +3 -2
  25. package/dist/cjs/vite/one.native.js.map +1 -1
  26. package/dist/cjs/vite/plugins/environmentGuardPlugin.cjs +61 -0
  27. package/dist/cjs/vite/plugins/environmentGuardPlugin.js +57 -0
  28. package/dist/cjs/vite/plugins/environmentGuardPlugin.js.map +6 -0
  29. package/dist/cjs/vite/plugins/environmentGuardPlugin.native.js +65 -0
  30. package/dist/cjs/vite/plugins/environmentGuardPlugin.native.js.map +1 -0
  31. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.cjs +73 -0
  32. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.js +73 -0
  33. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.js.map +6 -0
  34. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.native.js +76 -0
  35. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.native.js.map +1 -0
  36. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +13 -1
  37. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.js +13 -5
  38. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.js.map +2 -2
  39. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +14 -1
  40. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  41. package/dist/esm/babel-plugins/environment-guard.js +29 -0
  42. package/dist/esm/babel-plugins/environment-guard.js.map +6 -0
  43. package/dist/esm/babel-plugins/environment-guard.mjs +17 -0
  44. package/dist/esm/babel-plugins/environment-guard.mjs.map +1 -0
  45. package/dist/esm/babel-plugins/environment-guard.native.js +17 -0
  46. package/dist/esm/babel-plugins/environment-guard.native.js.map +1 -0
  47. package/dist/esm/cli/daemon.js +10 -1
  48. package/dist/esm/cli/daemon.js.map +1 -1
  49. package/dist/esm/cli/daemon.mjs +7 -1
  50. package/dist/esm/cli/daemon.mjs.map +1 -1
  51. package/dist/esm/cli/daemon.native.js +11 -1
  52. package/dist/esm/cli/daemon.native.js.map +1 -1
  53. package/dist/esm/daemon/server.js +29 -2
  54. package/dist/esm/daemon/server.js.map +1 -1
  55. package/dist/esm/daemon/server.mjs +39 -3
  56. package/dist/esm/daemon/server.mjs.map +1 -1
  57. package/dist/esm/daemon/server.native.js +60 -7
  58. package/dist/esm/daemon/server.native.js.map +1 -1
  59. package/dist/esm/metro-config/getViteMetroPluginOptions.js +2 -0
  60. package/dist/esm/metro-config/getViteMetroPluginOptions.js.map +1 -1
  61. package/dist/esm/metro-config/getViteMetroPluginOptions.mjs +2 -0
  62. package/dist/esm/metro-config/getViteMetroPluginOptions.mjs.map +1 -1
  63. package/dist/esm/metro-config/getViteMetroPluginOptions.native.js +2 -0
  64. package/dist/esm/metro-config/getViteMetroPluginOptions.native.js.map +1 -1
  65. package/dist/esm/vite/one.js +2 -0
  66. package/dist/esm/vite/one.js.map +1 -1
  67. package/dist/esm/vite/one.mjs +2 -1
  68. package/dist/esm/vite/one.mjs.map +1 -1
  69. package/dist/esm/vite/one.native.js +2 -1
  70. package/dist/esm/vite/one.native.js.map +1 -1
  71. package/dist/esm/vite/plugins/environmentGuardPlugin.js +41 -0
  72. package/dist/esm/vite/plugins/environmentGuardPlugin.js.map +6 -0
  73. package/dist/esm/vite/plugins/environmentGuardPlugin.mjs +36 -0
  74. package/dist/esm/vite/plugins/environmentGuardPlugin.mjs.map +1 -0
  75. package/dist/esm/vite/plugins/environmentGuardPlugin.native.js +37 -0
  76. package/dist/esm/vite/plugins/environmentGuardPlugin.native.js.map +1 -0
  77. package/dist/esm/vite/plugins/environmentGuardPlugin.test.js +74 -0
  78. package/dist/esm/vite/plugins/environmentGuardPlugin.test.js.map +6 -0
  79. package/dist/esm/vite/plugins/environmentGuardPlugin.test.mjs +74 -0
  80. package/dist/esm/vite/plugins/environmentGuardPlugin.test.mjs.map +1 -0
  81. package/dist/esm/vite/plugins/environmentGuardPlugin.test.native.js +74 -0
  82. package/dist/esm/vite/plugins/environmentGuardPlugin.test.native.js.map +1 -0
  83. package/dist/esm/vite/plugins/fileSystemRouterPlugin.js +13 -4
  84. package/dist/esm/vite/plugins/fileSystemRouterPlugin.js.map +1 -1
  85. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +13 -1
  86. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
  87. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +14 -1
  88. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  89. package/env.d.ts +6 -0
  90. package/package.json +16 -11
  91. package/src/babel-plugins/environment-guard.ts +50 -0
  92. package/src/cli/daemon.ts +19 -0
  93. package/src/daemon/server.ts +56 -0
  94. package/src/metro-config/getViteMetroPluginOptions.ts +2 -0
  95. package/src/vite/one.ts +3 -0
  96. package/src/vite/plugins/environmentGuardPlugin.test.ts +125 -0
  97. package/src/vite/plugins/environmentGuardPlugin.ts +92 -0
  98. package/src/vite/plugins/fileSystemRouterPlugin.tsx +19 -5
  99. package/src/vite/types.ts +7 -1
  100. package/types/babel-plugins/environment-guard.d.ts +11 -0
  101. package/types/babel-plugins/environment-guard.d.ts.map +1 -0
  102. package/types/cli/daemon.d.ts.map +1 -1
  103. package/types/daemon/server.d.ts.map +1 -1
  104. package/types/metro-config/getViteMetroPluginOptions.d.ts.map +1 -1
  105. package/types/vite/one.d.ts.map +1 -1
  106. package/types/vite/plugins/environmentGuardPlugin.d.ts +27 -0
  107. package/types/vite/plugins/environmentGuardPlugin.d.ts.map +1 -0
  108. package/types/vite/plugins/environmentGuardPlugin.test.d.ts +2 -0
  109. package/types/vite/plugins/environmentGuardPlugin.test.d.ts.map +1 -0
  110. package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
  111. package/types/vite/types.d.ts +7 -1
  112. package/types/vite/types.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one",
3
- "version": "1.6.16",
3
+ "version": "1.6.18",
4
4
  "license": "BSD-3-Clause",
5
5
  "sideEffects": [
6
6
  "setup.mjs",
@@ -75,6 +75,11 @@
75
75
  "import": "./dist/esm/babel-plugins/remove-server-code.mjs",
76
76
  "require": "./dist/cjs/babel-plugins/remove-server-code.cjs"
77
77
  },
78
+ "./babel-plugin-environment-guard": {
79
+ "types": "./types/babel-plugins/environment-guard.d.ts",
80
+ "import": "./dist/esm/babel-plugins/environment-guard.mjs",
81
+ "require": "./dist/cjs/babel-plugins/environment-guard.cjs"
82
+ },
78
83
  "./getViteMetroPluginOptions": {
79
84
  "types": "./types/metro-config/getViteMetroPluginOptions.d.ts",
80
85
  "import": "./dist/esm/metro-config/getViteMetroPluginOptions.mjs",
@@ -127,8 +132,8 @@
127
132
  "check": "depcheck",
128
133
  "clean": "tamagui-build clean",
129
134
  "clean:build": "tamagui-build clean:build",
130
- "lint": "../../node_modules/.bin/biome check src",
131
- "lint:fix": "../../node_modules/.bin/biome check --write --unsafe src",
135
+ "lint": "oxlint src",
136
+ "lint:fix": "oxlint --fix --fix-suggestions src",
132
137
  "test": "bun run vitest --run",
133
138
  "typecheck": "tsc --noEmit",
134
139
  "watch": "tamagui-build --watch"
@@ -149,17 +154,17 @@
149
154
  "@react-navigation/routers": "~7.5.1",
150
155
  "@swc/core": "^1.14.0",
151
156
  "@ungap/structured-clone": "^1.2.0",
152
- "@vxrn/color-scheme": "1.6.16",
153
- "@vxrn/compiler": "1.6.16",
154
- "@vxrn/resolve": "1.6.16",
155
- "@vxrn/tslib-lite": "1.6.16",
156
- "@vxrn/use-isomorphic-layout-effect": "1.6.16",
157
- "@vxrn/vite-plugin-metro": "1.6.16",
157
+ "@vxrn/color-scheme": "1.6.18",
158
+ "@vxrn/compiler": "1.6.18",
159
+ "@vxrn/resolve": "1.6.18",
160
+ "@vxrn/tslib-lite": "1.6.18",
161
+ "@vxrn/use-isomorphic-layout-effect": "1.6.18",
162
+ "@vxrn/vite-plugin-metro": "1.6.18",
158
163
  "babel-dead-code-elimination": "1.0.10",
159
164
  "babel-plugin-module-resolver": "^5.0.2",
160
165
  "citty": "^0.1.6",
161
166
  "core-js": "^3.38.1",
162
- "create-vxrn": "1.6.16",
167
+ "create-vxrn": "1.6.18",
163
168
  "escape-string-regexp": "^5.0.0",
164
169
  "expo-linking": "~8.0.8",
165
170
  "expo-modules-core": "~3.0.24",
@@ -184,7 +189,7 @@
184
189
  "use-latest-callback": "^0.2.3",
185
190
  "vite": "^7.1.12",
186
191
  "vite-tsconfig-paths": "^6.0.5",
187
- "vxrn": "1.6.16",
192
+ "vxrn": "1.6.18",
188
193
  "ws": "^8.18.0",
189
194
  "xxhashjs": "^0.2.2"
190
195
  },
@@ -0,0 +1,50 @@
1
+ /**
2
+ * babel plugin for native (metro) builds that enforces environment guard imports.
3
+ *
4
+ * in native builds, `native-only` imports are allowed (removed as no-ops),
5
+ * while `server-only`, `client-only`, and `web-only` imports are replaced
6
+ * with a throw statement.
7
+ */
8
+
9
+ import type { PluginObj } from '@babel/core'
10
+ import * as t from '@babel/types'
11
+
12
+ const GUARD_SPECIFIERS = [
13
+ 'server-only',
14
+ 'client-only',
15
+ 'native-only',
16
+ 'web-only',
17
+ ] as const
18
+
19
+ type GuardSpecifier = (typeof GUARD_SPECIFIERS)[number]
20
+
21
+ // native builds allow native-only, forbid the rest
22
+ const ALLOWED_IN_NATIVE: readonly GuardSpecifier[] = ['native-only']
23
+
24
+ function environmentGuardBabelPlugin(_: unknown): PluginObj {
25
+ return {
26
+ name: 'one-environment-guard',
27
+ visitor: {
28
+ ImportDeclaration(path) {
29
+ const source = path.node.source.value as GuardSpecifier
30
+ if (!GUARD_SPECIFIERS.includes(source)) return
31
+
32
+ if (ALLOWED_IN_NATIVE.includes(source)) {
33
+ // allowed — remove the import (it's a side-effect-only guard)
34
+ path.remove()
35
+ } else {
36
+ // forbidden — replace with throw
37
+ path.replaceWith(
38
+ t.throwStatement(
39
+ t.newExpression(t.identifier('Error'), [
40
+ t.stringLiteral(`${source} cannot be imported in a native environment`),
41
+ ])
42
+ )
43
+ )
44
+ }
45
+ },
46
+ },
47
+ }
48
+ }
49
+
50
+ export default environmentGuardBabelPlugin
package/src/cli/daemon.ts CHANGED
@@ -46,6 +46,9 @@ async function daemonStart(args: { port?: string; host?: string; tui?: boolean }
46
46
  process.exit(1)
47
47
  }
48
48
 
49
+ // suggest tray app if available
50
+ await suggestTrayApp()
51
+
49
52
  const { startDaemon } = await import('../daemon/server')
50
53
 
51
54
  // default to TUI if running in interactive terminal
@@ -231,3 +234,19 @@ async function daemonRoute(args: { app?: string; slot?: string; project?: string
231
234
  const shortRoot = targetServer.root.replace(process.env.HOME || '', '~')
232
235
  console.log(colors.green(`Route set: ${args.app} → ${targetServer.id} (${shortRoot})`))
233
236
  }
237
+
238
+ async function suggestTrayApp() {
239
+ const { existsSync } = await import('node:fs')
240
+ const trayPaths = [
241
+ '/Applications/OneTray.app',
242
+ `${process.env.HOME}/Applications/OneTray.app`,
243
+ ]
244
+ const installed = trayPaths.some((p) => existsSync(p))
245
+ if (!installed) {
246
+ console.log(
247
+ colors.dim(' Tip: install OneTray.app for a native macOS cable interface')
248
+ )
249
+ console.log(colors.dim(' https://github.com/onejs/one/releases?q=one-tray'))
250
+ console.log('')
251
+ }
252
+ }
@@ -286,6 +286,7 @@ import {
286
286
  getAllServers,
287
287
  getRoute,
288
288
  setRoute,
289
+ clearRoute,
289
290
  touchServer,
290
291
  pruneDeadServers,
291
292
  checkServerAlive,
@@ -651,6 +652,11 @@ async function handleDaemonEndpoint(
651
652
  if (url.pathname === '/__daemon/status') {
652
653
  const servers = getAllServers(state)
653
654
  const simulators = await getBootedSimulators()
655
+ const simMappings = getSimulatorMappings()
656
+ const simulatorRoutes: Record<string, string> = {}
657
+ for (const [udid, serverId] of simMappings) {
658
+ simulatorRoutes[udid] = serverId
659
+ }
654
660
 
655
661
  res.writeHead(200, { 'Content-Type': 'application/json' })
656
662
  res.end(
@@ -663,6 +669,8 @@ async function handleDaemonEndpoint(
663
669
  root: s.root,
664
670
  })),
665
671
  simulators,
672
+ simulatorRoutes,
673
+ routeMode: routeModeOverride || 'most-recent',
666
674
  },
667
675
  null,
668
676
  2
@@ -699,6 +707,54 @@ async function handleDaemonEndpoint(
699
707
  return
700
708
  }
701
709
 
710
+ // POST /__daemon/simulator-route?simulatorUdid=...&serverId=...
711
+ // used by tray app to set simulator -> server mappings
712
+ if (url.pathname === '/__daemon/simulator-route' && req.method === 'POST') {
713
+ const simulatorUdid = url.searchParams.get('simulatorUdid')
714
+ const serverId = url.searchParams.get('serverId')
715
+
716
+ if (!simulatorUdid || !serverId) {
717
+ res.writeHead(400)
718
+ res.end('Missing simulatorUdid or serverId')
719
+ return
720
+ }
721
+
722
+ const server = findServerById(state, serverId)
723
+ if (!server) {
724
+ res.writeHead(404)
725
+ res.end('Server not found')
726
+ return
727
+ }
728
+
729
+ // set the mapping (same as TUI cable connect)
730
+ setSimulatorMapping(simulatorUdid, serverId)
731
+ setPendingMapping(serverId, simulatorUdid)
732
+ setRoute(state, `sim:${simulatorUdid}`, serverId)
733
+
734
+ res.writeHead(200, { 'Content-Type': 'application/json' })
735
+ res.end(JSON.stringify({ ok: true }))
736
+ return
737
+ }
738
+
739
+ // DELETE /__daemon/simulator-route?simulatorUdid=...
740
+ // used by tray app to clear simulator -> server mappings
741
+ if (url.pathname === '/__daemon/simulator-route' && req.method === 'DELETE') {
742
+ const simulatorUdid = url.searchParams.get('simulatorUdid')
743
+
744
+ if (!simulatorUdid) {
745
+ res.writeHead(400)
746
+ res.end('Missing simulatorUdid')
747
+ return
748
+ }
749
+
750
+ clearMappingsForSimulator(simulatorUdid)
751
+ clearRoute(state, `sim:${simulatorUdid}`)
752
+
753
+ res.writeHead(200, { 'Content-Type': 'application/json' })
754
+ res.end(JSON.stringify({ ok: true }))
755
+ return
756
+ }
757
+
702
758
  res.writeHead(404)
703
759
  res.end('Not found')
704
760
  }
@@ -151,6 +151,8 @@ export function getViteMetroPluginOptions({
151
151
  },
152
152
  babelConfig: {
153
153
  plugins: [
154
+ // enforce environment guard imports (server-only, client-only, etc.)
155
+ 'one/babel-plugin-environment-guard',
154
156
  // Remove server-only code (loader, generateStaticParams) from route files
155
157
  // This must run early to prevent server-only imports from being bundled
156
158
  [
package/src/vite/one.ts CHANGED
@@ -27,6 +27,7 @@ import { sourceInspectorPlugin } from './plugins/sourceInspectorPlugin'
27
27
  import { SSRCSSPlugin } from './plugins/SSRCSSPlugin'
28
28
  import { virtualEntryId } from './plugins/virtualEntryConstants'
29
29
  import { createVirtualEntry } from './plugins/virtualEntryPlugin'
30
+ import { environmentGuardPlugin } from './plugins/environmentGuardPlugin'
30
31
  import type { One } from './types'
31
32
 
32
33
  type MetroOptions = MetroPluginOptions
@@ -161,6 +162,8 @@ export function one(options: One.PluginOptions = {}): PluginOption {
161
162
  },
162
163
  },
163
164
 
165
+ environmentGuardPlugin(),
166
+
164
167
  imageDataPlugin(),
165
168
 
166
169
  {
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolveEnvironmentGuard, loadEnvironmentGuard } from './environmentGuardPlugin'
3
+
4
+ describe('environmentGuardPlugin', () => {
5
+ describe('resolveEnvironmentGuard', () => {
6
+ it('returns null for non-guard specifiers', () => {
7
+ expect(resolveEnvironmentGuard('react', 'client')).toBeNull()
8
+ expect(resolveEnvironmentGuard('vite', 'ssr')).toBeNull()
9
+ expect(resolveEnvironmentGuard('some-other-pkg', 'ios')).toBeNull()
10
+ })
11
+
12
+ it('returns virtual id for server-only', () => {
13
+ const id = resolveEnvironmentGuard('server-only', 'ssr')
14
+ expect(id).toBe('\0one-env-guard:server-only:ssr')
15
+ })
16
+
17
+ it('returns virtual id for client-only', () => {
18
+ const id = resolveEnvironmentGuard('client-only', 'client')
19
+ expect(id).toBe('\0one-env-guard:client-only:client')
20
+ })
21
+
22
+ it('returns virtual id for native-only', () => {
23
+ const id = resolveEnvironmentGuard('native-only', 'ios')
24
+ expect(id).toBe('\0one-env-guard:native-only:ios')
25
+ })
26
+
27
+ it('returns virtual id for web-only', () => {
28
+ const id = resolveEnvironmentGuard('web-only', 'client')
29
+ expect(id).toBe('\0one-env-guard:web-only:client')
30
+ })
31
+ })
32
+
33
+ describe('loadEnvironmentGuard', () => {
34
+ it('returns null for non-virtual ids', () => {
35
+ expect(loadEnvironmentGuard('react')).toBeNull()
36
+ expect(loadEnvironmentGuard('/some/path.ts')).toBeNull()
37
+ })
38
+
39
+ // server-only: allowed in ssr, forbidden in client/ios/android
40
+ it('server-only: allowed in ssr', () => {
41
+ const result = loadEnvironmentGuard('\0one-env-guard:server-only:ssr')
42
+ expect(result).toBe('export {}')
43
+ })
44
+
45
+ it('server-only: forbidden in client', () => {
46
+ const result = loadEnvironmentGuard('\0one-env-guard:server-only:client')
47
+ expect(result).toContain('throw new Error')
48
+ expect(result).toContain('server-only')
49
+ expect(result).toContain('client')
50
+ })
51
+
52
+ it('server-only: forbidden in ios', () => {
53
+ const result = loadEnvironmentGuard('\0one-env-guard:server-only:ios')
54
+ expect(result).toContain('throw new Error')
55
+ })
56
+
57
+ it('server-only: forbidden in android', () => {
58
+ const result = loadEnvironmentGuard('\0one-env-guard:server-only:android')
59
+ expect(result).toContain('throw new Error')
60
+ })
61
+
62
+ // client-only: allowed in client, forbidden in ssr/ios/android
63
+ it('client-only: allowed in client', () => {
64
+ const result = loadEnvironmentGuard('\0one-env-guard:client-only:client')
65
+ expect(result).toBe('export {}')
66
+ })
67
+
68
+ it('client-only: forbidden in ssr', () => {
69
+ const result = loadEnvironmentGuard('\0one-env-guard:client-only:ssr')
70
+ expect(result).toContain('throw new Error')
71
+ })
72
+
73
+ it('client-only: forbidden in ios', () => {
74
+ const result = loadEnvironmentGuard('\0one-env-guard:client-only:ios')
75
+ expect(result).toContain('throw new Error')
76
+ })
77
+
78
+ it('client-only: forbidden in android', () => {
79
+ const result = loadEnvironmentGuard('\0one-env-guard:client-only:android')
80
+ expect(result).toContain('throw new Error')
81
+ })
82
+
83
+ // native-only: allowed in ios/android, forbidden in client/ssr
84
+ it('native-only: allowed in ios', () => {
85
+ const result = loadEnvironmentGuard('\0one-env-guard:native-only:ios')
86
+ expect(result).toBe('export {}')
87
+ })
88
+
89
+ it('native-only: allowed in android', () => {
90
+ const result = loadEnvironmentGuard('\0one-env-guard:native-only:android')
91
+ expect(result).toBe('export {}')
92
+ })
93
+
94
+ it('native-only: forbidden in client', () => {
95
+ const result = loadEnvironmentGuard('\0one-env-guard:native-only:client')
96
+ expect(result).toContain('throw new Error')
97
+ })
98
+
99
+ it('native-only: forbidden in ssr', () => {
100
+ const result = loadEnvironmentGuard('\0one-env-guard:native-only:ssr')
101
+ expect(result).toContain('throw new Error')
102
+ })
103
+
104
+ // web-only: allowed in client/ssr, forbidden in ios/android
105
+ it('web-only: allowed in client', () => {
106
+ const result = loadEnvironmentGuard('\0one-env-guard:web-only:client')
107
+ expect(result).toBe('export {}')
108
+ })
109
+
110
+ it('web-only: allowed in ssr', () => {
111
+ const result = loadEnvironmentGuard('\0one-env-guard:web-only:ssr')
112
+ expect(result).toBe('export {}')
113
+ })
114
+
115
+ it('web-only: forbidden in ios', () => {
116
+ const result = loadEnvironmentGuard('\0one-env-guard:web-only:ios')
117
+ expect(result).toContain('throw new Error')
118
+ })
119
+
120
+ it('web-only: forbidden in android', () => {
121
+ const result = loadEnvironmentGuard('\0one-env-guard:web-only:android')
122
+ expect(result).toContain('throw new Error')
123
+ })
124
+ })
125
+ })
@@ -0,0 +1,92 @@
1
+ /**
2
+ * vite plugin that enforces environment guard imports.
3
+ *
4
+ * bare imports like `import 'server-only'` will either be a no-op (allowed)
5
+ * or throw at build/dev time (forbidden) depending on which vite environment
6
+ * is processing the module.
7
+ *
8
+ * | import | allowed in | throws in |
9
+ * |-----------------|------------------|----------------------------|
10
+ * | server-only | ssr | client, ios, android |
11
+ * | client-only | client | ssr, ios, android |
12
+ * | native-only | ios, android | client, ssr |
13
+ * | web-only | client, ssr | ios, android |
14
+ */
15
+
16
+ import type { Plugin } from 'vite'
17
+
18
+ const VIRTUAL_PREFIX = '\0one-env-guard:'
19
+
20
+ const GUARD_SPECIFIERS = [
21
+ 'server-only',
22
+ 'client-only',
23
+ 'native-only',
24
+ 'web-only',
25
+ ] as const
26
+
27
+ type GuardSpecifier = (typeof GUARD_SPECIFIERS)[number]
28
+ type ViteEnvironment = 'client' | 'ssr' | 'ios' | 'android'
29
+
30
+ const ALLOWED_ENVIRONMENTS: Record<GuardSpecifier, readonly ViteEnvironment[]> = {
31
+ 'server-only': ['ssr'],
32
+ 'client-only': ['client'],
33
+ 'native-only': ['ios', 'android'],
34
+ 'web-only': ['client', 'ssr'],
35
+ }
36
+
37
+ /**
38
+ * returns a virtual module id if the specifier is a guard, otherwise null.
39
+ * pure function extracted for testing.
40
+ */
41
+ export function resolveEnvironmentGuard(
42
+ specifier: string,
43
+ envName: string
44
+ ): string | null {
45
+ if (!GUARD_SPECIFIERS.includes(specifier as GuardSpecifier)) {
46
+ return null
47
+ }
48
+ return `${VIRTUAL_PREFIX}${specifier}:${envName}`
49
+ }
50
+
51
+ /**
52
+ * returns the module source for a virtual guard id.
53
+ * pure function extracted for testing.
54
+ */
55
+ export function loadEnvironmentGuard(id: string): string | null {
56
+ if (!id.startsWith(VIRTUAL_PREFIX)) {
57
+ return null
58
+ }
59
+
60
+ const rest = id.slice(VIRTUAL_PREFIX.length)
61
+ const lastColon = rest.lastIndexOf(':')
62
+ if (lastColon === -1) return null
63
+
64
+ const specifier = rest.slice(0, lastColon) as GuardSpecifier
65
+ const envName = rest.slice(lastColon + 1) as ViteEnvironment
66
+
67
+ const allowed = ALLOWED_ENVIRONMENTS[specifier]
68
+ if (!allowed) return null
69
+
70
+ if (allowed.includes(envName)) {
71
+ return 'export {}'
72
+ }
73
+
74
+ return `throw new Error("${specifier} cannot be imported in the \\"${envName}\\" environment")`
75
+ }
76
+
77
+ export function environmentGuardPlugin(): Plugin {
78
+ return {
79
+ name: 'one:environment-guard',
80
+ enforce: 'pre',
81
+
82
+ resolveId(source) {
83
+ const envName = this.environment?.name
84
+ if (!envName) return null
85
+ return resolveEnvironmentGuard(source, envName)
86
+ },
87
+
88
+ load(id) {
89
+ return loadEnvironmentGuard(id)
90
+ },
91
+ }
92
+ }
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { Readable } from 'node:stream'
3
3
  import { debounce } from 'perfect-debounce'
4
+ import colors from 'picocolors'
4
5
  import type { Connect, Plugin, ViteDevServer } from 'vite'
5
6
  import { createServerModuleRunner } from 'vite'
6
7
  import type { ModuleRunner } from 'vite/module-runner'
@@ -23,6 +24,13 @@ import { virtalEntryIdClient, virtualEntryId } from './virtualEntryConstants'
23
24
  const debugRouter = process.env.ONE_DEBUG_ROUTER
24
25
  const debugLoaderDeps = process.env.ONE_DEBUG_LOADER_DEPS
25
26
 
27
+ const routeTypeColors: Record<string, (s: string) => string> = {
28
+ ssg: colors.green,
29
+ ssr: colors.blue,
30
+ spa: colors.yellow,
31
+ api: colors.magenta,
32
+ }
33
+
26
34
  // server needs better dep optimization
27
35
  const USE_SERVER_ENV = false //!!process.env.USE_SERVER_ENV
28
36
 
@@ -45,11 +53,17 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
45
53
  return createHandleRequest(
46
54
  {
47
55
  async handlePage({ route, url, loaderProps }) {
48
- console.info(
49
- ` [${route.type}] ${url} resolved to ${
50
- route.isNotFound ? '‼️ 404 not found' : `app/${route.file.slice(2)}`
51
- }`
52
- )
56
+ if (options.server?.loggingEnabled !== false) {
57
+ const colorType = routeTypeColors[route.type] || colors.white
58
+ const pathname =
59
+ typeof url === 'string' ? new URL(url).pathname : url.pathname
60
+ const file = route.isNotFound
61
+ ? colors.red('404')
62
+ : colors.dim(`app/${route.file.slice(2)}`)
63
+ console.info(
64
+ ` ⓵ ${colorType(`[${route.type}]`)} ${pathname} ${colors.dim('→')} ${file}`
65
+ )
66
+ }
53
67
 
54
68
  const isSpaShell = route.type === 'spa' && !!options.web?.renderRootLayout
55
69
 
package/src/vite/types.ts CHANGED
@@ -436,7 +436,13 @@ export namespace One {
436
436
  seoPreview?: boolean
437
437
  }
438
438
 
439
- server?: VXRNOptions['server']
439
+ server?: VXRNOptions['server'] & {
440
+ /**
441
+ * Log HTTP requests to the console
442
+ * @default true
443
+ */
444
+ loggingEnabled?: boolean
445
+ }
440
446
 
441
447
  /**
442
448
  * Skip loading .env files during build
@@ -0,0 +1,11 @@
1
+ /**
2
+ * babel plugin for native (metro) builds that enforces environment guard imports.
3
+ *
4
+ * in native builds, `native-only` imports are allowed (removed as no-ops),
5
+ * while `server-only`, `client-only`, and `web-only` imports are replaced
6
+ * with a throw statement.
7
+ */
8
+ import type { PluginObj } from '@babel/core';
9
+ declare function environmentGuardBabelPlugin(_: unknown): PluginObj;
10
+ export default environmentGuardBabelPlugin;
11
+ //# sourceMappingURL=environment-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-guard.d.ts","sourceRoot":"","sources":["../../src/babel-plugins/environment-guard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAe5C,iBAAS,2BAA2B,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,CAwB1D;AAED,eAAe,2BAA2B,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/cli/daemon.ts"],"names":[],"mappings":"AAKA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,iBAsBA;AAkGD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,iBA6C7D"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/cli/daemon.ts"],"names":[],"mappings":"AAKA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,iBAsBA;AAqGD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,iBA6C7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/daemon/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AA6GjC,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,QAKxE;AAGD,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,QAS9D;AAGD,wBAAgB,gBAAgB,SAI/B;AAGD,wBAAgB,oBAAoB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ1D;AAGD,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAU1E;AAyHD,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAA;AAoB9D,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,KAAK,GAAG,IAAI,QAE9D;AA2ID,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB;;;;;;GA8L5D"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/daemon/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AA6GjC,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,QAKxE;AAGD,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,QAS9D;AAGD,wBAAgB,gBAAgB,SAI/B;AAGD,wBAAgB,oBAAoB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ1D;AAGD,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAU1E;AAyHD,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAA;AAqB9D,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,KAAK,GAAG,IAAI,QAE9D;AA2ID,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB;;;;;;GA8L5D"}
@@ -1 +1 @@
1
- {"version":3,"file":"getViteMetroPluginOptions.d.ts","sourceRoot":"","sources":["../../src/metro-config/getViteMetroPluginOptions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAQ1D,wBAAgB,yBAAyB,CAAC,EACxC,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,EAC1B,SAAS,GACV,EAAE;IACD,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,KAAK,CAAC,OAAO,MAAM,EAAE,CAAC,CAAA;IAC1C,0BAA0B,CAAC,EAAE,WAAW,CACtC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAClC,CAAC,wBAAwB,CAAC,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACzE,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAgMpC"}
1
+ {"version":3,"file":"getViteMetroPluginOptions.d.ts","sourceRoot":"","sources":["../../src/metro-config/getViteMetroPluginOptions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAQ1D,wBAAgB,yBAAyB,CAAC,EACxC,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,EAC1B,SAAS,GACV,EAAE;IACD,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,KAAK,CAAC,OAAO,MAAM,EAAE,CAAC,CAAA;IAC1C,0BAA0B,CAAC,EAAE,WAAW,CACtC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAClC,CAAC,wBAAwB,CAAC,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACzE,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAkMpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"one.d.ts","sourceRoot":"","sources":["../../src/vite/one.ts"],"names":[],"mappings":"AASA,OAAO,EAA4B,KAAK,YAAY,EAAE,MAAM,MAAM,CAAA;AAMlE,OAAO,qBAAqB,CAAA;AAc5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAoBlC,wBAAgB,GAAG,CAAC,OAAO,GAAE,GAAG,CAAC,aAAkB,GAAG,YAAY,CA0lBjE"}
1
+ {"version":3,"file":"one.d.ts","sourceRoot":"","sources":["../../src/vite/one.ts"],"names":[],"mappings":"AASA,OAAO,EAA4B,KAAK,YAAY,EAAE,MAAM,MAAM,CAAA;AAMlE,OAAO,qBAAqB,CAAA;AAe5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAoBlC,wBAAgB,GAAG,CAAC,OAAO,GAAE,GAAG,CAAC,aAAkB,GAAG,YAAY,CA4lBjE"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * vite plugin that enforces environment guard imports.
3
+ *
4
+ * bare imports like `import 'server-only'` will either be a no-op (allowed)
5
+ * or throw at build/dev time (forbidden) depending on which vite environment
6
+ * is processing the module.
7
+ *
8
+ * | import | allowed in | throws in |
9
+ * |-----------------|------------------|----------------------------|
10
+ * | server-only | ssr | client, ios, android |
11
+ * | client-only | client | ssr, ios, android |
12
+ * | native-only | ios, android | client, ssr |
13
+ * | web-only | client, ssr | ios, android |
14
+ */
15
+ import type { Plugin } from 'vite';
16
+ /**
17
+ * returns a virtual module id if the specifier is a guard, otherwise null.
18
+ * pure function extracted for testing.
19
+ */
20
+ export declare function resolveEnvironmentGuard(specifier: string, envName: string): string | null;
21
+ /**
22
+ * returns the module source for a virtual guard id.
23
+ * pure function extracted for testing.
24
+ */
25
+ export declare function loadEnvironmentGuard(id: string): string | null;
26
+ export declare function environmentGuardPlugin(): Plugin;
27
+ //# sourceMappingURL=environmentGuardPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environmentGuardPlugin.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/environmentGuardPlugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAqBlC;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAKf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoB9D;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAe/C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=environmentGuardPlugin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environmentGuardPlugin.test.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/environmentGuardPlugin.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"fileSystemRouterPlugin.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/fileSystemRouterPlugin.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAe1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AAU3C,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,GAAG,CAAC,aAAa,GAAG,MAAM,CA8lB/E"}
1
+ {"version":3,"file":"fileSystemRouterPlugin.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/fileSystemRouterPlugin.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAe1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AAiB3C,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,GAAG,CAAC,aAAa,GAAG,MAAM,CAomB/E"}
@@ -382,7 +382,13 @@ export declare namespace One {
382
382
  */
383
383
  seoPreview?: boolean;
384
384
  };
385
- server?: VXRNOptions['server'];
385
+ server?: VXRNOptions['server'] & {
386
+ /**
387
+ * Log HTTP requests to the console
388
+ * @default true
389
+ */
390
+ loggingEnabled?: boolean;
391
+ };
386
392
  /**
387
393
  * Skip loading .env files during build
388
394
  */