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.
- package/dist/config/getOptionsFilled.mjs +3 -5
- package/dist/config/getOptionsFilled.mjs.map +1 -1
- package/dist/config/getOptionsFilled.native.js +3 -5
- package/dist/config/getOptionsFilled.native.js.map +1 -1
- package/dist/config/getServerOptionsFilled.mjs +3 -2
- package/dist/config/getServerOptionsFilled.mjs.map +1 -1
- package/dist/config/getServerOptionsFilled.native.js +3 -2
- package/dist/config/getServerOptionsFilled.native.js.map +1 -1
- package/dist/config/getServerOptionsFilled.test.mjs +20 -0
- package/dist/config/getServerOptionsFilled.test.mjs.map +1 -0
- package/dist/config/getServerOptionsFilled.test.native.js +20 -0
- package/dist/config/getServerOptionsFilled.test.native.js.map +1 -0
- package/dist/config/getViteServerConfig.mjs +1 -0
- package/dist/config/getViteServerConfig.mjs.map +1 -1
- package/dist/config/getViteServerConfig.native.js +1 -0
- package/dist/config/getViteServerConfig.native.js.map +1 -1
- package/dist/exports/clean.mjs +84 -16
- package/dist/exports/clean.mjs.map +1 -1
- package/dist/exports/clean.native.js +89 -16
- package/dist/exports/clean.native.js.map +1 -1
- package/dist/exports/clean.test.mjs +81 -0
- package/dist/exports/clean.test.mjs.map +1 -0
- package/dist/exports/clean.test.native.js +88 -0
- package/dist/exports/clean.test.native.js.map +1 -0
- package/dist/exports/dev.mjs +14 -3
- package/dist/exports/dev.mjs.map +1 -1
- package/dist/exports/dev.native.js +14 -3
- package/dist/exports/dev.native.js.map +1 -1
- package/dist/serve/node.mjs +20 -24
- package/dist/serve/node.mjs.map +1 -1
- package/dist/serve/node.native.js +20 -22
- package/dist/serve/node.native.js.map +1 -1
- package/dist/serve/node.test.mjs +60 -0
- package/dist/serve/node.test.mjs.map +1 -0
- package/dist/serve/node.test.native.js +72 -0
- package/dist/serve/node.test.native.js.map +1 -0
- package/dist/utils/state.mjs +11 -1
- package/dist/utils/state.mjs.map +1 -1
- package/dist/utils/state.native.js +11 -1
- package/dist/utils/state.native.js.map +1 -1
- package/package.json +11 -11
- package/src/config/getOptionsFilled.ts +2 -4
- package/src/config/getServerOptionsFilled.test.ts +24 -0
- package/src/config/getServerOptionsFilled.ts +2 -1
- package/src/config/getViteServerConfig.ts +1 -0
- package/src/exports/clean.test.ts +82 -0
- package/src/exports/clean.ts +90 -26
- package/src/exports/dev.ts +19 -3
- package/src/serve/node.test.ts +63 -0
- package/src/serve/node.ts +22 -26
- package/src/types.ts +4 -2
- package/src/utils/state.ts +9 -1
- package/types/config/getOptionsFilled.d.ts +2 -0
- package/types/config/getOptionsFilled.d.ts.map +1 -1
- package/types/config/getServerOptionsFilled.d.ts.map +1 -1
- package/types/config/getServerOptionsFilled.test.d.ts.map +1 -0
- package/types/config/getViteServerConfig.d.ts.map +1 -1
- package/types/exports/build.d.ts +1 -0
- package/types/exports/build.d.ts.map +1 -1
- package/types/exports/clean.d.ts +6 -9
- package/types/exports/clean.d.ts.map +1 -1
- package/types/exports/clean.test.d.ts.map +1 -0
- package/types/exports/dev.d.ts.map +1 -1
- package/types/serve/node.d.ts.map +1 -1
- package/types/serve/node.test.d.ts.map +1 -0
- package/types/types.d.ts +3 -1
- package/types/types.d.ts.map +1 -1
- 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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
64
|
-
"@vxrn/debug": "1.21.
|
|
65
|
-
"@vxrn/query-string": "1.21.
|
|
66
|
-
"@vxrn/resolve": "1.21.
|
|
67
|
-
"@vxrn/safe-area": "1.21.
|
|
68
|
-
"@vxrn/url-parse": "1.21.
|
|
69
|
-
"@vxrn/utils": "1.21.
|
|
70
|
-
"@vxrn/vendor": "1.21.
|
|
71
|
-
"@vxrn/vite-flow": "1.21.
|
|
72
|
-
"@vxrn/vite-plugin-metro": "1.21.
|
|
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
|
|
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
|
|
@@ -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
|
+
})
|
package/src/exports/clean.ts
CHANGED
|
@@ -1,39 +1,73 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
}
|
package/src/exports/dev.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
81
|
-
process.
|
|
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
|
}
|
package/src/utils/state.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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"}
|
package/types/exports/build.d.ts
CHANGED
|
@@ -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
|
|
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"}
|