one 1.25.12 → 1.25.13

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 (210) hide show
  1. package/dist/cjs/cli/build.cjs +8 -161
  2. package/dist/cjs/cli/build.native.js +8 -204
  3. package/dist/cjs/cli/build.native.js.map +1 -1
  4. package/dist/cjs/createDevHandleRequest.cjs +151 -0
  5. package/dist/cjs/createDevHandleRequest.native.js +194 -0
  6. package/dist/cjs/createDevHandleRequest.native.js.map +1 -0
  7. package/dist/cjs/createHandleRequest.cjs +1 -123
  8. package/dist/cjs/createHandleRequest.native.js +1 -163
  9. package/dist/cjs/createHandleRequest.native.js.map +1 -1
  10. package/dist/cjs/createHandleRequest.test.cjs +25 -25
  11. package/dist/cjs/createHandleRequest.test.native.js +25 -25
  12. package/dist/cjs/createHandleRequest.test.native.js.map +1 -1
  13. package/dist/cjs/layouts/Drawer.cjs +0 -2
  14. package/dist/cjs/layouts/Drawer.native.js.map +1 -1
  15. package/dist/cjs/layouts/Tabs.cjs +0 -2
  16. package/dist/cjs/layouts/Tabs.native.js.map +1 -1
  17. package/dist/cjs/router/web/WebStackView.cjs +0 -2
  18. package/dist/cjs/router/web/WebStackView.native.js +0 -2
  19. package/dist/cjs/router/web/WebStackView.native.js.map +1 -1
  20. package/dist/cjs/serve-worker.cjs +3 -2
  21. package/dist/cjs/serve-worker.native.js +3 -2
  22. package/dist/cjs/serve-worker.native.js.map +1 -1
  23. package/dist/cjs/server/workerHandler.cjs +12 -7
  24. package/dist/cjs/server/workerHandler.native.js +13 -8
  25. package/dist/cjs/server/workerHandler.native.js.map +1 -1
  26. package/dist/cjs/server/workerHandler.test.cjs +25 -0
  27. package/dist/cjs/server/workerHandler.test.native.js +25 -0
  28. package/dist/cjs/server/workerHandler.test.native.js.map +1 -1
  29. package/dist/cjs/vite/cloudflareWranglerConfig.cjs +247 -0
  30. package/dist/cjs/vite/cloudflareWranglerConfig.native.js +392 -0
  31. package/dist/cjs/vite/cloudflareWranglerConfig.native.js.map +1 -0
  32. package/dist/cjs/vite/cloudflareWranglerConfig.test.cjs +116 -0
  33. package/dist/cjs/vite/cloudflareWranglerConfig.test.native.js +120 -0
  34. package/dist/cjs/vite/cloudflareWranglerConfig.test.native.js.map +1 -0
  35. package/dist/cjs/vite/one.cjs +4 -2
  36. package/dist/cjs/vite/one.native.js +8 -6
  37. package/dist/cjs/vite/one.native.js.map +1 -1
  38. package/dist/cjs/vite/plugins/SSRCSSPlugin.cjs +10 -1
  39. package/dist/cjs/vite/plugins/SSRCSSPlugin.native.js +52 -3
  40. package/dist/cjs/vite/plugins/SSRCSSPlugin.native.js.map +1 -1
  41. package/dist/cjs/vite/plugins/environmentGuardPlugin.cjs +1 -1
  42. package/dist/cjs/vite/plugins/environmentGuardPlugin.native.js +1 -1
  43. package/dist/cjs/vite/plugins/environmentGuardPlugin.native.js.map +1 -1
  44. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.cjs +4 -0
  45. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.native.js +4 -0
  46. package/dist/cjs/vite/plugins/environmentGuardPlugin.test.native.js.map +1 -1
  47. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +14 -5
  48. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +15 -5
  49. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  50. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.test.cjs +47 -0
  51. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.test.native.js +47 -0
  52. package/dist/cjs/vite/plugins/fileSystemRouterPlugin.test.native.js.map +1 -1
  53. package/dist/cjs/vite/plugins/virtualEntryPlugin.cjs +2 -1
  54. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js +2 -1
  55. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  56. package/dist/cjs/vite/plugins/virtualEntryPlugin.test.cjs +12 -0
  57. package/dist/cjs/vite/plugins/virtualEntryPlugin.test.native.js +12 -0
  58. package/dist/cjs/vite/plugins/virtualEntryPlugin.test.native.js.map +1 -1
  59. package/dist/cjs/vite/plugins/workerdDevPlugin.cjs +377 -0
  60. package/dist/cjs/vite/plugins/workerdDevPlugin.native.js +493 -0
  61. package/dist/cjs/vite/plugins/workerdDevPlugin.native.js.map +1 -0
  62. package/dist/cjs/vite/plugins/workerdDevPlugin.test.cjs +42 -0
  63. package/dist/cjs/vite/plugins/workerdDevPlugin.test.native.js +45 -0
  64. package/dist/cjs/vite/plugins/workerdDevPlugin.test.native.js.map +1 -0
  65. package/dist/esm/cli/build.mjs +1 -154
  66. package/dist/esm/cli/build.mjs.map +1 -1
  67. package/dist/esm/cli/build.native.js +1 -197
  68. package/dist/esm/cli/build.native.js.map +1 -1
  69. package/dist/esm/createDevHandleRequest.mjs +126 -0
  70. package/dist/esm/createDevHandleRequest.mjs.map +1 -0
  71. package/dist/esm/createDevHandleRequest.native.js +166 -0
  72. package/dist/esm/createDevHandleRequest.native.js.map +1 -0
  73. package/dist/esm/createHandleRequest.mjs +1 -123
  74. package/dist/esm/createHandleRequest.mjs.map +1 -1
  75. package/dist/esm/createHandleRequest.native.js +1 -163
  76. package/dist/esm/createHandleRequest.native.js.map +1 -1
  77. package/dist/esm/createHandleRequest.test.mjs +1 -1
  78. package/dist/esm/createHandleRequest.test.native.js +1 -1
  79. package/dist/esm/fork/useLinking.native.test.mjs +1 -1
  80. package/dist/esm/getURL.native.test.mjs +1 -1
  81. package/dist/esm/layouts/Drawer.mjs +0 -2
  82. package/dist/esm/layouts/Drawer.mjs.map +1 -1
  83. package/dist/esm/layouts/Drawer.native.js.map +1 -1
  84. package/dist/esm/layouts/Tabs.mjs +0 -2
  85. package/dist/esm/layouts/Tabs.mjs.map +1 -1
  86. package/dist/esm/layouts/Tabs.native.js.map +1 -1
  87. package/dist/esm/router/hmrImport.native.test.mjs +1 -1
  88. package/dist/esm/router/routeHmr.native.test.mjs +1 -1
  89. package/dist/esm/router/routeHmr.native.test.mjs.map +1 -1
  90. package/dist/esm/router/web/WebStackView.mjs +0 -2
  91. package/dist/esm/router/web/WebStackView.mjs.map +1 -1
  92. package/dist/esm/router/web/WebStackView.native.js +0 -2
  93. package/dist/esm/router/web/WebStackView.native.js.map +1 -1
  94. package/dist/esm/serve-worker.mjs +3 -2
  95. package/dist/esm/serve-worker.mjs.map +1 -1
  96. package/dist/esm/serve-worker.native.js +3 -2
  97. package/dist/esm/serve-worker.native.js.map +1 -1
  98. package/dist/esm/server/workerHandler.mjs +12 -7
  99. package/dist/esm/server/workerHandler.mjs.map +1 -1
  100. package/dist/esm/server/workerHandler.native.js +13 -8
  101. package/dist/esm/server/workerHandler.native.js.map +1 -1
  102. package/dist/esm/server/workerHandler.test.mjs +25 -0
  103. package/dist/esm/server/workerHandler.test.mjs.map +1 -1
  104. package/dist/esm/server/workerHandler.test.native.js +25 -0
  105. package/dist/esm/server/workerHandler.test.native.js.map +1 -1
  106. package/dist/esm/vite/cloudflareWranglerConfig.mjs +202 -0
  107. package/dist/esm/vite/cloudflareWranglerConfig.mjs.map +1 -0
  108. package/dist/esm/vite/cloudflareWranglerConfig.native.js +344 -0
  109. package/dist/esm/vite/cloudflareWranglerConfig.native.js.map +1 -0
  110. package/dist/esm/vite/cloudflareWranglerConfig.test.mjs +117 -0
  111. package/dist/esm/vite/cloudflareWranglerConfig.test.mjs.map +1 -0
  112. package/dist/esm/vite/cloudflareWranglerConfig.test.native.js +118 -0
  113. package/dist/esm/vite/cloudflareWranglerConfig.test.native.js.map +1 -0
  114. package/dist/esm/vite/one.mjs +4 -2
  115. package/dist/esm/vite/one.mjs.map +1 -1
  116. package/dist/esm/vite/one.native.js +8 -6
  117. package/dist/esm/vite/one.native.js.map +1 -1
  118. package/dist/esm/vite/plugins/SSRCSSPlugin.mjs +10 -1
  119. package/dist/esm/vite/plugins/SSRCSSPlugin.mjs.map +1 -1
  120. package/dist/esm/vite/plugins/SSRCSSPlugin.native.js +52 -3
  121. package/dist/esm/vite/plugins/SSRCSSPlugin.native.js.map +1 -1
  122. package/dist/esm/vite/plugins/environmentGuardPlugin.mjs +1 -1
  123. package/dist/esm/vite/plugins/environmentGuardPlugin.mjs.map +1 -1
  124. package/dist/esm/vite/plugins/environmentGuardPlugin.native.js +1 -1
  125. package/dist/esm/vite/plugins/environmentGuardPlugin.native.js.map +1 -1
  126. package/dist/esm/vite/plugins/environmentGuardPlugin.test.mjs +4 -0
  127. package/dist/esm/vite/plugins/environmentGuardPlugin.test.mjs.map +1 -1
  128. package/dist/esm/vite/plugins/environmentGuardPlugin.test.native.js +4 -0
  129. package/dist/esm/vite/plugins/environmentGuardPlugin.test.native.js.map +1 -1
  130. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +13 -4
  131. package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
  132. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +14 -4
  133. package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
  134. package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.mjs +47 -0
  135. package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.mjs.map +1 -1
  136. package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.native.js +47 -0
  137. package/dist/esm/vite/plugins/fileSystemRouterPlugin.test.native.js.map +1 -1
  138. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs +2 -1
  139. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs.map +1 -1
  140. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js +2 -1
  141. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  142. package/dist/esm/vite/plugins/virtualEntryPlugin.test.mjs +12 -0
  143. package/dist/esm/vite/plugins/virtualEntryPlugin.test.mjs.map +1 -1
  144. package/dist/esm/vite/plugins/virtualEntryPlugin.test.native.js +12 -0
  145. package/dist/esm/vite/plugins/virtualEntryPlugin.test.native.js.map +1 -1
  146. package/dist/esm/vite/plugins/workerdDevPlugin.mjs +349 -0
  147. package/dist/esm/vite/plugins/workerdDevPlugin.mjs.map +1 -0
  148. package/dist/esm/vite/plugins/workerdDevPlugin.native.js +462 -0
  149. package/dist/esm/vite/plugins/workerdDevPlugin.native.js.map +1 -0
  150. package/dist/esm/vite/plugins/workerdDevPlugin.test.mjs +43 -0
  151. package/dist/esm/vite/plugins/workerdDevPlugin.test.mjs.map +1 -0
  152. package/dist/esm/vite/plugins/workerdDevPlugin.test.native.js +43 -0
  153. package/dist/esm/vite/plugins/workerdDevPlugin.test.native.js.map +1 -0
  154. package/package.json +9 -9
  155. package/src/cli/build.ts +7 -192
  156. package/src/createDevHandleRequest.ts +186 -0
  157. package/src/createHandleRequest.test.ts +1 -1
  158. package/src/createHandleRequest.ts +2 -172
  159. package/src/layouts/Drawer.web.tsx +0 -3
  160. package/src/layouts/Tabs.web.tsx +0 -2
  161. package/src/router/web/WebStackView.tsx +0 -3
  162. package/src/serve-worker.ts +6 -1
  163. package/src/server/workerHandler.test.ts +23 -0
  164. package/src/server/workerHandler.ts +15 -7
  165. package/src/vite/cloudflareWranglerConfig.test.ts +101 -0
  166. package/src/vite/cloudflareWranglerConfig.ts +248 -0
  167. package/src/vite/one.ts +11 -1
  168. package/src/vite/plugins/SSRCSSPlugin.ts +17 -2
  169. package/src/vite/plugins/environmentGuardPlugin.test.ts +5 -0
  170. package/src/vite/plugins/environmentGuardPlugin.ts +2 -2
  171. package/src/vite/plugins/fileSystemRouterPlugin.test.ts +44 -0
  172. package/src/vite/plugins/fileSystemRouterPlugin.tsx +20 -8
  173. package/src/vite/plugins/virtualEntryPlugin.test.ts +13 -0
  174. package/src/vite/plugins/virtualEntryPlugin.ts +3 -1
  175. package/src/vite/plugins/workerdDevPlugin.test.ts +44 -0
  176. package/src/vite/plugins/workerdDevPlugin.ts +408 -0
  177. package/types/cli/build.d.ts.map +1 -1
  178. package/types/createDevHandleRequest.d.ts +10 -0
  179. package/types/createDevHandleRequest.d.ts.map +1 -0
  180. package/types/createHandleRequest.d.ts +2 -9
  181. package/types/createHandleRequest.d.ts.map +1 -1
  182. package/types/layouts/Drawer.web.d.ts.map +1 -1
  183. package/types/layouts/Tabs.web.d.ts.map +1 -1
  184. package/types/router/web/WebStackView.d.ts.map +1 -1
  185. package/types/serve-worker.d.ts +3 -1
  186. package/types/serve-worker.d.ts.map +1 -1
  187. package/types/server/workerHandler.d.ts +1 -0
  188. package/types/server/workerHandler.d.ts.map +1 -1
  189. package/types/vite/cloudflareWranglerConfig.d.ts +21 -0
  190. package/types/vite/cloudflareWranglerConfig.d.ts.map +1 -0
  191. package/types/vite/cloudflareWranglerConfig.test.d.ts +2 -0
  192. package/types/vite/cloudflareWranglerConfig.test.d.ts.map +1 -0
  193. package/types/vite/one.d.ts.map +1 -1
  194. package/types/vite/plugins/SSRCSSPlugin.d.ts.map +1 -1
  195. package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
  196. package/types/vite/plugins/virtualEntryPlugin.d.ts.map +1 -1
  197. package/types/vite/plugins/workerdDevPlugin.d.ts +15 -0
  198. package/types/vite/plugins/workerdDevPlugin.d.ts.map +1 -0
  199. package/types/vite/plugins/workerdDevPlugin.test.d.ts +2 -0
  200. package/types/vite/plugins/workerdDevPlugin.test.d.ts.map +1 -0
  201. package/dist/cjs/headless/devHeadlessNote.cjs +0 -34
  202. package/dist/cjs/headless/devHeadlessNote.native.js +0 -37
  203. package/dist/cjs/headless/devHeadlessNote.native.js.map +0 -1
  204. package/dist/esm/headless/devHeadlessNote.mjs +0 -9
  205. package/dist/esm/headless/devHeadlessNote.mjs.map +0 -1
  206. package/dist/esm/headless/devHeadlessNote.native.js +0 -9
  207. package/dist/esm/headless/devHeadlessNote.native.js.map +0 -1
  208. package/src/headless/devHeadlessNote.ts +0 -13
  209. package/types/headless/devHeadlessNote.d.ts +0 -2
  210. package/types/headless/devHeadlessNote.d.ts.map +0 -1
