vxrn 1.21.6 → 1.21.8

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 (68) hide show
  1. package/dist/config/getOptionsFilled.mjs +3 -5
  2. package/dist/config/getOptionsFilled.mjs.map +1 -1
  3. package/dist/config/getOptionsFilled.native.js +3 -5
  4. package/dist/config/getOptionsFilled.native.js.map +1 -1
  5. package/dist/config/getServerOptionsFilled.mjs +3 -2
  6. package/dist/config/getServerOptionsFilled.mjs.map +1 -1
  7. package/dist/config/getServerOptionsFilled.native.js +3 -2
  8. package/dist/config/getServerOptionsFilled.native.js.map +1 -1
  9. package/dist/config/getServerOptionsFilled.test.mjs +20 -0
  10. package/dist/config/getServerOptionsFilled.test.mjs.map +1 -0
  11. package/dist/config/getServerOptionsFilled.test.native.js +20 -0
  12. package/dist/config/getServerOptionsFilled.test.native.js.map +1 -0
  13. package/dist/config/getViteServerConfig.mjs +1 -0
  14. package/dist/config/getViteServerConfig.mjs.map +1 -1
  15. package/dist/config/getViteServerConfig.native.js +1 -0
  16. package/dist/config/getViteServerConfig.native.js.map +1 -1
  17. package/dist/exports/clean.mjs +84 -16
  18. package/dist/exports/clean.mjs.map +1 -1
  19. package/dist/exports/clean.native.js +89 -16
  20. package/dist/exports/clean.native.js.map +1 -1
  21. package/dist/exports/clean.test.mjs +81 -0
  22. package/dist/exports/clean.test.mjs.map +1 -0
  23. package/dist/exports/clean.test.native.js +88 -0
  24. package/dist/exports/clean.test.native.js.map +1 -0
  25. package/dist/exports/dev.mjs +14 -3
  26. package/dist/exports/dev.mjs.map +1 -1
  27. package/dist/exports/dev.native.js +14 -3
  28. package/dist/exports/dev.native.js.map +1 -1
  29. package/dist/serve/node.mjs +20 -24
  30. package/dist/serve/node.mjs.map +1 -1
  31. package/dist/serve/node.native.js +20 -22
  32. package/dist/serve/node.native.js.map +1 -1
  33. package/dist/serve/node.test.mjs +60 -0
  34. package/dist/serve/node.test.mjs.map +1 -0
  35. package/dist/serve/node.test.native.js +72 -0
  36. package/dist/serve/node.test.native.js.map +1 -0
  37. package/dist/utils/state.mjs +11 -1
  38. package/dist/utils/state.mjs.map +1 -1
  39. package/dist/utils/state.native.js +11 -1
  40. package/dist/utils/state.native.js.map +1 -1
  41. package/package.json +11 -11
  42. package/src/config/getOptionsFilled.ts +2 -4
  43. package/src/config/getServerOptionsFilled.test.ts +24 -0
  44. package/src/config/getServerOptionsFilled.ts +2 -1
  45. package/src/config/getViteServerConfig.ts +1 -0
  46. package/src/exports/clean.test.ts +82 -0
  47. package/src/exports/clean.ts +90 -26
  48. package/src/exports/dev.ts +19 -3
  49. package/src/serve/node.test.ts +63 -0
  50. package/src/serve/node.ts +22 -26
  51. package/src/types.ts +4 -2
  52. package/src/utils/state.ts +9 -1
  53. package/types/config/getOptionsFilled.d.ts +2 -0
  54. package/types/config/getOptionsFilled.d.ts.map +1 -1
  55. package/types/config/getServerOptionsFilled.d.ts.map +1 -1
  56. package/types/config/getServerOptionsFilled.test.d.ts.map +1 -0
  57. package/types/config/getViteServerConfig.d.ts.map +1 -1
  58. package/types/exports/build.d.ts +1 -0
  59. package/types/exports/build.d.ts.map +1 -1
  60. package/types/exports/clean.d.ts +6 -9
  61. package/types/exports/clean.d.ts.map +1 -1
  62. package/types/exports/clean.test.d.ts.map +1 -0
  63. package/types/exports/dev.d.ts.map +1 -1
  64. package/types/serve/node.d.ts.map +1 -1
  65. package/types/serve/node.test.d.ts.map +1 -0
  66. package/types/types.d.ts +3 -1
  67. package/types/types.d.ts.map +1 -1
  68. package/types/utils/state.d.ts.map +1 -1
@@ -1,4 +1,6 @@
1
1
  import FSExtra from "fs-extra";
2
+ import { randomUUID } from "crypto";
3
+ import { rename, rm } from "fs/promises";
2
4
  import { join } from "path";
