catalyst-core 1.1.0 → 1.2.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/catalyst.js +54 -6
- package/changelog.md +27 -1
- package/dist/errors/registry.js +306 -18
- package/dist/errors-index.json +273 -70
- package/dist/native/androidProject/app/build.gradle.kts +53 -79
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
- package/dist/native/androidSetup.js +76 -170
- package/dist/native/androidToolchain.js +642 -0
- package/dist/native/appIcon.js +100 -0
- package/dist/native/buildAndroid/assets.js +29 -5
- package/dist/native/buildAndroid/build.js +110 -223
- package/dist/native/buildAndroid/config.js +57 -65
- package/dist/native/buildAndroid/devices.js +176 -0
- package/dist/native/buildAndroid/index.js +271 -46
- package/dist/native/buildAndroid/output.js +43 -0
- package/dist/native/buildAndroid/signing.js +456 -0
- package/dist/native/buildAppAndroid.js +27 -7
- package/dist/native/buildAppIos.js +139 -20
- package/dist/native/buildIos/assets.js +76 -4
- package/dist/native/buildIos/build.js +31 -181
- package/dist/native/buildIos/config.js +126 -11
- package/dist/native/buildIos/index.js +354 -34
- package/dist/native/buildIos/localhostCert.js +87 -0
- package/dist/native/buildIos/output.js +28 -0
- package/dist/native/buildIos/signing.js +596 -0
- package/dist/native/credentials.js +309 -0
- package/dist/native/exportCredentials.js +51 -0
- package/dist/native/iosToolchain.js +583 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
- package/dist/native/localState.js +148 -0
- package/dist/native/nativeConfig.js +621 -0
- package/dist/native/releaseOutput.js +53 -0
- package/dist/native/renameAndroidProject.js +154 -369
- package/dist/native/setupEmulatorIos.js +58 -275
- package/dist/native/setupServer.js +520 -124
- package/dist/native/utils.js +418 -115
- package/dist/vite/resolveDevServerConfig.js +21 -0
- package/mcp_v2/knowledge-base.json +13 -11
- package/mcp_v2/test/config.test.ts +40 -0
- package/mcp_v2/tools/build.js +80 -104
- package/mcp_v2/tools/config.js +181 -174
- package/package.json +1 -1
- package/test/cli/nativeFlags.test.ts +97 -0
- package/test/native/androidDevices.test.ts +230 -0
- package/test/native/androidElfAlignment.test.ts +148 -0
- package/test/native/androidSigning.test.ts +646 -0
- package/test/native/androidToolchain.test.ts +766 -0
- package/test/native/appIcon.test.ts +124 -0
- package/test/native/credentials.test.ts +241 -0
- package/test/native/interaction.test.ts +179 -0
- package/test/native/iosConfigPhase.test.ts +611 -0
- package/test/native/iosSigning.test.ts +1060 -0
- package/test/native/iosToolchain.test.ts +821 -0
- package/test/native/localState.test.ts +80 -0
- package/test/native/nativeConfig.test.ts +602 -0
- package/test/native/setupServer.test.ts +529 -0
- package/test/native/utils.test.ts +102 -127
- package/test/vite/resolveDevServerConfig.test.js +25 -0
- package/dist/native/androidProject/keystore/Untitled +0 -0
- package/dist/native/androidProject/keystore/release-key.jks +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { spawnSync } from "child_process"
|
|
2
|
+
import { EventEmitter } from "events"
|
|
3
|
+
import fs from "fs"
|
|
4
|
+
import http from "http"
|
|
5
|
+
import net from "net"
|
|
6
|
+
import os from "os"
|
|
7
|
+
import path from "path"
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
devServerPorts,
|
|
12
|
+
ensureDevServer,
|
|
13
|
+
getDeviceReachableHost,
|
|
14
|
+
getLocalIPAddress,
|
|
15
|
+
isProcessAlive,
|
|
16
|
+
isServerAnswering,
|
|
17
|
+
migrateLanAddresses,
|
|
18
|
+
planLanAddressMigration,
|
|
19
|
+
readHmrPort,
|
|
20
|
+
readLocalHostName,
|
|
21
|
+
startDevServerProcess,
|
|
22
|
+
} from "../../src/native/setupServer.js"
|
|
23
|
+
import { readLocalState, writeLocalState } from "../../src/native/localState.js"
|
|
24
|
+
|
|
25
|
+
const temps: string[] = []
|
|
26
|
+
function tempDir() {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-setup-server-"))
|
|
28
|
+
temps.push(dir)
|
|
29
|
+
return dir
|
|
30
|
+
}
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const v4 = (address: string, internal = false) => ({ family: "IPv4", address, internal })
|
|
36
|
+
|
|
37
|
+
describe("getLocalIPAddress", () => {
|
|
38
|
+
it("prefers en0 and Wi-Fi/Ethernet over tunnels and bridges", () => {
|
|
39
|
+
expect(getLocalIPAddress({ utun3: [v4("100.64.0.2")], en0: [v4("192.168.1.9")] } as any)).toBe(
|
|
40
|
+
"192.168.1.9"
|
|
41
|
+
)
|
|
42
|
+
expect(getLocalIPAddress({ docker0: [v4("172.17.0.1")], wlp2s0: [v4("10.0.0.7")] } as any)).toBe(
|
|
43
|
+
"10.0.0.7"
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("is null with only loopback", () => {
|
|
48
|
+
expect(getLocalIPAddress({ lo: [v4("127.0.0.1", true)] } as any)).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("getDeviceReachableHost", () => {
|
|
53
|
+
it("uses the Bonjour name first", () => {
|
|
54
|
+
expect(
|
|
55
|
+
getDeviceReachableHost({ localHostName: () => "Janes-MacBook-Pro\n", interfaces: {} as any })
|
|
56
|
+
).toEqual({
|
|
57
|
+
host: "Janes-MacBook-Pro.local",
|
|
58
|
+
source: "bonjour",
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("falls back to the LAN IP, then null", () => {
|
|
63
|
+
const fails = () => {
|
|
64
|
+
throw new Error("no scutil")
|
|
65
|
+
}
|
|
66
|
+
expect(
|
|
67
|
+
getDeviceReachableHost({ localHostName: fails, interfaces: { en0: [v4("192.168.1.9")] } as any })
|
|
68
|
+
).toEqual({
|
|
69
|
+
host: "192.168.1.9",
|
|
70
|
+
source: "lan",
|
|
71
|
+
})
|
|
72
|
+
expect(getDeviceReachableHost({ localHostName: () => "", interfaces: {} as any })).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("readLocalHostName", () => {
|
|
77
|
+
it("asks scutil on macOS only", () => {
|
|
78
|
+
const calls: any[] = []
|
|
79
|
+
const exec = ((...args: any[]) => {
|
|
80
|
+
calls.push(args)
|
|
81
|
+
return "Janes-Mac\n"
|
|
82
|
+
}) as any
|
|
83
|
+
expect(readLocalHostName({ platform: "linux", exec })).toBe("")
|
|
84
|
+
expect(calls).toHaveLength(0)
|
|
85
|
+
expect(readLocalHostName({ platform: "darwin", exec })).toBe("Janes-Mac\n")
|
|
86
|
+
expect(calls[0][0]).toBe("scutil")
|
|
87
|
+
expect(calls[0][1]).toEqual(["--get", "LocalHostName"])
|
|
88
|
+
expect(calls[0][2]).toMatchObject({ encoding: "utf8", timeout: 5000 })
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("devServerPorts", () => {
|
|
93
|
+
it("lists the server port, the HMR port and every localhost port in config", () => {
|
|
94
|
+
const config = {
|
|
95
|
+
NODE_SERVER_PORT: 3005,
|
|
96
|
+
API_URL: "http://localhost:8080/api",
|
|
97
|
+
PUBLIC_STATIC_ASSET_URL: "http://localhost:3005",
|
|
98
|
+
WEBVIEW_CONFIG: { apiBaseUrl: "http://127.0.0.1:9000" },
|
|
99
|
+
OTHER: ["https://example.com:8443"],
|
|
100
|
+
}
|
|
101
|
+
expect(devServerPorts(config)).toEqual([3005, 8080, 9000, 24678])
|
|
102
|
+
expect(devServerPorts({}, { hmrPort: 5000 })).toEqual([3005, 5000])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("reads the HMR port from buildConfig.js", async () => {
|
|
106
|
+
const cwd = tempDir()
|
|
107
|
+
expect(await readHmrPort({ cwd })).toBe(24678)
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(cwd, "buildConfig.js"),
|
|
110
|
+
"module.exports = { devServer: { hmr: { port: 24999 } } }\n"
|
|
111
|
+
)
|
|
112
|
+
expect(await readHmrPort({ cwd })).toBe(24999)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("falls back to the default HMR port for a buildConfig.js that does not load or names a bad port", async () => {
|
|
116
|
+
const broken = tempDir()
|
|
117
|
+
fs.writeFileSync(path.join(broken, "buildConfig.js"), "module.exports = {\n")
|
|
118
|
+
expect(await readHmrPort({ cwd: broken })).toBe(24678)
|
|
119
|
+
const outOfRange = tempDir()
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(outOfRange, "webpackConfig.js"),
|
|
122
|
+
"module.exports = { devServer: { hmr: { port: 70000 } } }\n"
|
|
123
|
+
)
|
|
124
|
+
expect(await readHmrPort({ cwd: outOfRange })).toBe(24678)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("startDevServerProcess", () => {
|
|
129
|
+
it("starts catalyst start detached, logging to an owner-only file under .catalyst/", () => {
|
|
130
|
+
const cwd = tempDir()
|
|
131
|
+
let spawned: any
|
|
132
|
+
let unrefs = 0
|
|
133
|
+
const child = Object.assign(new EventEmitter(), {
|
|
134
|
+
pid: 4242,
|
|
135
|
+
unref: () => void unrefs++,
|
|
136
|
+
})
|
|
137
|
+
const { child: returned, logFile } = startDevServerProcess({
|
|
138
|
+
cwd,
|
|
139
|
+
port: 3005,
|
|
140
|
+
overrides: { NODE_SERVER_HOSTNAME: "0.0.0.0", PORT: 3005 },
|
|
141
|
+
spawnProcess: ((command: string, args: string[], options: any) => {
|
|
142
|
+
spawned = { command, args, options }
|
|
143
|
+
return child
|
|
144
|
+
}) as any,
|
|
145
|
+
})
|
|
146
|
+
expect(returned).toBe(child)
|
|
147
|
+
expect(logFile).toBe(path.join(cwd, ".catalyst", "dev-server-3005.log"))
|
|
148
|
+
expect(logFile.startsWith(os.tmpdir() + path.sep + "catalyst-dev-server")).toBe(false)
|
|
149
|
+
expect(fs.statSync(logFile).mode & 0o777).toBe(0o600)
|
|
150
|
+
expect(fs.readFileSync(path.join(cwd, ".catalyst", ".gitignore"), "utf8")).toBe("*\n")
|
|
151
|
+
expect(spawned.command).toBe(process.execPath)
|
|
152
|
+
expect(path.basename(spawned.args[0])).toBe("catalyst.js")
|
|
153
|
+
expect(spawned.args.slice(1)).toEqual(["start", "NODE_SERVER_HOSTNAME=0.0.0.0", "PORT=3005"])
|
|
154
|
+
expect(spawned.options).toMatchObject({ cwd, detached: true })
|
|
155
|
+
expect(spawned.options.env.PWD).toBe(cwd)
|
|
156
|
+
expect(spawned.options.stdio[0]).toBe("ignore")
|
|
157
|
+
expect(unrefs).toBe(1)
|
|
158
|
+
// A spawn failure after the fact is swallowed, not an unhandled error.
|
|
159
|
+
expect(() => child.emit("error", new Error("ENOENT"))).not.toThrow()
|
|
160
|
+
|
|
161
|
+
// Without a port the file says so.
|
|
162
|
+
const second = startDevServerProcess({
|
|
163
|
+
cwd,
|
|
164
|
+
spawnProcess: (() => Object.assign(new EventEmitter(), { unref() {} })) as any,
|
|
165
|
+
})
|
|
166
|
+
expect(path.basename(second.logFile)).toBe("dev-server-default.log")
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe("isProcessAlive", () => {
|
|
171
|
+
it("is true for a running process and false once it has exited", () => {
|
|
172
|
+
expect(isProcessAlive(process.pid)).toBe(true)
|
|
173
|
+
const done = spawnSync(process.execPath, ["-e", "process.stdout.write(String(process.pid))"], {
|
|
174
|
+
encoding: "utf8",
|
|
175
|
+
})
|
|
176
|
+
expect(isProcessAlive(Number(done.stdout))).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe("ensureDevServer", () => {
|
|
181
|
+
const config = { NODE_SERVER_PORT: 3005 }
|
|
182
|
+
|
|
183
|
+
it("does nothing when the server answers", async () => {
|
|
184
|
+
let started = false
|
|
185
|
+
const result = await ensureDevServer({
|
|
186
|
+
config,
|
|
187
|
+
probe: async () => true,
|
|
188
|
+
start: (() => {
|
|
189
|
+
started = true
|
|
190
|
+
}) as any,
|
|
191
|
+
})
|
|
192
|
+
expect(result).toEqual({ started: false, url: "http://localhost:3005", port: 3005 })
|
|
193
|
+
expect(started).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("starts catalyst start and waits until it answers", async () => {
|
|
197
|
+
let calls = 0
|
|
198
|
+
let startedWith: any
|
|
199
|
+
const result = await ensureDevServer({
|
|
200
|
+
config,
|
|
201
|
+
intervalMs: 1,
|
|
202
|
+
probe: async () => ++calls > 3,
|
|
203
|
+
start: ((options: any) => {
|
|
204
|
+
startedWith = options
|
|
205
|
+
return { child: null, logFile: "/tmp/x.log" }
|
|
206
|
+
}) as any,
|
|
207
|
+
})
|
|
208
|
+
expect(result).toMatchObject({ started: true, url: "http://localhost:3005", logFile: "/tmp/x.log" })
|
|
209
|
+
expect(startedWith.overrides).toEqual({})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("binds all interfaces and serves assets on a device host", async () => {
|
|
213
|
+
let startedWith: any
|
|
214
|
+
let probes = 0
|
|
215
|
+
await ensureDevServer({
|
|
216
|
+
host: "jane-mac.local",
|
|
217
|
+
config,
|
|
218
|
+
intervalMs: 1,
|
|
219
|
+
probe: async ({ host }: any) => host === "jane-mac.local" && ++probes > 1,
|
|
220
|
+
start: ((options: any) => {
|
|
221
|
+
startedWith = options
|
|
222
|
+
return { child: null, logFile: "/tmp/x.log" }
|
|
223
|
+
}) as any,
|
|
224
|
+
})
|
|
225
|
+
expect(startedWith.overrides).toEqual({
|
|
226
|
+
NODE_SERVER_HOSTNAME: "0.0.0.0",
|
|
227
|
+
PUBLIC_STATIC_ASSET_URL: "http://jane-mac.local:3005",
|
|
228
|
+
// Vite 6 answers 403 to a .local Host header unless it is allowed.
|
|
229
|
+
CATALYST_DEV_ALLOWED_HOSTS: "jane-mac.local",
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it("does not add an allowed host for a LAN IP (Vite accepts IPs)", async () => {
|
|
234
|
+
let startedWith: any
|
|
235
|
+
let probes = 0
|
|
236
|
+
await ensureDevServer({
|
|
237
|
+
host: "192.168.1.20",
|
|
238
|
+
config,
|
|
239
|
+
intervalMs: 1,
|
|
240
|
+
probe: async ({ host }: any) => host === "192.168.1.20" && ++probes > 1,
|
|
241
|
+
start: ((options: any) => {
|
|
242
|
+
startedWith = options
|
|
243
|
+
return { child: null, logFile: "/tmp/x.log" }
|
|
244
|
+
}) as any,
|
|
245
|
+
})
|
|
246
|
+
expect(startedWith.overrides).toEqual({
|
|
247
|
+
NODE_SERVER_HOSTNAME: "0.0.0.0",
|
|
248
|
+
PUBLIC_STATIC_ASSET_URL: "http://192.168.1.20:3005",
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("refuses a server that only answers on localhost when a device needs it (NATIVE-006)", async () => {
|
|
253
|
+
const error = await ensureDevServer({
|
|
254
|
+
host: "jane-mac.local",
|
|
255
|
+
config,
|
|
256
|
+
probe: async ({ host }: any) => host === "localhost",
|
|
257
|
+
start: (() => {
|
|
258
|
+
throw new Error("must not start")
|
|
259
|
+
}) as any,
|
|
260
|
+
}).catch((e) => e)
|
|
261
|
+
expect(error.code).toBe("NATIVE-006")
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("records the host a started server is for", async () => {
|
|
265
|
+
const cwd = tempDir()
|
|
266
|
+
let probes = 0
|
|
267
|
+
await ensureDevServer({
|
|
268
|
+
host: "jane-mac.local",
|
|
269
|
+
cwd,
|
|
270
|
+
config,
|
|
271
|
+
intervalMs: 1,
|
|
272
|
+
probe: async ({ host }: any) => host === "jane-mac.local" && ++probes > 1,
|
|
273
|
+
start: (() => ({ child: { pid: 4242, on: () => {} }, logFile: "/tmp/x.log" })) as any,
|
|
274
|
+
})
|
|
275
|
+
expect(readLocalState({ cwd }).devServer).toEqual({ host: "jane-mac.local", port: 3005, pid: 4242 })
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it("refuses to reuse a server it started for another target (NATIVE-006)", async () => {
|
|
279
|
+
const cwd = tempDir()
|
|
280
|
+
writeLocalState({ devServer: { host: "jane-mac.local", port: 3005, pid: 4242 } }, { cwd })
|
|
281
|
+
const run = (host: string, alive: boolean) =>
|
|
282
|
+
ensureDevServer({
|
|
283
|
+
host,
|
|
284
|
+
cwd,
|
|
285
|
+
config,
|
|
286
|
+
probe: async () => true,
|
|
287
|
+
isAlive: () => alive,
|
|
288
|
+
start: (() => {
|
|
289
|
+
throw new Error("must not start")
|
|
290
|
+
}) as any,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// The simulator after an iPhone build: the running server serves
|
|
294
|
+
// assets on jane-mac.local, not localhost.
|
|
295
|
+
const error = await run("localhost", true).catch((e) => e)
|
|
296
|
+
expect(error.code).toBe("NATIVE-006")
|
|
297
|
+
expect(error.message).toContain("started for jane-mac.local")
|
|
298
|
+
expect(error.hint).toContain("kill 4242")
|
|
299
|
+
|
|
300
|
+
// The same target reuses it, and a recorded process that is gone is ignored.
|
|
301
|
+
expect(await run("jane-mac.local", true)).toMatchObject({ started: false })
|
|
302
|
+
expect(await run("localhost", false)).toMatchObject({ started: false })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it("reuses a server the user started on localhost for a localhost target", async () => {
|
|
306
|
+
const cwd = tempDir()
|
|
307
|
+
const result = await ensureDevServer({
|
|
308
|
+
cwd,
|
|
309
|
+
config,
|
|
310
|
+
probe: async () => true,
|
|
311
|
+
isAlive: () => true,
|
|
312
|
+
start: (() => {
|
|
313
|
+
throw new Error("must not start")
|
|
314
|
+
}) as any,
|
|
315
|
+
})
|
|
316
|
+
expect(result).toEqual({ started: false, url: "http://localhost:3005", port: 3005 })
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it("fails with NATIVE-006 when catalyst start exits before the server answers", async () => {
|
|
320
|
+
const exitWith = (code: number | null) => {
|
|
321
|
+
const child = new EventEmitter()
|
|
322
|
+
setTimeout(() => child.emit("exit", code), 5)
|
|
323
|
+
return child
|
|
324
|
+
}
|
|
325
|
+
for (const [code, shown] of [
|
|
326
|
+
[1, "exited (1)"],
|
|
327
|
+
[null, "exited (signal)"],
|
|
328
|
+
] as const) {
|
|
329
|
+
const error = await ensureDevServer({
|
|
330
|
+
config,
|
|
331
|
+
intervalMs: 2,
|
|
332
|
+
probe: async () => false,
|
|
333
|
+
start: (() => ({
|
|
334
|
+
child: exitWith(code),
|
|
335
|
+
logFile: "/app/.catalyst/dev-server-3005.log",
|
|
336
|
+
})) as any,
|
|
337
|
+
}).catch((e) => e)
|
|
338
|
+
expect(error.code).toBe("NATIVE-006")
|
|
339
|
+
expect(error.message).toContain(shown)
|
|
340
|
+
expect(error.message).toContain("/app/.catalyst/dev-server-3005.log")
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it("times out with NATIVE-006", async () => {
|
|
345
|
+
const error = await ensureDevServer({
|
|
346
|
+
config,
|
|
347
|
+
timeoutMs: 20,
|
|
348
|
+
intervalMs: 5,
|
|
349
|
+
probe: async () => false,
|
|
350
|
+
start: (() => ({ child: null, logFile: "/tmp/x.log" })) as any,
|
|
351
|
+
}).catch((e) => e)
|
|
352
|
+
expect(error.code).toBe("NATIVE-006")
|
|
353
|
+
expect(error.message).toContain("/tmp/x.log")
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it("probes a real HTTP server", async () => {
|
|
357
|
+
const server = http.createServer((_req, res) => {
|
|
358
|
+
res.statusCode = 500
|
|
359
|
+
res.end()
|
|
360
|
+
})
|
|
361
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
|
|
362
|
+
const { port } = server.address() as any
|
|
363
|
+
try {
|
|
364
|
+
expect(await isServerAnswering({ host: "127.0.0.1", port })).toBe(true)
|
|
365
|
+
} finally {
|
|
366
|
+
await new Promise((resolve) => server.close(resolve))
|
|
367
|
+
}
|
|
368
|
+
expect(await isServerAnswering({ host: "127.0.0.1", port, timeoutMs: 200 })).toBe(false)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it("gives up on a server that accepts but never answers", async () => {
|
|
372
|
+
const sockets: net.Socket[] = []
|
|
373
|
+
const server = net.createServer((socket) => void sockets.push(socket))
|
|
374
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
|
|
375
|
+
const { port } = server.address() as any
|
|
376
|
+
try {
|
|
377
|
+
expect(await isServerAnswering({ host: "127.0.0.1", port, timeoutMs: 50 })).toBe(false)
|
|
378
|
+
} finally {
|
|
379
|
+
for (const socket of sockets) socket.destroy()
|
|
380
|
+
await new Promise((resolve) => server.close(resolve))
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
describe("LAN address migration", () => {
|
|
386
|
+
// This machine is 192.168.1.20; LOCAL_IP still holds the address an
|
|
387
|
+
// earlier setup wrote before DHCP moved the machine.
|
|
388
|
+
const HERE = new Set(["127.0.0.1", "::1", "192.168.1.20", "jane-mac"])
|
|
389
|
+
const saved = {
|
|
390
|
+
NODE_SERVER_HOSTNAME: "192.168.1.20",
|
|
391
|
+
NODE_SERVER_PORT: 3005,
|
|
392
|
+
PUBLIC_STATIC_ASSET_URL: "http://192.168.1.20:3005",
|
|
393
|
+
API_URL: "https://api.acme.com",
|
|
394
|
+
CDN: "http://8.8.8.8/x",
|
|
395
|
+
OLD_API: "http://10.0.0.4:8080/api",
|
|
396
|
+
DB_HOST: "172.18.0.5",
|
|
397
|
+
BIND: "0.0.0.0",
|
|
398
|
+
LOOPBACK: "http://127.0.0.1:9000",
|
|
399
|
+
WEBVIEW_CONFIG: {
|
|
400
|
+
LOCAL_IP: "10.0.0.4",
|
|
401
|
+
port: "3005",
|
|
402
|
+
accessControl: { allowedUrls: ["http://192.168.1.20:3005/*", "https://acme.com/*"] },
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
it("puts localhost back for this machine's addresses and the saved LOCAL_IP only", () => {
|
|
407
|
+
const { config, changes } = planLanAddressMigration(saved, { localHosts: HERE })
|
|
408
|
+
expect(config).toMatchObject({
|
|
409
|
+
NODE_SERVER_HOSTNAME: "localhost",
|
|
410
|
+
PUBLIC_STATIC_ASSET_URL: "http://localhost:3005",
|
|
411
|
+
API_URL: "https://api.acme.com",
|
|
412
|
+
CDN: "http://8.8.8.8/x",
|
|
413
|
+
OLD_API: "http://localhost:8080/api",
|
|
414
|
+
WEBVIEW_CONFIG: {
|
|
415
|
+
LOCAL_IP: "localhost",
|
|
416
|
+
accessControl: { allowedUrls: ["http://localhost:3005/*", "https://acme.com/*"] },
|
|
417
|
+
},
|
|
418
|
+
})
|
|
419
|
+
expect(changes.map((change) => change.path)).toEqual([
|
|
420
|
+
"NODE_SERVER_HOSTNAME",
|
|
421
|
+
"PUBLIC_STATIC_ASSET_URL",
|
|
422
|
+
"OLD_API",
|
|
423
|
+
"WEBVIEW_CONFIG.LOCAL_IP",
|
|
424
|
+
"WEBVIEW_CONFIG.accessControl.allowedUrls[0]",
|
|
425
|
+
])
|
|
426
|
+
expect(saved.NODE_SERVER_HOSTNAME).toBe("192.168.1.20")
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it("leaves a Docker bind address, a container IP and loopback alone", () => {
|
|
430
|
+
const docker = {
|
|
431
|
+
NODE_SERVER_HOSTNAME: "0.0.0.0",
|
|
432
|
+
DB_HOST: "172.18.0.5",
|
|
433
|
+
REDIS_URL: "http://172.18.0.6:6379",
|
|
434
|
+
LOOPBACK: "http://127.0.0.1:9000",
|
|
435
|
+
WEBVIEW_CONFIG: { LOCAL_IP: "localhost" },
|
|
436
|
+
}
|
|
437
|
+
expect(planLanAddressMigration(docker, { localHosts: HERE }).changes).toEqual([])
|
|
438
|
+
// Even when 0.0.0.0 or a loopback address is reported as this machine's.
|
|
439
|
+
const odd = new Set(["0.0.0.0", "127.0.0.1"])
|
|
440
|
+
expect(planLanAddressMigration(docker, { localHosts: odd }).changes).toEqual([])
|
|
441
|
+
// And a LOCAL_IP of 0.0.0.0 or 127.x is not a saved LAN address.
|
|
442
|
+
const bound = { BIND: "0.0.0.0", WEBVIEW_CONFIG: { LOCAL_IP: "0.0.0.0" } }
|
|
443
|
+
expect(planLanAddressMigration(bound, { localHosts: new Set() }).changes).toEqual([])
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it("leaves WEBVIEW_CONFIG.url alone, and everything when allowPrivateHost is set", () => {
|
|
447
|
+
const withUrl = { WEBVIEW_CONFIG: { url: "http://10.0.0.4:3005", LOCAL_IP: "10.0.0.4" } }
|
|
448
|
+
expect(
|
|
449
|
+
planLanAddressMigration(withUrl, { localHosts: new Set() }).changes.map((c: any) => c.path)
|
|
450
|
+
).toEqual(["WEBVIEW_CONFIG.LOCAL_IP"])
|
|
451
|
+
const internal = { ...saved, WEBVIEW_CONFIG: { ...saved.WEBVIEW_CONFIG, allowPrivateHost: true } }
|
|
452
|
+
expect(planLanAddressMigration(internal, { localHosts: HERE }).changes).toEqual([])
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
function appWith(config: object) {
|
|
456
|
+
const cwd = tempDir()
|
|
457
|
+
const file = path.join(cwd, "config", "config.json")
|
|
458
|
+
fs.mkdirSync(path.dirname(file))
|
|
459
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 4) + "\n")
|
|
460
|
+
return { cwd, file }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
it("rewrites config.json once, keeping its indentation, and says what changed", () => {
|
|
464
|
+
const { cwd, file } = appWith(saved)
|
|
465
|
+
const logs: string[] = []
|
|
466
|
+
const first = migrateLanAddresses({
|
|
467
|
+
cwd,
|
|
468
|
+
env: {},
|
|
469
|
+
localHosts: HERE,
|
|
470
|
+
log: (message: string) => logs.push(message),
|
|
471
|
+
})
|
|
472
|
+
expect(first.ran).toBe(true)
|
|
473
|
+
expect(first.changes).toHaveLength(5)
|
|
474
|
+
const text = fs.readFileSync(file, "utf8")
|
|
475
|
+
expect(text).toMatch(/^\{\n {4}"NODE_SERVER_HOSTNAME": "localhost"/)
|
|
476
|
+
expect(text.endsWith("}\n")).toBe(true)
|
|
477
|
+
expect(JSON.parse(text).DB_HOST).toBe("172.18.0.5")
|
|
478
|
+
expect(logs.join("\n")).toContain("NODE_SERVER_HOSTNAME: 192.168.1.20 -> localhost")
|
|
479
|
+
expect(readLocalState({ cwd }).migrations.lanAddresses).toBeTruthy()
|
|
480
|
+
|
|
481
|
+
// A private address added later is the user's own: left alone.
|
|
482
|
+
fs.writeFileSync(file, JSON.stringify({ API_URL: "http://192.168.1.20:8080" }, null, 4))
|
|
483
|
+
expect(migrateLanAddresses({ cwd, env: {}, localHosts: HERE })).toEqual({ ran: false, changes: [] })
|
|
484
|
+
expect(JSON.parse(fs.readFileSync(file, "utf8")).API_URL).toBe("http://192.168.1.20:8080")
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it("never runs for a release or in CI", () => {
|
|
488
|
+
const releaseConfig = {
|
|
489
|
+
...saved,
|
|
490
|
+
WEBVIEW_CONFIG: { ...saved.WEBVIEW_CONFIG, android: { buildType: "release" } },
|
|
491
|
+
}
|
|
492
|
+
const cases: Array<[object, Record<string, unknown>]> = [
|
|
493
|
+
[saved, { release: true, env: {} }],
|
|
494
|
+
[saved, { env: { CI: "true" } }],
|
|
495
|
+
[releaseConfig, { platform: "android", env: {} }],
|
|
496
|
+
]
|
|
497
|
+
for (const [config, options] of cases) {
|
|
498
|
+
const { cwd, file } = appWith(config)
|
|
499
|
+
const before = fs.readFileSync(file, "utf8")
|
|
500
|
+
expect(migrateLanAddresses({ cwd, localHosts: HERE, ...options })).toEqual({
|
|
501
|
+
ran: false,
|
|
502
|
+
changes: [],
|
|
503
|
+
})
|
|
504
|
+
expect(fs.readFileSync(file, "utf8")).toBe(before)
|
|
505
|
+
// Not recorded either: the next debug build on this machine still runs it.
|
|
506
|
+
expect(readLocalState({ cwd }).migrations).toBeUndefined()
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it("never throws on a broken config", () => {
|
|
511
|
+
const cwd = tempDir()
|
|
512
|
+
const logs: string[] = []
|
|
513
|
+
expect(migrateLanAddresses({ cwd, env: {}, log: (message: string) => logs.push(message) })).toEqual({
|
|
514
|
+
ran: false,
|
|
515
|
+
changes: [],
|
|
516
|
+
})
|
|
517
|
+
expect(logs[0]).toContain("Could not check")
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it("leaves an unchanged config untouched", () => {
|
|
521
|
+
const cwd = tempDir()
|
|
522
|
+
const file = path.join(cwd, "config", "config.json")
|
|
523
|
+
fs.mkdirSync(path.dirname(file))
|
|
524
|
+
const text = '{"NODE_SERVER_HOSTNAME":"localhost"}'
|
|
525
|
+
fs.writeFileSync(file, text)
|
|
526
|
+
expect(migrateLanAddresses({ cwd, env: {} }).changes).toEqual([])
|
|
527
|
+
expect(fs.readFileSync(file, "utf8")).toBe(text)
|
|
528
|
+
})
|
|
529
|
+
})
|