@@ -122,4 +122,27 @@ describe('createWorkerHandler', () => {
122
122
  expect(response).toBeInstanceOf(Response)
123
123
  expect(response?.status).toBe(204)
124
124
  })
125
+
126
+ it('disableModuleCache re-imports the page module on each request', async () => {
127
+ vi.stubEnv('ONE_BUFFERED_SSR', '1')
128
+ let imports = 0
129
+ const handleRequest = createWorkerHandler({
130
+ oneOptions: { web: { defaultRenderMode: 'ssr' } },
131
+ buildInfo,
132
+ disableModuleCache: true,
133
+ lazyRoutes: {
134
+ ...lazyRoutes,
135
+ pages: {
136
+ [pageRoute.file]: async () => {
137
+ imports += 1
138
+ return {}
139
+ },
140
+ },
141
+ },
142
+ }).handleRequest
143
+
144
+ await handleRequest(new Request('https://example.com/some-page'))
145
+ await handleRequest(new Request('https://example.com/some-page'))
146
+ expect(imports).toBeGreaterThan(1)
147
+ })
125
148
  })
@@ -38,10 +38,14 @@ type WorkerHandlerOptions = {
38
38
  oneOptions: One.PluginOptions
39
39
  buildInfo: One.BuildInfo
40
40
  lazyRoutes: LazyRoutes
41
+ // when true, skip the module/loader caches so vite hmr of a route file is
42
+ // visible on the next request. production omits this.
43
+ disableModuleCache?: boolean
41
44
  }