3
5
  async function readState(cacheDir) {
4
6
  var statePath = join(cacheDir, "state.json");
@@ -11,8 +13,16 @@ async function readState(cacheDir) {
11
13
  }
12
14
  async function writeState(cacheDir, state) {
13
15
  var statePath = join(cacheDir, "state.json");
16
+ var pendingStatePath = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
14
17
  await FSExtra.ensureDir(cacheDir);
15
- await FSExtra.writeJSON(statePath, state);
18
+ try {
19
+ await FSExtra.writeJSON(pendingStatePath, state);
20
+ await rename(pendingStatePath, statePath);
21
+ } finally {
22
+ await rm(pendingStatePath, {
23
+ force: true
24
+ });
25
+ }
16
26
  }
17
27
  export { readState, writeState };
18
28
  //# sourceMappingURL=state.native.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["FSExtra","join","readState","cacheDir","statePath","state","pathExists","readJSON","unused","writeState","ensureDir","writeJSON"],"sources":["../../src/utils/state.ts"],"sourcesContent":[null],"mappings":"AAAA,OAAOA,OAAA,MAAa;AACpB,SAASC,IAAA,QAAY;AAMrB,eAAsBC,UAAUC,QAAA,EAAkB;EAChD,IAAAC,SAAM,GAAAH,IAAY,CAAAE,QAAK,cAAU;EACjC,IAAI;IACF,IAAAE,KAAM,UAAgBL,OAAM,CAAAM,UAAQ,CAAAF,SAAW,KAAS,MACpDJ,OAAM,CAAAO,QAAQ,CAAAH,SAAS,IAAS,EAChC;IACJ,OAAOC,KAAA;EACT,SAAQG,MAAA;IAEN,OAAO,CAAC;EACV;AACF;AAEA,eAAsBC,WAAWN,QAAA,EAAkBE,KAAA,EAAc;EAC/D,IAAAD,SAAM,GAAAH,IAAY,CAAAE,QAAK,cAAU;EACjC,MAAMH,OAAA,CAAQU,SAAA,CAAUP,QAAQ;EAChC,MAAMH,OAAA,CAAQW,SAAA,CAAUP,SAAA,EAAWC,KAAK;AAC1C","ignoreList":[]}
1
+ {"version":3,"names":["FSExtra","randomUUID","rename","rm","join","readState","cacheDir","statePath","state","pathExists","readJSON","unused","writeState","pendingStatePath","process","pid","ensureDir","writeJSON","force"],"sources":["../../src/utils/state.ts"],"sourcesContent":[null],"mappings":"AAAA,OAAOA,OAAA,MAAa;AACpB,SAASC,UAAA,QAAkB;AAC3B,SAASC,MAAA,EAAQC,EAAA,QAAU;AAC3B,SAASC,IAAA,QAAY;AAMrB,eAAsBC,UAAUC,QAAA,EAAkB;EAChD,IAAAC,SAAM,GAAAH,IAAY,CAAAE,QAAK,cAAU;EACjC,IAAI;IACF,IAAAE,KAAM,UAAgBR,OAAM,CAAAS,UAAQ,CAAAF,SAAW,KAAS,MACpDP,OAAM,CAAAU,QAAQ,CAAAH,SAAS,IAAS,EAChC;IACJ,OAAOC,KAAA;EACT,SAAQG,MAAA;IAEN,OAAO,CAAC;EACV;AACF;AAEA,eAAsBC,WAAWN,QAAA,EAAkBE,KAAA,EAAc;EAC/D,IAAAD,SAAM,GAAAH,IAAY,CAAAE,QAAK,cAAU;EACjC,IAAAO,gBAAM,MAAmBN,SAAG,IAASO,OAAI,CAAAC,GAAQ,IAAGd,UAAI,QAAY;EACpE,MAAMD,OAAA,CAAQgB,SAAA,CAAUV,QAAQ;EAChC,IAAI;IACF,MAAMN,OAAA,CAAQiB,SAAA,CAAUJ,gBAAA,EAAkBL,KAAK;IAC/C,MAAMN,MAAA,CAAOW,gBAAA,EAAkBN,SAAS;EAC1C,UAAE;IACA,MAAMJ,EAAA,CAAGU,gBAAA,EAAkB;MAC7BK,KAAA;IACF","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vxrn",
3
- "version": "1.21.6",
3
+ "version": "1.21.8",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,16 +60,16 @@
60
60
  "@expo/config-plugins": "~55.0.6",
61
61
  "@hono/node-server": "^1.19.14",
62
62
  "@react-native/dev-middleware": "^0.83.4",
63
- "@vxrn/compiler": "1.21.6",
64
- "@vxrn/debug": "1.21.6",
65
- "@vxrn/query-string": "1.21.6",
66
- "@vxrn/resolve": "1.21.6",
67
- "@vxrn/safe-area": "1.21.6",
68
- "@vxrn/url-parse": "1.21.6",
69
- "@vxrn/utils": "1.21.6",
70
- "@vxrn/vendor": "1.21.6",
71
- "@vxrn/vite-flow": "1.21.6",
72
- "@vxrn/vite-plugin-metro": "1.21.6",
63
+ "@vxrn/compiler": "1.21.8",
64
+ "@vxrn/debug": "1.21.8",
65
+ "@vxrn/query-string": "1.21.8",
66
+ "@vxrn/resolve": "1.21.8",
67
+ "@vxrn/safe-area": "1.21.8",
68
+ "@vxrn/url-parse": "1.21.8",
69
+ "@vxrn/utils": "1.21.8",
70
+ "@vxrn/vendor": "1.21.8",
71
+ "@vxrn/vite-flow": "1.21.8",
72
+ "@vxrn/vite-plugin-metro": "1.21.8",
73
73
  "citty": "^0.1.6",
74
74
  "dotenv": "^17.2.1",
75
75
  "dotenv-expand": "^12.0.2",
@@ -5,7 +5,7 @@ import { join } from 'node:path'
5
5
  import { readPackageJSON } from 'pkg-types'
6
6
  import type { Mode, VXRNOptions } from '../types'
7
7
  import { getServerOptionsFilled } from './getServerOptionsFilled'
8
- import { readState, writeState } from '../utils/state'
8
+ import { readState } from '../utils/state'
9
9
  import { getCacheDir } from '../utils/getCacheDir'
10
10
 
11
11
  const require = createRequire(import.meta.url)
@@ -71,9 +71,6 @@ export async function fillOptions(
71
71
  ? 'vite'
72
72
  : false
73
73
 
74
- // no need to wait to write state
75
- void writeState(cacheDir, { versionHash })
76
-
77
74
  if (typeof options.build?.server !== 'boolean' && !options.build?.server) {
78
75
  // default building server to off
79
76
  options.build ||= {}
@@ -106,6 +103,7 @@ export async function fillOptions(
106
103
  state,
107
104
  packageRootDir,
108
105
  cacheDir,
106
+ versionHash,
109
107
  } as const
110
108
 
111
109
  optionsFilled = final
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { getServerOptionsFilled } from './getServerOptionsFilled'
3
+
4
+ describe('getServerOptionsFilled', () => {
5
+ test('uses the configured TLS protocol in the public server URL', async () => {
6
+ const server = await getServerOptionsFilled(
7
+ {
8
+ host: '127.0.0.1',
9
+ https: {
10
+ cert: 'certificate',
11
+ key: 'private-key',
12
+ },
13
+ },
14
+ 'dev'
15
+ )
16
+
17
+ expect(server.protocol).toBe('https:')
18
+ expect(server.url).toBe(`https://127.0.0.1:${server.port}`)
19
+ expect(server.https).toEqual({
20
+ cert: 'certificate',
21
+ key: 'private-key',
22
+ })
23
+ })
24
+ })
@@ -8,6 +8,7 @@ export async function getServerOptionsFilled(
8
8
  const {
9
9
  host = '0.0.0.0' /* TODO: Better default to 127.0.0.1 due to security reasons, and only dynamically change to 0.0.0.0 if the user is requesting an Expo QR code */,
10
10
  port: requestedPort,
11
+ https,
11
12
  } = serverOptions || {}
12
13
 
13
14
  const envPort = process.env.ONE_PORT ? Number(process.env.ONE_PORT) : undefined
@@ -16,7 +17,7 @@ export async function getServerOptionsFilled(
16
17
  : undefined
17
18
  const defaultPort = mode === 'dev' ? 8081 : 3000
18
19
  const portToUse = forcePort ?? requestedPort ?? envPort ?? defaultPort
19
- const protocol = 'http:' as const
20
+ const protocol = https ? ('https:' as const) : ('http:' as const)
20
21
 
21
22
  // in cluster worker mode with SO_REUSEPORT, skip the port check —
22
23
  // multiple processes intentionally bind the same port
@@ -30,6 +30,7 @@ export async function getViteServerConfig(
30
30
  path: '/__vxrnhmr',
31
31
  },
32
32
  host: server.host,
33
+ https: server.https,
33
34
  port: server.port,
34
35
  },
35
36
  root,
@@ -0,0 +1,82 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+ import { promisify } from 'node:util'
7
+ import { afterEach, describe, expect, it } from 'vitest'
8
+
9
+ const execFileAsync = promisify(execFile)
10
+
11
+ describe('prepareCacheForVersion', () => {
12
+ let root: string | undefined
13
+
14
+ afterEach(async () => {
15
+ if (root) {
16
+ await rm(root, { recursive: true, force: true })
17
+ }
18
+ })
19
+
20
+ it('lets one process clean a stale same-root cache', async () => {
21
+ root = await mkdtemp(join(tmpdir(), 'vxrn-concurrent-clean-'))
22
+ const cacheDir = join(root, 'node_modules', '.vxrn')
23
+ const viteDir = join(root, 'node_modules', '.vite')
24
+ const participantsDir = join(root, 'participants')
25
+ const participantCount = 4
26
+
27
+ await Promise.all([
28
+ mkdir(cacheDir, { recursive: true }),
29
+ mkdir(viteDir, { recursive: true }),
30
+ mkdir(participantsDir, { recursive: true }),
31
+ ])
32
+ await Promise.all([
33
+ writeFile(join(cacheDir, 'state.json'), JSON.stringify({ versionHash: 'old' })),
34
+ writeFile(join(cacheDir, 'stale'), 'stale'),
35
+ writeFile(join(viteDir, 'stale'), 'stale'),
36
+ ])
37
+
38
+ const moduleUrl = pathToFileURL(join(dirname(import.meta.filename), 'clean.ts')).href
39
+ const workerScript = (participant: number) => `
40
+ import { readdir, writeFile } from 'node:fs/promises'
41
+ import { join } from 'node:path'
42
+ import { prepareCacheForVersion } from ${JSON.stringify(moduleUrl)}
43
+
44
+ const root = ${JSON.stringify(root)}
45
+ const participantsDir = ${JSON.stringify(participantsDir)}
46
+ await writeFile(join(participantsDir, ${JSON.stringify(String(participant))}), '')
47
+ while ((await readdir(participantsDir)).length < ${participantCount}) {
48
+ await Bun.sleep(5)
49
+ }
50
+ const cleaned = await prepareCacheForVersion({
51
+ root,
52
+ cacheDir: join(root, 'node_modules', '.vxrn'),
53
+ versionHash: 'new',
54
+ })
55
+ console.info(cleaned ? 'cleaned-by-worker' : 'cache-already-current')
56
+ `
57
+
58
+ const results = await Promise.all(
59
+ Array.from({ length: participantCount }, (_, participant) =>
60
+ execFileAsync('bun', ['-e', workerScript(participant)], {
61
+ env: { ...process.env, VXRN_DONT_CLEAN_SELF: '' },
62
+ })
63
+ )
64
+ )
65
+ const output = results.map(({ stdout }) => stdout).join('\n')
66
+
67
+ expect(output.match(/\[vxrn\] cleaning/g)).toHaveLength(1)
68
+ expect(output.match(/cleaned-by-worker/g)).toHaveLength(1)
69
+ expect(JSON.parse(await readFile(join(cacheDir, 'state.json'), 'utf8'))).toEqual({
70
+ versionHash: 'new',
71
+ })
72
+ await expect(readFile(join(cacheDir, 'stale'))).rejects.toMatchObject({
73
+ code: 'ENOENT',
74
+ })
75
+ await expect(readFile(join(viteDir, 'stale'))).rejects.toMatchObject({
76
+ code: 'ENOENT',
77
+ })
78
+ await expect(
79
+ readFile(join(root, 'node_modules', '.vxrn-clean.lock'))
80
+ ).rejects.toMatchObject({ code: 'ENOENT' })
81
+ }, 20_000)
82
+ })
@@ -1,39 +1,73 @@
1
- import FSExtra from 'fs-extra'
2
- import { rm } from 'node:fs/promises'
3
- import { join } from 'node:path'
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
4
3
  import type { VXRNOptions } from '../types'
5
4
  import { fillOptions } from '../config/getOptionsFilled'
6
5
  import { getSSRExternalsCachePath } from '../plugins/autoDepOptimizePlugin'
7
6
  import { getCacheDir } from '../utils/getCacheDir'
7
+ import { readState, writeState } from '../utils/state'
8
8
 
9
- /**
10
- * The main entry point for dev mode
11
- *
12
- * Note that much of the logic is being run by plugins:
13
- *
14
- * - createFileSystemRouterPlugin does most of the fs-routes/request handling
15
- * - clientTreeShakePlugin handles loaders/transforms
16
- *
17
- */
9
+ const CLEAN_LOCK_WAIT_MS = 25
10
+ const CLEAN_LOCK_TIMEOUT_MS = 60_000
18
11
 
19
- export const clean = async (rest: VXRNOptions, only?: 'vite') => {
20
- const options = await fillOptions(rest)
21
- const { root } = options
12
+ function getErrorCode(err: unknown) {
13
+ if (err instanceof Error && 'code' in err && typeof err.code === 'string') {
14
+ return err.code
15
+ }
16
+ }
17
+
18
+ async function withCacheCleanLock<T>(cacheDir: string, action: () => Promise<T>) {
19
+ const lockPath = join(dirname(cacheDir), '.vxrn-clean.lock')
20
+ const startedAt = Date.now()
21
+ await mkdir(dirname(lockPath), { recursive: true })
22
+
23
+ while (true) {
24
+ try {
25
+ await writeFile(lockPath, `${process.pid}\n`, { flag: 'wx' })
26
+ break
27
+ } catch (err) {
28
+ if (getErrorCode(err) !== 'EEXIST') {
29
+ throw err
30
+ }
31
+
32
+ if (Date.now() - startedAt >= CLEAN_LOCK_TIMEOUT_MS) {
33
+ let owner = 'unknown process'
34
+ try {
35
+ owner = `process ${(await readFile(lockPath, 'utf8')).trim()}`
36
+ } catch (readErr) {
37
+ if (getErrorCode(readErr) === 'ENOENT') {
38
+ continue
39
+ }
40
+ throw readErr
41
+ }
42
+ throw new Error(`Timed out waiting for VXRN cache cleanup lock held by ${owner}`)
43
+ }
44
+ await new Promise<void>((resolve) => setTimeout(resolve, CLEAN_LOCK_WAIT_MS))
45
+ }
46
+ }
47
+
48
+ try {
49
+ return await action()
50
+ } finally {
51
+ await rm(lockPath, { force: true })
52
+ }
53
+ }
54
+
55
+ async function removeCachePaths(root: string, only?: 'vite') {
56
+ const removeVxrnCache = process.env.VXRN_DONT_CLEAN_SELF
57
+ ? rm(getSSRExternalsCachePath(root)).catch(throwIfNotMissingError)
58
+ : rm(getCacheDir(root), {
59
+ recursive: true,
60
+ force: true,
61
+ }).catch(throwIfNotMissingError)
22
62
 
23
63
  console.info(`[vxrn] cleaning`)
24
64
 
25
65
  await Promise.all([
26
- rm(getSSRExternalsCachePath(root)).catch(throwIfNotMissingError),
27
66
  rm(join(root, 'node_modules', '.vite'), {
28
67
  recursive: true,
29
68
  force: true,
30
69
  }).catch(throwIfNotMissingError),
31
- process.env.VXRN_DONT_CLEAN_SELF
32
- ? null
33
- : rm(getCacheDir(root), {
34
- recursive: true,
35
- force: true,
36
- }).catch(throwIfNotMissingError),
70
+ removeVxrnCache,
37
71
  only === 'vite'
38
72
  ? null
39
73
  : rm(join(root, 'dist'), {
@@ -43,10 +77,40 @@ export const clean = async (rest: VXRNOptions, only?: 'vite') => {
43
77
  ])
44
78
  }
45
79
 
46
- function throwIfNotMissingError(err: unknown) {
47
- if (err instanceof Error) {
48
- if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
49
- throw Error
80
+ export async function prepareCacheForVersion({
81
+ root,
82
+ cacheDir,
83
+ versionHash,
84
+ forceClean = false,
85
+ }: {
86
+ root: string
87
+ cacheDir: string
88
+ versionHash: string
89
+ forceClean?: boolean
90
+ }) {
91
+ return withCacheCleanLock(cacheDir, async () => {
92
+ const state = await readState(cacheDir)
93
+ const versionChanged = !!state.versionHash && state.versionHash !== versionHash
94
+ const shouldClean = forceClean || versionChanged
95
+
96
+ if (shouldClean) {
97
+ await removeCachePaths(root, 'vite')
98
+ }
99
+ if (shouldClean || state.versionHash !== versionHash) {
100
+ await writeState(cacheDir, { versionHash })
50
101
  }
102
+
103
+ return shouldClean
104
+ })
105
+ }
106
+
107
+ export const clean = async (rest: VXRNOptions, only?: 'vite') => {
108
+ const options = await fillOptions(rest)
109
+ await withCacheCleanLock(options.cacheDir, () => removeCachePaths(options.root, only))
110
+ }
111
+
112
+ function throwIfNotMissingError(err: unknown) {
113
+ if (getErrorCode(err) !== 'ENOENT') {
114
+ throw err
51
115
  }
52
116
  }
@@ -44,7 +44,7 @@ export const dev = async (optionsIn: DevOptions) => {
44
44
  const { fillOptions } = await import('../config/getOptionsFilled')
45
45
  const { getViteServerConfig } = await import('../config/getViteServerConfig')
46
46
  const { printServerUrls } = await import('../utils/printServerUrls')
47
- const { clean } = await import('./clean')
47
+ const { prepareCacheForVersion } = await import('./clean')
48
48
  const { filterViteServerResolvedUrls } =
49
49
  await import('../utils/filterViteServerResolvedUrls')
50
50
  const { removeUndefined } = await import('../utils/removeUndefined')
@@ -94,11 +94,22 @@ export default defineConfig({
94
94
  const oneServerConfig = config?.plugins?.find(
95
95
  (x) => Array.isArray(x) && x[0]?.['name'] === 'one:config'
96
96
  )?.[0]?.['__get']?.server
97
+ const viteServerConfig = config.server
97
98
 
98
99
  const options = await fillOptions({
99
100
  ...optionsIn,
100
101
  server: {
101
102
  ...oneServerConfig,
103
+ ...removeUndefined({
104
+ host:
105
+ viteServerConfig?.host === true
106
+ ? '0.0.0.0'
107
+ : typeof viteServerConfig?.host === 'string'
108
+ ? viteServerConfig.host
109
+ : undefined,
110
+ https: viteServerConfig?.https,
111
+ port: viteServerConfig?.port,
112
+ }),
102
113
  ...removeUndefined(optionsIn.server || {}),
103
114
  },
104
115
  })
@@ -107,8 +118,13 @@ export default defineConfig({
107
118
 
108
119
  bindKeypressInput()
109
120
 
110
- if (options.clean) {
111
- await clean(optionsIn, options.clean)
121
+ const cacheWasCleaned = await prepareCacheForVersion({
122
+ root: options.root,
123
+ cacheDir,
124
+ versionHash: options.versionHash,
125
+ forceClean: optionsIn.clean === true,
126
+ })
127
+ if (options.clean || cacheWasCleaned) {
112
128
  // signal metro to reset its cache as well
113
129
  process.env.METRO_RESET_CACHE = '1'
114
130
  }
@@ -0,0 +1,63 @@
1
+ import { createServer } from 'node:http'
2
+ import { Hono } from 'hono'
3
+ import { describe, expect, test } from 'vitest'
4
+ import { honoServeNode } from './node'
5
+
6
+ describe('honoServeNode', () => {
7
+ test('rejects when the listener cannot bind', async () => {
8
+ const occupied = createServer()
9
+ await new Promise<void>((resolve, reject) => {
10
+ occupied.once('error', reject)
11
+ occupied.listen(0, '127.0.0.1', resolve)
12
+ })
13
+ const address = occupied.address()
14
+ if (!address || typeof address === 'string') throw new Error('missing test port')
15
+
16
+ const app = new Hono().get('/health', (context) => context.text('ok'))
17
+ await expect(
18
+ honoServeNode(app, { host: '127.0.0.1', port: address.port })
19
+ ).rejects.toMatchObject({ code: 'EADDRINUSE' })
20
+
21
+ await new Promise<void>((resolve, reject) =>
22
+ occupied.close((error) => (error ? reject(error) : resolve()))
23
+ )
24
+ })
25
+
26
+ test('overwrites the trusted peer address header from the socket', async () => {
27
+ const reservation = createServer()
28
+ await new Promise<void>((resolve, reject) => {
29
+ reservation.once('error', reject)
30
+ reservation.listen(0, '127.0.0.1', resolve)
31
+ })
32
+ const address = reservation.address()
33
+ if (!address || typeof address === 'string') throw new Error('missing test port')
34
+ await new Promise<void>((resolve, reject) =>
35
+ reservation.close((error) => (error ? reject(error) : resolve()))
36
+ )
37
+
38
+ const app = new Hono().get('/peer', (context) =>
39
+ context.text(context.req.header('x-vxrn-peer-address') || '')
40
+ )
41
+ const running = honoServeNode(app, { host: '127.0.0.1', port: address.port })
42
+ await new Promise<void>((resolve, reject) => {
43
+ const startedAt = Date.now()
44
+ const probe = async () => {
45
+ try {
46
+ const response = await fetch(`http://127.0.0.1:${address.port}/peer`, {
47
+ headers: { 'x-vxrn-peer-address': '203.0.113.9' },
48
+ })
49
+ expect(await response.text()).toBe('127.0.0.1')
50
+ resolve()
51
+ } catch (error) {
52
+ if (Date.now() - startedAt > 2_000) reject(error)
53
+ else setTimeout(probe, 20)
54
+ }
55
+ }
56
+ void probe()
57
+ })
58
+ const shutdown = process.listeners('SIGTERM').at(-1)
59
+ expect(shutdown).toBeTypeOf('function')
60
+ shutdown?.('SIGTERM')
61
+ await running
62
+ })
63
+ })
package/src/serve/node.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { createServer, type Server } from 'node:http'
1
+ import { createServer } from 'node:http'
2
2
  import { networkInterfaces, platform } from 'node:os'
3
3
  import { getRequestListener } from '@hono/node-server'
4
- import { serve as honoServe } from '@hono/node-server'
5
- import type { ServerType } from '@hono/node-server'
6
4
  import type { Hono } from 'hono'
7
5
  import colors from 'picocolors'
8
6
  import type { VXRNServeOptions } from '../types'
@@ -35,21 +33,20 @@ export async function honoServeNode(app: Hono, options: VXRNServeOptions) {
35
33
  const port = options.port ?? 3000
36
34
  const host = options.host ?? '0.0.0.0'
37
35
 
38
- let server: Server | ServerType
39
-
40
- if (canReusePort) {
41
- // bypass @hono/node-server's serve() to use reusePort directly
42
- // kernel distributes connections across workers - no IPC bottleneck
43
- const listener = getRequestListener(app.fetch)
44
- server = createServer(listener)
45
- server.listen({ port, host, reusePort: true })
46
- } else {
47
- server = honoServe({
48
- fetch: app.fetch,
49
- port,
50
- hostname: host,
51
- })
52
- }
36
+ const listener = getRequestListener((request, env) => {
37
+ request.headers.delete('x-vxrn-peer-address')
38
+ const address = env.incoming.socket.remoteAddress
39
+ if (address) request.headers.set('x-vxrn-peer-address', address)
40
+ return app.fetch(request, env)
41
+ })
42
+ const server = createServer(listener)
43
+ server.listen({
44
+ port,
45
+ host,
46
+ ...(canReusePort && process.env.ONE_CLUSTER_WORKER === '1'
47
+ ? { reusePort: true }
48
+ : {}),
49
+ })
53
50
 
54
51
  const colorUrl = (url: string) =>
55
52
  colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`))
@@ -71,17 +68,16 @@ export async function honoServeNode(app: Hono, options: VXRNServeOptions) {
71
68
  }
72
69
  console.info()
73
70
 
74
- const shutdown = () => {
75
- server.close(() => {
76
- process.exit(0)
77
- })
78
- }
71
+ const shutdown = () => server.close()
79
72
 
80
- process.on('SIGINT', shutdown)
81
- process.on('SIGTERM', shutdown)
73
+ process.once('SIGINT', shutdown)
74
+ process.once('SIGTERM', shutdown)
82
75
 
83
- await new Promise<void>((res) => {
76
+ await new Promise<void>((res, rej) => {
77
+ server.once('error', rej)
84
78
  server.on('close', () => {
79
+ process.off('SIGINT', shutdown)
80
+ process.off('SIGTERM', shutdown)
85
81
  res()
86
82
  })
87
83
  })
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Hono } from 'hono'
2
+ import type { ServerOptions as HttpsServerOptions } from 'node:https'
2
3
  import type { OutputAsset, OutputChunk, TreeshakingOptions } from 'rolldown'
3
4
  import type { FilterPattern, InlineConfig, UserConfig } from 'vite'
4
5
 
@@ -120,6 +121,7 @@ export type VXRNOptions = {
120
121
  host?: string
121
122
  port?: number
122
123
  compress?: boolean
124
+ https?: HttpsServerOptions
123
125
 
124
126
  /**
125
127
  * Whether to run the Vite logic to load .env files before running the server
@@ -165,9 +167,9 @@ export type HMRListener = (update: { file: string; contents: string }) => void
165
167
  type VXRNServeOptionsBase = VXRNOptions['server']
166
168
 
167
169
  export type VXRNServeOptionsFilled = Required<
168
- Omit<NonNullable<VXRNServeOptionsBase>, 'cacheControl'>
170
+ Omit<NonNullable<VXRNServeOptionsBase>, 'cacheControl' | 'https'>
169
171
  > &
170
- Pick<NonNullable<VXRNServeOptionsBase>, 'cacheControl'> & {
172
+ Pick<NonNullable<VXRNServeOptionsBase>, 'cacheControl' | 'https'> & {
171
173
  url: string
172
174
  protocol: string
173
175
  }
@@ -1,4 +1,6 @@
1
1
  import FSExtra from 'fs-extra'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { rename, rm } from 'node:fs/promises'
2
4
  import { join } from 'node:path'
3
5
 
4
6
  type State = {
@@ -20,6 +22,12 @@ export async function readState(cacheDir: string) {
20
22
 
21
23
  export async function writeState(cacheDir: string, state: State) {
22
24
  const statePath = join(cacheDir, 'state.json')
25
+ const pendingStatePath = `${statePath}.${process.pid}.${randomUUID()}.tmp`
23
26
  await FSExtra.ensureDir(cacheDir)
24
- await FSExtra.writeJSON(statePath, state)
27
+ try {
28
+ await FSExtra.writeJSON(pendingStatePath, state)
29
+ await rename(pendingStatePath, statePath)
30
+ } finally {
31
+ await rm(pendingStatePath, { force: true })
32
+ }
25
33
  }
@@ -54,6 +54,7 @@ export declare function fillOptions(options: VXRNOptions, { mode }?: {
54
54
  };
55
55
  readonly packageRootDir: string;
56
56
  readonly cacheDir: string;
57
+ readonly versionHash: string;
57
58
  readonly skipEnv?: boolean;
58
59
  readonly build?: {
59
60
  server?: boolean | import("..").VXRNBuildOptions;
@@ -86,6 +87,7 @@ export declare function getOptionsFilled(): {
86
87
  };
87
88
  readonly packageRootDir: string;
88
89
  readonly cacheDir: string;
90
+ readonly versionHash: string;
89
91
  readonly skipEnv?: boolean;
90
92
  readonly build?: {
91
93
  server?: boolean | import("..").VXRNBuildOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"getOptionsFilled.d.ts","sourceRoot":"","sources":["../../src/config/getOptionsFilled.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAOjD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAA;AAIvE,wBAAsB,WAAW,CAC/B,OAAO,EAAE,WAAW,EACpB,EAAE,IAAY,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,IAAI,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;;;;;cAelC,CAAN;eAME,CADF;;;;GA+CC;AAED,wBAAgB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;cAtD1B,CAAN;eAME,CADF;;;;SAmDC"}
1
+ {"version":3,"file":"getOptionsFilled.d.ts","sourceRoot":"","sources":["../../src/config/getOptionsFilled.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAOjD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAA;AAIvE,wBAAsB,WAAW,CAC/B,OAAO,EAAE,WAAW,EACpB,EAAE,IAAY,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,IAAI,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBnB,CAAC;eAMlB,CAAD;;;;GA0CF;AAED,wBAAgB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;cAlDX,CAAC;eAMlB,CAAD;;;;SA8CF"}
@@ -1 +1 @@
1
- {"version":3,"file":"getServerOptionsFilled.d.ts","sourceRoot":"","sources":["../../src/config/getServerOptionsFilled.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEzE,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,WAAW,CAAC,QAAQ,CAAC,EACpC,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,sBAAsB,CAAC,CAmDjC"}
1
+ {"version":3,"file":"getServerOptionsFilled.d.ts","sourceRoot":"","sources":["../../src/config/getServerOptionsFilled.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEzE,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,WAAW,CAAC,QAAQ,CAAC,EACpC,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,sBAAsB,CAAC,CAoDjC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getServerOptionsFilled.test.d.ts","sourceRoot":"","sources":["../../src/config/getServerOptionsFilled.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"getViteServerConfig.d.ts","sourceRoot":"","sources":["../../src/config/getViteServerConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAItE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAM3D;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,iBAAiB,EACzB,cAAc,CAAC,EAAE,UAAU,uBAiD5B"}
1
+ {"version":3,"file":"getViteServerConfig.d.ts","sourceRoot":"","sources":["../../src/config/getViteServerConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAItE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAM3D;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,iBAAiB,EACzB,cAAc,CAAC,EAAE,UAAU,uBAkD5B"}
@@ -29,6 +29,7 @@ export declare const build: (optionsIn: VXRNOptions, buildArgs?: BuildArgs) => P
29
29
  };
30
30
  readonly packageRootDir: string;
31
31
  readonly cacheDir: string;
32
+ readonly versionHash: string;
32
33
  readonly skipEnv?: boolean;
33
34
  readonly build?: {
34
35
  server?: boolean | import("..").VXRNBuildOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/exports/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAkB,MAAM,UAAU,CAAA;AAaxE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAuBtD,eAAO,MAAM,KAAK,GAAU,WAAW,WAAW,EAAE,YAAW,SAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAiDrE,CAAN;mBAKsE,CAAC;;;;;;;;;;;;EA4SxE,CAAA"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/exports/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAkB,MAAM,UAAU,CAAA;AAaxE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAuBtD,eAAO,MAAM,KAAK,GAAU,WAAW,WAAW,EAAE,YAAW,SAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmDzE,CAAC;mBAI+D,CAAC;;;;;;;;;;;;EA2SpE,CAAA"}