42
45
 
43
46
  export function createWorkerHandler(options: WorkerHandlerOptions) {
44
47
  const { oneOptions } = options
48
+ const disableModuleCache = !!options.disableModuleCache
45
49
 
46
50
  // mutable state for route swapping
47
51
  let currentLazyRoutes = options.lazyRoutes
@@ -137,25 +141,29 @@ export function createWorkerHandler(options: WorkerHandlerOptions) {
137
141
  lazyKey: string | undefined
138
142
  ): Function | null | Promise<Function | null> {
139
143
  const cacheKey = lazyKey || ''
140
- const cached = loaderCache.get(cacheKey)
141
- if (cached !== undefined) return cached
144
+ if (!disableModuleCache) {
145
+ const cached = loaderCache.get(cacheKey)
146
+ if (cached !== undefined) return cached
147
+ }
142
148
 
143
149
  return (async () => {
144
150
  let routeExported: any
145
- if (moduleImportCache.has(cacheKey)) {
151
+ if (!disableModuleCache && moduleImportCache.has(cacheKey)) {
146
152
  routeExported = moduleImportCache.get(cacheKey)
147
153
  } else if (lazyKey && currentLazyRoutes.pages[lazyKey]) {
148
154
  routeExported = await currentLazyRoutes.pages[lazyKey]()
149
- moduleImportCache.set(cacheKey, routeExported)
155
+ if (!disableModuleCache) moduleImportCache.set(cacheKey, routeExported)
150
156
  } else {
151
157
  console.warn(`[one/worker] no lazy route for ${cacheKey}`)
152
- loaderCache.set(cacheKey, null)
158
+ if (!disableModuleCache) loaderCache.set(cacheKey, null)
153
159
  return null
154
160
  }
155
161
 
156
162
  const loader = routeExported?.loader || null
157
- loaderCache.set(cacheKey, loader)
158
- loaderCacheFnMap.set(cacheKey, routeExported?.loaderCache ?? null)
163
+ if (!disableModuleCache) {
164
+ loaderCache.set(cacheKey, loader)
165
+ loaderCacheFnMap.set(cacheKey, routeExported?.loaderCache ?? null)
166
+ }
159
167
  return loader
160
168
  })()
161
169
  }
@@ -0,0 +1,101 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import {
6
+ createCloudflareWranglerConfig,
7
+ isExperimentalWorkerDevEnabled,
8
+ loadUserWranglerConfigSync,
9
+ shouldEnableWorkerdDev,
10
+ } from './cloudflareWranglerConfig'
11
+
12
+ describe('createCloudflareWranglerConfig', () => {
13
+ it('synthesizes a worker config when no user wrangler file exists', () => {
14
+ const config = createCloudflareWranglerConfig('my-app')
15
+ expect(config.name).toBe('my-app')
16
+ expect(config.main).toBe('worker.js')
17
+ expect(config.compatibility_flags).toEqual(['nodejs_compat'])
18
+ expect(config.find_additional_modules).toBe(true)
19
+ expect(config.assets).toEqual({
20
+ directory: 'client',
21
+ binding: 'ASSETS',
22
+ run_worker_first: true,
23
+ })
24
+ })
25
+
26
+ it('merges user wrangler.jsonc the same way the cloudflare build does', () => {
27
+ const config = createCloudflareWranglerConfig('generated-name', {
28
+ name: 'test-cloudflare-user-config',
29
+ compatibility_date: '2025-01-01',
30
+ compatibility_flags: ['nodejs_als'],
31
+ vars: { CUSTOM_VALUE: 'from-root-config' },
32
+ assets: { html_handling: 'auto-trailing-slash' },
33
+ rules: [{ type: 'ESModule', globs: ['./extra/**/*.js'], fallthrough: true }],
34
+ })
35
+
36
+ expect(config.name).toBe('test-cloudflare-user-config')
37
+ expect(config.main).toBe('worker.js')
38
+ expect(config.compatibility_date).toBe('2025-01-01')
39
+ expect(config.compatibility_flags).toEqual(['nodejs_compat', 'nodejs_als'])
40
+ expect(config.vars).toEqual({ CUSTOM_VALUE: 'from-root-config' })
41
+ expect(config.assets).toEqual({
42
+ html_handling: 'auto-trailing-slash',
43
+ directory: 'client',
44
+ binding: 'ASSETS',
45
+ run_worker_first: true,
46
+ })
47
+ expect(config.rules).toEqual([
48
+ { type: 'ESModule', globs: ['./server/**/*.js'], fallthrough: true },
49
+ { type: 'ESModule', globs: ['./api/**/*.js'], fallthrough: true },
50
+ { type: 'ESModule', globs: ['./middlewares/**/*.js'], fallthrough: true },
51
+ { type: 'ESModule', globs: ['./assets/**/*.js'], fallthrough: true },
52
+ { type: 'ESModule', globs: ['./extra/**/*.js'], fallthrough: true },
53
+ ])
54
+ })
55
+ })
56
+
57
+ describe('shouldEnableWorkerdDev', () => {
58
+ const previous = process.env.ONE_EXPERIMENTAL_WORKER_DEV
59
+
60
+ afterEach(() => {
61
+ if (previous === undefined) delete process.env.ONE_EXPERIMENTAL_WORKER_DEV
62
+ else process.env.ONE_EXPERIMENTAL_WORKER_DEV = previous
63
+ })
64
+
65
+ it('is off when the flag is unset', () => {
66
+ delete process.env.ONE_EXPERIMENTAL_WORKER_DEV
67
+ expect(isExperimentalWorkerDevEnabled()).toBe(false)
68
+ expect(shouldEnableWorkerdDev('cloudflare', '/tmp')).toBe(false)
69
+ })
70
+
71
+ it('is off when the flag is on but the app has no cloudflare target or wrangler file', () => {
72
+ process.env.ONE_EXPERIMENTAL_WORKER_DEV = '1'
73
+ const root = mkdtempSync(join(tmpdir(), 'one-workerd-flag-'))
74
+ try {
75
+ expect(shouldEnableWorkerdDev(undefined, root)).toBe(false)
76
+ expect(shouldEnableWorkerdDev('node', root)).toBe(false)
77
+ } finally {
78
+ rmSync(root, { recursive: true, force: true })
79
+ }
80
+ })
81
+
82
+ it('is on when the flag is set and deploy is cloudflare', () => {
83
+ process.env.ONE_EXPERIMENTAL_WORKER_DEV = '1'
84
+ expect(shouldEnableWorkerdDev('cloudflare', '/tmp-does-not-exist')).toBe(true)
85
+ expect(shouldEnableWorkerdDev({ target: 'cloudflare' }, '/tmp-does-not-exist')).toBe(
86
+ true
87
+ )
88
+ })
89
+
90
+ it('is on when the flag is set and wrangler.jsonc exists', () => {
91
+ process.env.ONE_EXPERIMENTAL_WORKER_DEV = '1'
92
+ const root = mkdtempSync(join(tmpdir(), 'one-workerd-wrangler-'))
93
+ try {
94
+ writeFileSync(join(root, 'wrangler.jsonc'), '{ "name": "from-file" }\n')
95
+ expect(shouldEnableWorkerdDev(undefined, root)).toBe(true)
96
+ expect(loadUserWranglerConfigSync(root)?.config.name).toBe('from-file')
97
+ } finally {
98
+ rmSync(root, { recursive: true, force: true })
99
+ }
100
+ })
101
+ })
@@ -0,0 +1,248 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join, relative } from 'node:path'
3
+ import FSExtra from 'fs-extra'
4
+
5
+ export type JsonPrimitive = string | number | boolean | null
6
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }
7
+
8
+ export const GENERATED_CLOUDFLARE_WRANGLER_RULES: JsonValue[] = [
9
+ { type: 'ESModule', globs: ['./server/**/*.js'], fallthrough: true },
10
+ { type: 'ESModule', globs: ['./api/**/*.js'], fallthrough: true },
11
+ { type: 'ESModule', globs: ['./middlewares/**/*.js'], fallthrough: true },
12
+ { type: 'ESModule', globs: ['./assets/**/*.js'], fallthrough: true },
13
+ ]
14
+
15
+ export function isPlainObject(value: unknown): value is Record<string, JsonValue> {
16
+ return !!value && typeof value === 'object' && !Array.isArray(value)
17
+ }
18
+
19
+ function mergeJsonObjects(
20
+ base: Record<string, JsonValue>,
21
+ overrides: Record<string, JsonValue>
22
+ ): Record<string, JsonValue> {
23
+ const merged: Record<string, JsonValue> = { ...base }
24
+
25
+ for (const [key, value] of Object.entries(overrides)) {
26
+ const baseValue = merged[key]
27
+ if (isPlainObject(baseValue) && isPlainObject(value)) {
28
+ merged[key] = mergeJsonObjects(baseValue, value)
29
+ } else {
30
+ merged[key] = value
31
+ }
32
+ }
33
+
34
+ return merged
35
+ }
36
+
37
+ function dedupeJsonValues<T extends JsonValue>(values: T[]): T[] {
38
+ const seen = new Set<string>()
39
+ return values.filter((value) => {
40
+ const key = JSON.stringify(value)
41
+ if (seen.has(key)) return false
42
+ seen.add(key)
43
+ return true
44
+ })
45
+ }
46
+
47
+ function mergeCloudflareCompatibilityFlags(flags: unknown): string[] {
48
+ const userFlags = Array.isArray(flags)
49
+ ? flags.filter((flag): flag is string => typeof flag === 'string')
50
+ : []
51
+
52
+ return dedupeJsonValues<string>(['nodejs_compat', ...userFlags])
53
+ }
54
+
55
+ function mergeCloudflareRules(rules: unknown): JsonValue[] {
56
+ const userRules = Array.isArray(rules)
57
+ ? rules.filter((rule): rule is JsonValue => isPlainObject(rule))
58
+ : []
59
+
60
+ return dedupeJsonValues<JsonValue>([
61
+ ...GENERATED_CLOUDFLARE_WRANGLER_RULES,
62
+ ...userRules,
63
+ ])
64
+ }
65
+
66
+ // minimal JSONC parser: strips line/block comments (string-aware) and trailing
67
+ // commas, then runs JSON.parse. sufficient for small hand-written config files.
68
+ function parseJsonc(text: string): unknown {
69
+ let out = ''
70
+ let i = 0
71
+ let inString = false
72
+ let quote = ''
73
+ while (i < text.length) {
74
+ const ch = text[i]
75
+ const next = text[i + 1]
76
+ if (inString) {
77
+ if (ch === '\\') {
78
+ out += ch + (next ?? '')
79
+ i += 2
80
+ continue
81
+ }
82
+ if (ch === quote) inString = false
83
+ out += ch
84
+ i++
85
+ continue
86
+ }
87
+ if (ch === '"' || ch === "'") {
88
+ inString = true
89
+ quote = ch
90
+ out += ch
91
+ i++
92
+ continue
93
+ }
94
+ if (ch === '/' && next === '/') {
95
+ while (i < text.length && text[i] !== '\n') i++
96
+ continue
97
+ }
98
+ if (ch === '/' && next === '*') {
99
+ i += 2
100
+ while (i < text.length - 1 && !(text[i] === '*' && text[i + 1] === '/')) i++
101
+ i += 2
102
+ continue
103
+ }
104
+ out += ch
105
+ i++
106
+ }
107
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'))
108
+ }
109
+
110
+ const WRANGLER_FILE_NAMES = ['wrangler.jsonc', 'wrangler.json'] as const
111
+
112
+ function wranglerCandidateRoots(root: string): string[] {
113
+ return [...new Set([root, process.cwd()])]
114
+ }
115
+
116
+ export function hasUserWranglerConfig(root: string): boolean {
117
+ for (const candidateRoot of wranglerCandidateRoots(root)) {
118
+ for (const fileName of WRANGLER_FILE_NAMES) {
119
+ if (existsSync(join(candidateRoot, fileName))) return true
120
+ }
121
+ }
122
+ return false
123
+ }
124
+
125
+ function parseUserWranglerFile(
126
+ configPath: string
127
+ ): { path: string; config: Record<string, JsonValue> } {
128
+ const contents = readFileSync(configPath, 'utf-8')
129
+ let parsed: unknown
130
+ try {
131
+ parsed = parseJsonc(contents)
132
+ } catch (err) {
133
+ throw new Error(
134
+ `Failed to parse ${relative(process.cwd(), configPath)}: ${(err as Error).message}`
135
+ )
136
+ }
137
+
138
+ if (!isPlainObject(parsed)) {
139
+ throw new Error(
140
+ `Expected ${relative(process.cwd(), configPath)} to contain a top-level JSON object`
141
+ )
142
+ }
143
+
144
+ return {
145
+ path: configPath,
146
+ config: parsed,
147
+ }
148
+ }
149
+
150
+ export function loadUserWranglerConfigSync(
151
+ root: string
152
+ ): { path: string; config: Record<string, JsonValue> } | null {
153
+ for (const candidateRoot of wranglerCandidateRoots(root)) {
154
+ for (const fileName of WRANGLER_FILE_NAMES) {
155
+ const configPath = join(candidateRoot, fileName)
156
+ if (!existsSync(configPath)) continue
157
+ return parseUserWranglerFile(configPath)
158
+ }
159
+ }
160
+ return null
161
+ }
162
+
163
+ export async function loadUserWranglerConfig(
164
+ root: string
165
+ ): Promise<{ path: string; config: Record<string, JsonValue> } | null> {
166
+ const candidateRoots = wranglerCandidateRoots(root)
167
+
168
+ for (const candidateRoot of candidateRoots) {
169
+ for (const fileName of WRANGLER_FILE_NAMES) {
170
+ const configPath = join(candidateRoot, fileName)
171
+ if (!(await FSExtra.pathExists(configPath))) {
172
+ continue
173
+ }
174
+ return parseUserWranglerFile(configPath)
175
+ }
176
+ }
177
+
178
+ return null
179
+ }
180
+
181
+ export function createCloudflareWranglerConfig(
182
+ projectName: string,
183
+ userConfig?: Record<string, JsonValue>
184
+ ): Record<string, JsonValue> {
185
+ const generatedConfig: Record<string, JsonValue> = {
186
+ name: projectName,
187
+ main: 'worker.js',
188
+ compatibility_date: '2024-12-05',
189
+ compatibility_flags: ['nodejs_compat'],
190
+ find_additional_modules: true,
191
+ rules: GENERATED_CLOUDFLARE_WRANGLER_RULES,
192
+ assets: {
193
+ directory: 'client',
194
+ binding: 'ASSETS',
195
+ run_worker_first: true,
196
+ },
197
+ }
198
+
199
+ const mergedConfig = userConfig
200
+ ? mergeJsonObjects(generatedConfig, userConfig)
201
+ : generatedConfig
202
+
203
+ mergedConfig.main = 'worker.js'
204
+ mergedConfig.find_additional_modules = true
205
+ mergedConfig.compatibility_flags = mergeCloudflareCompatibilityFlags(
206
+ mergedConfig.compatibility_flags
207
+ )
208
+ mergedConfig.rules = mergeCloudflareRules(mergedConfig.rules)
209
+ mergedConfig.assets = {
210
+ ...(isPlainObject(mergedConfig.assets) ? mergedConfig.assets : {}),
211
+ directory: 'client',
212
+ binding: 'ASSETS',
213
+ run_worker_first: true,
214
+ }
215
+
216
+ return mergedConfig
217
+ }
218
+
219
+ export function getCloudflareProjectNameSync(root: string): string {
220
+ try {
221
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'))
222
+ if (pkg.name) {
223
+ return String(pkg.name).replace(/^@[^/]+\//, '')
224
+ }
225
+ } catch {}
226
+ return 'one-app'
227
+ }
228
+
229
+ export async function getCloudflareProjectName(root: string): Promise<string> {
230
+ try {
231
+ const pkg = JSON.parse(await FSExtra.readFile(join(root, 'package.json'), 'utf-8'))
232
+ if (pkg.name) {
233
+ return pkg.name.replace(/^@[^/]+\//, '')
234
+ }
235
+ } catch {}
236
+ return 'one-app'
237
+ }
238
+
239
+ export function isExperimentalWorkerDevEnabled(): boolean {
240
+ return process.env.ONE_EXPERIMENTAL_WORKER_DEV === '1'
241
+ }
242
+
243
+ export function shouldEnableWorkerdDev(deploy: unknown, root: string): boolean {
244
+ if (!isExperimentalWorkerDevEnabled()) return false
245
+ const target = typeof deploy === 'string' ? deploy : (deploy as { target?: string } | undefined)?.target
246
+ if (target === 'cloudflare') return true
247
+ return hasUserWranglerConfig(root)
248
+ }
package/src/vite/one.ts CHANGED
@@ -34,6 +34,7 @@ import { SSRCSSPlugin } from './plugins/SSRCSSPlugin'
34
34
  import { virtualEntryId } from './plugins/virtualEntryConstants'
35
35
  import { createVirtualEntry } from './plugins/virtualEntryPlugin'
36
36
  import { environmentGuardPlugin } from './plugins/environmentGuardPlugin'
37
+ import { createWorkerdDevPlugins, shouldEnableWorkerdDev } from './plugins/workerdDevPlugin'
37
38
  import type { One } from './types'
38
39
 
39
40
  type MetroOptions = MetroPluginOptions
@@ -553,7 +554,11 @@ export function one(options: One.PluginOptions = {}): PluginOption {
553
554
  resolveId: {
554
555
  filter: { id: RN_DEEP_IMPORT_RE },
555
556
  handler(source) {
556
- if (this.environment?.name === 'client' || this.environment?.name === 'ssr') {
557
+ if (
558
+ this.environment?.name === 'client' ||
559
+ this.environment?.name === 'ssr' ||
560
+ this.environment?.name === 'worker'
561
+ ) {
557
562
  return '\0rn-empty-module'
558
563
  }
559
564
  },
@@ -867,7 +872,12 @@ export function one(options: One.PluginOptions = {}): PluginOption {
867
872
  return inspector ? sourceInspectorPlugin({ editor }) : []
868
873
  })()
869
874
 
875
+ const workerdDevPlugins = shouldEnableWorkerdDev(options.web?.deploy, root)
876
+ ? createWorkerdDevPlugins(options, root, routeIndex)
877
+ : []
878
+
870
879
  return [
880
+ ...workerdDevPlugins,
871
881
  ...vxrnPlugins,
872
882
  ...devAndProdPlugins,
873
883
  ...inspectorPlugins,
@@ -1,4 +1,4 @@
1
- import type { Plugin, ViteDevServer } from 'vite'
1
+ import type { ModuleNode, Plugin, ViteDevServer } from 'vite'
2
2
  import { VIRTUAL_SSR_CSS_ENTRY, VIRTUAL_SSR_CSS_HREF } from '../../constants'
3
3
 
4
4
  // thanks to hi-ogawa https://github.com/hi-ogawa/vite-plugins/tree/main/packages/ssr-css
@@ -152,6 +152,21 @@ async function collectStyleUrls(
152
152
  ): Promise<string[]> {
153
153
  const visited = new Set<string>()
154
154
 
155
+ // every module in importedModules already carries the id vite resolved when
156
+ // it entered the graph, so walk them directly. resolveUrl runs the whole
157
+ // plugin container and was awaited once per import EDGE rather than once per
158
+ // module, which on a large app is the entire cost of serving this stylesheet.
159
+ // the walk is an explicit stack so a deep import chain cannot overflow.
160
+ function walk(root: ModuleNode) {
161
+ const stack = [root]
162
+ while (stack.length) {
163
+ const mod = stack.pop()!
164
+ if (!mod.id || mod.url.includes('.server.') || visited.has(mod.id)) continue
165
+ visited.add(mod.id)
166
+ for (const childMod of mod.importedModules) stack.push(childMod)
167
+ }
168
+ }
169
+
155
170
  async function traverse(url: string) {
156
171
  if (url.includes('.server.')) return
157
172
  const [, id] = await server.moduleGraph.resolveUrl(url)
@@ -163,7 +178,7 @@ async function collectStyleUrls(
163
178
  if (!mod) {
164
179
  return
165
180
  }
166
- await Promise.all([...mod.importedModules].map((childMod) => traverse(childMod.url)))
181
+ for (const childMod of mod.importedModules) walk(childMod)
167
182
  }
168
183
 
169
184
  // ensure vite's import analysis is ready _only_ for top entries to not go too aggresive
@@ -135,6 +135,11 @@ describe('environmentGuardPlugin', () => {
135
135
  expect(result).toBe('export {}')
136
136
  })
137
137
 
138
+ it('web-only: allowed in worker', () => {
139
+ const result = loadEnvironmentGuard('\0one-env-guard:web-only:worker:server')
140
+ expect(result).toBe('export {}')
141
+ })
142
+
138
143
  it('web-only: forbidden in ios', () => {
139
144
  const result = loadEnvironmentGuard('\0one-env-guard:web-only:ios:unknown')
140
145
  expect(result).toContain('throw new Error')
@@ -35,11 +35,11 @@ const GUARD_SPECIFIERS = [
35
35
  const GUARD_SPECIFIERS_RE = new RegExp(`^(?:${GUARD_SPECIFIERS.join('|')})$`)
36
36
 
37
37
  type GuardSpecifier = (typeof GUARD_SPECIFIERS)[number]
38
- type ViteEnvironment = 'client' | 'ssr' | 'ios' | 'android'
38
+ type ViteEnvironment = 'client' | 'ssr' | 'ios' | 'android' | 'worker'
39
39
 
40
40
  const ALLOWED_BY_NAME: Partial<Record<GuardSpecifier, readonly ViteEnvironment[]>> = {
41
41
  'native-only': ['ios', 'android'],
42
- 'web-only': ['client', 'ssr'],
42
+ 'web-only': ['client', 'ssr', 'worker'],
43
43
  }
44
44
 
45
45
  const ALLOWED_BY_CONSUMER: Partial<Record<GuardSpecifier, 'server' | 'client'>> = {
@@ -124,6 +124,50 @@ describe('createFileSystemRouterPlugin', () => {
124
124
  )
125
125
  })
126
126
 
127
+ it('does not install the node request middleware when a worker environment exists', async () => {
128
+ process.env.IS_VXRN_CLI = '1'
129
+ tempRoot = mkdtempSync(path.join(tmpdir(), 'one-router-worker-'))
130
+ const appDir = path.join(tempRoot, 'app')
131
+ writeFileSync(path.join(tempRoot, 'package.json'), '{}\n')
132
+ mkdirSync(appDir)
133
+ writeFileSync(
134
+ path.join(appDir, 'index.tsx'),
135
+ 'export default function Index() { return null }\n'
136
+ )
137
+
138
+ ;(globalThis as any).__vxrnPluginConfig__ = {
139
+ web: {
140
+ defaultRenderMode: 'ssg',
141
+ },
142
+ }
143
+
144
+ const { createRouteIndex } = await import('../../utils/routeIndex')
145
+ const { createFileSystemRouterPlugin } = await import('./fileSystemRouterPlugin')
146
+ const plugin = createFileSystemRouterPlugin(
147
+ {
148
+ router: { root: appDir },
149
+ },
150
+ createRouteIndex({ routerRoot: appDir })
151
+ )
152
+
153
+ const use = vi.fn()
154
+ const server = {
155
+ environments: {
156
+ ssr: {},
157
+ worker: { moduleGraph: { getModuleById: vi.fn() } },
158
+ },
159
+ middlewares: { use },
160
+ watcher: {
161
+ addListener: vi.fn(),
162
+ on: vi.fn(),
163
+ },
164
+ }
165
+
166
+ const installMiddlewares = (plugin as any).configureServer(server)
167
+ installMiddlewares()
168
+ expect(use).not.toHaveBeenCalled()
169
+ })
170
+
127
171
  it('caches dev ssg html until the module runner is invalidated', async () => {
128
172
  process.env.VITE_ENVIRONMENT = 'ssr'
129
173
  tempRoot = mkdtempSync(path.join(tmpdir(), 'one-router-ssg-cache-'))
@@ -7,7 +7,7 @@ import type { Connect, Plugin, ViteDevServer } from 'vite'
7
7
  import { createServerModuleRunner, normalizePath } from 'vite'
8
8
  import type { ModuleRunner } from 'vite/module-runner'
9
9
  import { getSpaHeaderElements } from '../../constants'
10
- import { createHandleRequest } from '../../createHandleRequest'
10
+ import { createHandleRequest } from '../../createDevHandleRequest'
11
11
  import type { RouteNode } from '../../router/Route' // used for type in runLoaderWithTracking
12
12
  import type { RenderAppProps } from '../../types'
13
13
  import { getPageExport } from '../../utils/getPageExport'
@@ -42,9 +42,6 @@ const routeTypeColors: Record<string, (s: string) => string> = {
42
42
  api: colors.magenta,
43
43
  }
44
44
 
45
- // server needs better dep optimization
46
- const USE_SERVER_ENV = false //!!process.env.USE_SERVER_ENV
47
-
48
45
  export function createFileSystemRouterPlugin(
49
46
  options: One.PluginOptions,
50
47
  routeIndex: RouteIndex
@@ -615,6 +612,11 @@ export function createFileSystemRouterPlugin(
615
612
  handleRequest = createRequestHandler()
616
613
  ssgHtmlCache.clear()
617
614
  needsRouteRefresh = true
615
+ const workerGraph = server?.environments.worker?.moduleGraph
616
+ const workerEntry = workerGraph?.getModuleById('\0virtual:one-workerd-dev-entry')
617
+ if (workerGraph && workerEntry) {
618
+ workerGraph.invalidateModule(workerEntry)
619
+ }
618
620
  } catch (error) {
619
621
  console.warn(`[one] Failed to rebuild routes after ${changedPath} changed.`, error)
620
622
  }
@@ -708,10 +710,14 @@ export function createFileSystemRouterPlugin(
708
710
  preloads = ['/bundledDevClient.mjs', '/assets/_virtual_one-entry.js']
709
711
  }
710
712
 
711
- // change this to .server to test using the indepedently scoped env
712
- runner = createServerModuleRunner(
713
- USE_SERVER_ENV ? server.environments.server : server.environments.ssr
714
- )
713
+ const workerDev = !!server.environments.worker
714
+
715
+ // workerd-backed worker env handles web requests via @cloudflare/vite-plugin
716
+ // middleware (node req/res <-> fetch). the node module runner is only for
717
+ // the default (flag off) path.
718
+ if (!workerDev) {
719
+ runner = createServerModuleRunner(server.environments.ssr)
720
+ }
715
721
 
716
722
  const appDir = path.resolve(process.cwd(), getRouterRootFromOneOptions(options))
717
723
 
@@ -762,6 +768,12 @@ export function createFileSystemRouterPlugin(
762
768
  // Vite's transform middleware so that we can focus on handling the requests
763
769
  // we're interested in.
764
770
  return () => {
771
+ if (workerDev) {
772
+ // @cloudflare/vite-plugin already dispatches the request into workerd.
773
+ // do not run the node handler alongside it.
774
+ return
775
+ }
776
+
765
777
  // packager status fallback — runs after one's routing so user-defined
766
778
  // /status routes, +api handlers, and custom middleware take priority.
767
779
  // only fires if nothing else handled the request.
@@ -194,6 +194,19 @@ export const registerPreloadedRoute = () => {}`
194
194
  expect(code).not.toContain('__oneGetSetupPromise')
195
195
  })
196
196
 
197
+ it('worker environment uses the server setupFile like ssr', () => {
198
+ const plugin = createVirtualEntry({
199
+ ...base,
200
+ setupFile: {
201
+ client: './src/setupClient.ts',
202
+ server: './src/setupServer.ts',
203
+ },
204
+ })
205
+ const code = loadEntry(plugin, 'worker', 'serve')
206
+ expect(code).toContain('import "./src/setupServer.ts"')
207
+ expect(code).not.toContain('__oneGetSetupPromise')
208
+ })
209
+
197
210
  it('no setupFile produces no import', () => {
198
211
  const plugin = createVirtualEntry(base)
199
212
  const code = loadEntry(plugin, 'ssr')
@@ -88,6 +88,7 @@ function getSetupFileImport(
88
88
  const envMap: Record<string, keyof NormalizedSetupFiles> = {
89
89
  client: 'client',
90
90
  ssr: 'server',
91
+ worker: 'server',
91
92
  ios: 'ios',
92
93
  android: 'android',
93
94
  }
@@ -170,7 +171,8 @@ export function createVirtualEntry(options: {
170
171
  handler(id) {
171
172
  if (id === resolvedVirtualEntryId) {
172
173
  const isNative = isNativeEnvironment(this.environment)
173
- const isSSR = this.environment.name === 'ssr'
174
+ const isSSR =
175
+ this.environment.name === 'ssr' || this.environment.name === 'worker'
174
176
  const serverSpaRouteFiles = isSSR
175
177
  ? options.routeIndex
176
178
  .getPaths()