@heybox/hb-sdk 0.3.3 → 0.4.1
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/README.md +63 -28
- package/dist/cli-chunks/browser-RAy8e8cV.cjs +635 -0
- package/dist/cli-chunks/create-Ds8A82lV.cjs +1376 -0
- package/dist/cli-chunks/deploy-D4uxwB2W.cjs +3730 -0
- package/dist/cli-chunks/dev-v-tcA7mM.cjs +955 -0
- package/dist/cli-chunks/doctor-C8NP7bow.cjs +186 -0
- package/dist/cli-chunks/index-BWrMUHh9.cjs +64023 -0
- package/dist/cli-chunks/index-DDqd9qAR.cjs +13348 -0
- package/dist/cli-chunks/login-BJVOo-hq.cjs +193 -0
- package/dist/cli-chunks/session-Iyxc2AGl.cjs +3040 -0
- package/dist/cli.cjs +19 -77707
- package/dist/devtools/mock-host/index.html +62 -2
- package/dist/devtools/mock-host/main.js +3246 -20
- package/dist/index.cjs.js +20 -0
- package/dist/index.esm.js +20 -0
- package/dist/miniapp-publish.cjs.js +2810 -4
- package/dist/miniapp-publish.esm.js +2809 -4
- package/dist/protocol.cjs.js +15 -0
- package/dist/protocol.esm.js +15 -1
- package/dist/templates/vue3-vite-ts/README.md.ejs +6 -2
- package/dist/vite.cjs.js +2814 -14
- package/dist/vite.esm.js +2814 -14
- package/package.json +6 -1
- package/skill/SKILL.md +19 -13
- package/skill/references/api-protocol.md +7 -2
- package/skill/references/api-root.md +24 -13
- package/skill/references/cli.md +339 -104
- package/skill/scripts/sync-references.mjs +17 -1
- package/skill/skill.json +4 -4
- package/types/index.d.ts +1 -1
- package/types/miniapp-manifest/schema.d.ts +2 -1
- package/types/miniapp-publish/index.d.ts +2 -1
- package/types/modules/viewport/index.d.ts +33 -0
- package/types/protocol/capabilities.d.ts +17 -2
- package/types/protocol.d.ts +2 -2
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_module = require('node:module');
|
|
4
|
+
var fs = require('node:fs');
|
|
5
|
+
var fs$1 = require('node:fs/promises');
|
|
6
|
+
var os = require('node:os');
|
|
7
|
+
var path = require('node:path');
|
|
8
|
+
var node_url = require('node:url');
|
|
9
|
+
var net = require('node:net');
|
|
10
|
+
var node_http = require('node:http');
|
|
11
|
+
var browser = require('./browser-RAy8e8cV.cjs');
|
|
12
|
+
var index = require('./index-DDqd9qAR.cjs');
|
|
13
|
+
require('node:process');
|
|
14
|
+
require('node:buffer');
|
|
15
|
+
require('node:util');
|
|
16
|
+
require('node:child_process');
|
|
17
|
+
require('path');
|
|
18
|
+
require('os');
|
|
19
|
+
require('readline');
|
|
20
|
+
require('tty');
|
|
21
|
+
require('assert');
|
|
22
|
+
require('events');
|
|
23
|
+
require('stream');
|
|
24
|
+
require('buffer');
|
|
25
|
+
require('util');
|
|
26
|
+
|
|
27
|
+
class Locked extends Error {
|
|
28
|
+
constructor(port) {
|
|
29
|
+
super(`${port} is locked`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lockedPorts = {
|
|
34
|
+
old: new Set(),
|
|
35
|
+
young: new Set(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// On this interval, the old locked ports are discarded,
|
|
39
|
+
// the young locked ports are moved to old locked ports,
|
|
40
|
+
// and a new young set for locked ports are created.
|
|
41
|
+
const releaseOldLockedPortsIntervalMs = 1000 * 15;
|
|
42
|
+
|
|
43
|
+
// Keep `reserve` deliberately process-wide by port number.
|
|
44
|
+
// It is meant to avoid in-process races, not to model every possible
|
|
45
|
+
// IPv4/IPv6 or host-specific bind combination.
|
|
46
|
+
const reservedPorts = new Set();
|
|
47
|
+
|
|
48
|
+
// Lazily create timeout on first use
|
|
49
|
+
let timeout;
|
|
50
|
+
|
|
51
|
+
const getLocalHosts = () => {
|
|
52
|
+
const interfaces = os.networkInterfaces();
|
|
53
|
+
|
|
54
|
+
// Add undefined value for createServer function to use default host,
|
|
55
|
+
// and default IPv4 host in case createServer defaults to IPv6.
|
|
56
|
+
const results = new Set([undefined, '0.0.0.0']);
|
|
57
|
+
|
|
58
|
+
for (const _interface of Object.values(interfaces)) {
|
|
59
|
+
for (const config of _interface) {
|
|
60
|
+
results.add(config.address);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const checkAvailablePort = options =>
|
|
68
|
+
new Promise((resolve, reject) => {
|
|
69
|
+
const server = net.createServer();
|
|
70
|
+
server.unref();
|
|
71
|
+
server.on('error', reject);
|
|
72
|
+
|
|
73
|
+
server.listen(options, () => {
|
|
74
|
+
const {port} = server.address();
|
|
75
|
+
server.close(() => {
|
|
76
|
+
resolve(port);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const getAvailablePort = async (options, hosts) => {
|
|
82
|
+
if (options.host || options.port === 0) {
|
|
83
|
+
return checkAvailablePort(options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const host of hosts) {
|
|
87
|
+
try {
|
|
88
|
+
await checkAvailablePort({port: options.port, host}); // eslint-disable-line no-await-in-loop
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return options.port;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isLockedPort = port => lockedPorts.old.has(port) || lockedPorts.young.has(port) || reservedPorts.has(port);
|
|
100
|
+
|
|
101
|
+
const portCheckSequence = function * (ports) {
|
|
102
|
+
if (ports) {
|
|
103
|
+
yield * ports;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
yield 0; // Fall back to 0 if anything else failed
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
async function getPorts(options) {
|
|
110
|
+
let ports;
|
|
111
|
+
let exclude = new Set();
|
|
112
|
+
|
|
113
|
+
if (options) {
|
|
114
|
+
if (options.port) {
|
|
115
|
+
ports = typeof options.port === 'number' ? [options.port] : options.port;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.exclude) {
|
|
119
|
+
const excludeIterable = options.exclude;
|
|
120
|
+
|
|
121
|
+
if (typeof excludeIterable[Symbol.iterator] !== 'function') {
|
|
122
|
+
throw new TypeError('The `exclude` option must be an iterable.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const element of excludeIterable) {
|
|
126
|
+
if (typeof element !== 'number') {
|
|
127
|
+
throw new TypeError('Each item in the `exclude` option must be a number corresponding to the port you want excluded.');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!Number.isSafeInteger(element)) {
|
|
131
|
+
throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
exclude = new Set(excludeIterable);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const {reserve, ...netOptions} = options ?? {};
|
|
140
|
+
|
|
141
|
+
if (timeout === undefined) {
|
|
142
|
+
timeout = setTimeout(() => {
|
|
143
|
+
timeout = undefined;
|
|
144
|
+
|
|
145
|
+
lockedPorts.old = lockedPorts.young;
|
|
146
|
+
lockedPorts.young = new Set();
|
|
147
|
+
}, releaseOldLockedPortsIntervalMs);
|
|
148
|
+
|
|
149
|
+
// Does not exist in some environments (Electron, Jest jsdom env, browser, etc).
|
|
150
|
+
if (timeout.unref) {
|
|
151
|
+
timeout.unref();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hosts = getLocalHosts();
|
|
156
|
+
|
|
157
|
+
for (const port of portCheckSequence(ports)) {
|
|
158
|
+
try {
|
|
159
|
+
if (exclude.has(port)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let availablePort = await getAvailablePort({...netOptions, port}, hosts); // eslint-disable-line no-await-in-loop
|
|
164
|
+
while (isLockedPort(availablePort)) {
|
|
165
|
+
if (port !== 0) {
|
|
166
|
+
throw new Locked(port);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
availablePort = await getAvailablePort({...netOptions, port}, hosts); // eslint-disable-line no-await-in-loop
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (reserve) {
|
|
173
|
+
reservedPorts.add(availablePort);
|
|
174
|
+
} else {
|
|
175
|
+
lockedPorts.young.add(availablePort);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return availablePort;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw new Error('No available ports found');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const MINI_PROGRAM_URL_QUERY_PARAM$1 = 'mini_url';
|
|
190
|
+
const LAN_ADDRESSES_PATH = '/__hb_sdk_lan_addresses__';
|
|
191
|
+
const MOCK_NETWORK_PROXY_PATH = '/__hb_sdk_mock_network__';
|
|
192
|
+
const MOCK_NETWORK_PROXY_BODY_LIMIT = 1024 * 1024;
|
|
193
|
+
const DEV_LISTEN_HOST$1 = '0.0.0.0';
|
|
194
|
+
const LOCAL_DEV_URL_HOST$1 = '127.0.0.1';
|
|
195
|
+
const MOCK_HOST_ROOT_CANDIDATES = [
|
|
196
|
+
path.resolve(__dirname, 'devtools/mock-host'),
|
|
197
|
+
path.resolve(__dirname, '../devtools/mock-host'),
|
|
198
|
+
path.resolve(__dirname, '../../devtools/mock-host'),
|
|
199
|
+
path.resolve(__dirname, '../../../devtools/mock-host'),
|
|
200
|
+
];
|
|
201
|
+
async function startMiniProgramMockHostServer(options) {
|
|
202
|
+
const root = options.root ?? resolveMiniProgramMockHostRoot(options.rootCandidates);
|
|
203
|
+
assertCompleteMiniProgramMockHostRoot(root);
|
|
204
|
+
const server = createMiniProgramMockHostServer(root, {
|
|
205
|
+
appUrl: options.appUrl,
|
|
206
|
+
defaultLanAddressId: options.defaultLanAddressId,
|
|
207
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
208
|
+
lanAddresses: options.lanAddresses ?? [],
|
|
209
|
+
macAppProtocol: options.macAppProtocol,
|
|
210
|
+
});
|
|
211
|
+
const port = await listenHttpServer(server, {
|
|
212
|
+
port: options.port,
|
|
213
|
+
}, options.getPort ?? getPorts);
|
|
214
|
+
return {
|
|
215
|
+
close: () => new Promise((resolve) => {
|
|
216
|
+
server.close(() => resolve());
|
|
217
|
+
}),
|
|
218
|
+
networkUrls: createMiniProgramMockHostNetworkUrls({
|
|
219
|
+
lanAddresses: options.lanAddresses,
|
|
220
|
+
port,
|
|
221
|
+
}),
|
|
222
|
+
port,
|
|
223
|
+
root,
|
|
224
|
+
server,
|
|
225
|
+
url: createMiniProgramMockHostUrl({
|
|
226
|
+
appUrl: options.appUrl,
|
|
227
|
+
port,
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function resolveMiniProgramMockHostRoot(candidates = MOCK_HOST_ROOT_CANDIDATES) {
|
|
232
|
+
const found = candidates.find(isCompleteMiniProgramMockHostRoot);
|
|
233
|
+
if (!found) {
|
|
234
|
+
throw createMissingMockHostError();
|
|
235
|
+
}
|
|
236
|
+
return found;
|
|
237
|
+
}
|
|
238
|
+
function assertCompleteMiniProgramMockHostRoot(root) {
|
|
239
|
+
if (!isCompleteMiniProgramMockHostRoot(root)) {
|
|
240
|
+
throw createMissingMockHostError();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function isCompleteMiniProgramMockHostRoot(root) {
|
|
244
|
+
return fs.existsSync(path.join(root, 'index.html')) && fs.existsSync(path.join(root, 'main.js'));
|
|
245
|
+
}
|
|
246
|
+
function createMissingMockHostError() {
|
|
247
|
+
return new Error('未找到完整的 hb-sdk mock host 静态产物。请先执行 @heybox/hb-sdk 的 build:mock-host。');
|
|
248
|
+
}
|
|
249
|
+
function createMiniProgramMockHostUrl(options) {
|
|
250
|
+
return createMiniProgramMockHostUrlWithHost({
|
|
251
|
+
...options,
|
|
252
|
+
host: LOCAL_DEV_URL_HOST$1,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function createMiniProgramMockHostNetworkUrls(options) {
|
|
256
|
+
return (options.lanAddresses ?? []).map((address) => createMiniProgramMockHostUrlWithHost({
|
|
257
|
+
appUrl: address.appUrl,
|
|
258
|
+
host: address.address,
|
|
259
|
+
port: options.port,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
function createMiniProgramMockHostUrlWithHost(options) {
|
|
263
|
+
const url = new URL(`http://${options.host}:${options.port}/`);
|
|
264
|
+
url.searchParams.set(MINI_PROGRAM_URL_QUERY_PARAM$1, options.appUrl);
|
|
265
|
+
return url.toString();
|
|
266
|
+
}
|
|
267
|
+
function createMiniProgramMockHostServer(root, runtime) {
|
|
268
|
+
return node_http.createServer(async (request, response) => {
|
|
269
|
+
const requestUrl = new URL(request.url, 'http://localhost');
|
|
270
|
+
const pathname = decodeURIComponent(requestUrl.pathname);
|
|
271
|
+
if (isMockHostDocumentPath(pathname) && !requestUrl.searchParams.has(MINI_PROGRAM_URL_QUERY_PARAM$1)) {
|
|
272
|
+
const targetUrl = new URL(requestUrl);
|
|
273
|
+
targetUrl.pathname = '/';
|
|
274
|
+
targetUrl.searchParams.set(MINI_PROGRAM_URL_QUERY_PARAM$1, runtime.appUrl);
|
|
275
|
+
response.writeHead(302, {
|
|
276
|
+
location: `${targetUrl.pathname}${targetUrl.search}`,
|
|
277
|
+
'cache-control': 'no-store',
|
|
278
|
+
});
|
|
279
|
+
response.end();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (pathname === LAN_ADDRESSES_PATH) {
|
|
283
|
+
writeJsonResponse(response, 200, {
|
|
284
|
+
defaultLanAddressId: runtime.defaultLanAddressId,
|
|
285
|
+
lanAddresses: runtime.lanAddresses,
|
|
286
|
+
macAppProtocol: runtime.macAppProtocol,
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (pathname === MOCK_NETWORK_PROXY_PATH) {
|
|
291
|
+
await handleMockNetworkProxy(request, response, runtime.fetchImpl);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const targetPath = resolveStaticPath(root, pathname);
|
|
295
|
+
if (!targetPath) {
|
|
296
|
+
response.writeHead(404);
|
|
297
|
+
response.end('Not found');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const stat = fs.statSync(targetPath);
|
|
302
|
+
if (stat.isDirectory()) {
|
|
303
|
+
response.writeHead(404);
|
|
304
|
+
response.end('Not found');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
response.writeHead(200, {
|
|
308
|
+
'content-type': readContentType(targetPath),
|
|
309
|
+
'cache-control': 'no-store',
|
|
310
|
+
});
|
|
311
|
+
fs.createReadStream(targetPath).pipe(response);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
response.writeHead(404);
|
|
315
|
+
response.end('Not found');
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function isMockHostDocumentPath(pathname) {
|
|
320
|
+
return pathname === '/' || pathname === '/index.html';
|
|
321
|
+
}
|
|
322
|
+
async function handleMockNetworkProxy(request, response, fetchImpl) {
|
|
323
|
+
if (request.method === 'OPTIONS') {
|
|
324
|
+
writeJsonResponse(response, 204, undefined);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (request.method !== 'POST') {
|
|
328
|
+
writeJsonResponse(response, 405, {
|
|
329
|
+
code: 'METHOD_NOT_ALLOWED',
|
|
330
|
+
message: 'mock network proxy only supports POST',
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const payload = await readJsonRequestBody(request);
|
|
336
|
+
const result = await requestMockNetwork(payload, fetchImpl);
|
|
337
|
+
writeJsonResponse(response, 200, result);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
writeJsonResponse(response, readMockNetworkErrorStatus(error), toMockNetworkErrorPayload(error));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function requestMockNetwork(payload, fetchImpl) {
|
|
344
|
+
if (!isRecord(payload) || typeof payload.url !== 'string' || !payload.url.trim()) {
|
|
345
|
+
throw createMockNetworkError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是非空字符串');
|
|
346
|
+
}
|
|
347
|
+
const url = createMockNetworkUrl(payload.url, payload.params);
|
|
348
|
+
const method = typeof payload.method === 'string' && payload.method.trim() ? payload.method.trim().toUpperCase() : 'GET';
|
|
349
|
+
const headers = normalizeMockNetworkHeaders(payload.headers);
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
const timeout = Number(payload.timeout) > 0 ? Number(payload.timeout) : 10000;
|
|
352
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
353
|
+
try {
|
|
354
|
+
const response = await fetchImpl(url.toString(), {
|
|
355
|
+
method,
|
|
356
|
+
headers,
|
|
357
|
+
body: createMockNetworkBody(method, payload.data, headers),
|
|
358
|
+
redirect: 'follow',
|
|
359
|
+
signal: controller.signal,
|
|
360
|
+
});
|
|
361
|
+
const contentType = response.headers.get('content-type') || '';
|
|
362
|
+
const text = await response.text();
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
return {
|
|
365
|
+
data: contentType.includes('application/json') ? safeJsonParse(text) : text,
|
|
366
|
+
status: response.status,
|
|
367
|
+
statusText: response.statusText,
|
|
368
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
throw createMockNetworkError(502, 'MOCK_NETWORK_REQUEST_FAILED', error instanceof Error && error.name === 'AbortError'
|
|
374
|
+
? `network.request timeout after ${timeout}ms`
|
|
375
|
+
: readErrorMessage(error));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function createMockNetworkUrl(input, params) {
|
|
379
|
+
let url;
|
|
380
|
+
try {
|
|
381
|
+
url = new URL(input);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
throw createMockNetworkError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是合法 URL');
|
|
385
|
+
}
|
|
386
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
387
|
+
throw createMockNetworkError(400, 'INVALID_NETWORK_REQUEST', 'network.request 仅支持 HTTP(S) URL');
|
|
388
|
+
}
|
|
389
|
+
if (isRecord(params)) {
|
|
390
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
391
|
+
if (value !== undefined && value !== null) {
|
|
392
|
+
url.searchParams.set(key, String(value));
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return url;
|
|
397
|
+
}
|
|
398
|
+
function normalizeMockNetworkHeaders(headers) {
|
|
399
|
+
const normalized = new Headers();
|
|
400
|
+
if (!isRecord(headers)) {
|
|
401
|
+
return normalized;
|
|
402
|
+
}
|
|
403
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
404
|
+
if (typeof value === 'string' && !isBlockedMockNetworkHeader(key)) {
|
|
405
|
+
normalized.set(key, value);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
return normalized;
|
|
409
|
+
}
|
|
410
|
+
function isBlockedMockNetworkHeader(key) {
|
|
411
|
+
return [
|
|
412
|
+
'connection',
|
|
413
|
+
'content-length',
|
|
414
|
+
'host',
|
|
415
|
+
'keep-alive',
|
|
416
|
+
'proxy-authenticate',
|
|
417
|
+
'proxy-authorization',
|
|
418
|
+
'te',
|
|
419
|
+
'trailer',
|
|
420
|
+
'transfer-encoding',
|
|
421
|
+
'upgrade',
|
|
422
|
+
].includes(key.toLowerCase());
|
|
423
|
+
}
|
|
424
|
+
function createMockNetworkBody(method, data, headers) {
|
|
425
|
+
if (data === undefined || method === 'GET' || method === 'HEAD') {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
if (typeof data === 'string') {
|
|
429
|
+
return data;
|
|
430
|
+
}
|
|
431
|
+
if (!headers.has('content-type')) {
|
|
432
|
+
headers.set('content-type', 'application/json');
|
|
433
|
+
}
|
|
434
|
+
return JSON.stringify(data);
|
|
435
|
+
}
|
|
436
|
+
function readJsonRequestBody(request) {
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
const chunks = [];
|
|
439
|
+
let size = 0;
|
|
440
|
+
request.on('data', (chunk) => {
|
|
441
|
+
const buffer = Buffer.from(chunk);
|
|
442
|
+
size += buffer.byteLength;
|
|
443
|
+
if (size > MOCK_NETWORK_PROXY_BODY_LIMIT) {
|
|
444
|
+
reject(createMockNetworkError(413, 'REQUEST_ENTITY_TOO_LARGE', 'mock network proxy request body too large'));
|
|
445
|
+
request.destroy();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
chunks.push(buffer);
|
|
449
|
+
});
|
|
450
|
+
request.on('error', reject);
|
|
451
|
+
request.on('aborted', () => {
|
|
452
|
+
reject('mock network proxy request aborted');
|
|
453
|
+
});
|
|
454
|
+
request.on('end', () => {
|
|
455
|
+
const rawBody = Buffer.concat(chunks).toString('utf8');
|
|
456
|
+
if (!rawBody.trim()) {
|
|
457
|
+
resolve({});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
resolve(JSON.parse(rawBody));
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
reject(createMockNetworkError(400, 'INVALID_JSON', 'mock network proxy received invalid JSON'));
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function writeJsonResponse(response, status, body) {
|
|
470
|
+
response.writeHead(status, {
|
|
471
|
+
'content-type': 'application/json; charset=utf-8',
|
|
472
|
+
'cache-control': 'no-store',
|
|
473
|
+
});
|
|
474
|
+
response.end(body === undefined ? '' : JSON.stringify(body));
|
|
475
|
+
}
|
|
476
|
+
function safeJsonParse(text) {
|
|
477
|
+
try {
|
|
478
|
+
return JSON.parse(text);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return text;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function isRecord(value) {
|
|
485
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
486
|
+
}
|
|
487
|
+
function createMockNetworkError(status, code, message) {
|
|
488
|
+
const error = new Error(message);
|
|
489
|
+
error.code = code;
|
|
490
|
+
error.status = status;
|
|
491
|
+
return error;
|
|
492
|
+
}
|
|
493
|
+
function readMockNetworkErrorStatus(error) {
|
|
494
|
+
return isObjectLike(error) && typeof error.status === 'number' ? error.status : 500;
|
|
495
|
+
}
|
|
496
|
+
function toMockNetworkErrorPayload(error) {
|
|
497
|
+
if (isObjectLike(error) && typeof error.code === 'string' && typeof error.message === 'string') {
|
|
498
|
+
return {
|
|
499
|
+
code: error.code,
|
|
500
|
+
message: error.message,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
code: 'MOCK_NETWORK_ERROR',
|
|
505
|
+
message: readErrorMessage(error),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function isObjectLike(value) {
|
|
509
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null;
|
|
510
|
+
}
|
|
511
|
+
function resolveStaticPath(root, pathname) {
|
|
512
|
+
const safePathname = pathname === '/' ? '/index.html' : pathname;
|
|
513
|
+
const normalizedRoot = path.normalize(root);
|
|
514
|
+
const targetPath = path.normalize(path.join(normalizedRoot, safePathname));
|
|
515
|
+
if (targetPath !== normalizedRoot && !targetPath.startsWith(`${normalizedRoot}${path.sep}`)) {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
return targetPath;
|
|
519
|
+
}
|
|
520
|
+
function readContentType(filePath) {
|
|
521
|
+
if (filePath.endsWith('.html')) {
|
|
522
|
+
return 'text/html; charset=utf-8';
|
|
523
|
+
}
|
|
524
|
+
if (filePath.endsWith('.js')) {
|
|
525
|
+
return 'text/javascript; charset=utf-8';
|
|
526
|
+
}
|
|
527
|
+
if (filePath.endsWith('.css')) {
|
|
528
|
+
return 'text/css; charset=utf-8';
|
|
529
|
+
}
|
|
530
|
+
if (filePath.endsWith('.json')) {
|
|
531
|
+
return 'application/json; charset=utf-8';
|
|
532
|
+
}
|
|
533
|
+
if (filePath.endsWith('.svg')) {
|
|
534
|
+
return 'image/svg+xml';
|
|
535
|
+
}
|
|
536
|
+
return 'application/octet-stream';
|
|
537
|
+
}
|
|
538
|
+
async function listenHttpServer(server, options, getPortImpl) {
|
|
539
|
+
const maxAttempts = 20;
|
|
540
|
+
const port = await getPortImpl({
|
|
541
|
+
host: DEV_LISTEN_HOST$1,
|
|
542
|
+
port: createPortCandidates(options.port, maxAttempts),
|
|
543
|
+
});
|
|
544
|
+
if (port < options.port || port >= options.port + maxAttempts) {
|
|
545
|
+
throw new Error(`无法找到可用 mock host 端口,起始端口: ${options.port}`);
|
|
546
|
+
}
|
|
547
|
+
await new Promise((resolve, reject) => {
|
|
548
|
+
const onError = (error) => {
|
|
549
|
+
server.off('listening', onListening);
|
|
550
|
+
reject(error);
|
|
551
|
+
};
|
|
552
|
+
const onListening = () => {
|
|
553
|
+
server.off('error', onError);
|
|
554
|
+
resolve();
|
|
555
|
+
};
|
|
556
|
+
server.once('error', onError);
|
|
557
|
+
server.once('listening', onListening);
|
|
558
|
+
server.listen(port, DEV_LISTEN_HOST$1);
|
|
559
|
+
});
|
|
560
|
+
const address = server.address();
|
|
561
|
+
return address.port;
|
|
562
|
+
}
|
|
563
|
+
function createPortCandidates(startPort, count) {
|
|
564
|
+
return Array.from({ length: count }, (_, index) => startPort + index);
|
|
565
|
+
}
|
|
566
|
+
function readErrorMessage(error) {
|
|
567
|
+
return error instanceof Error ? error.message : String(error);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const MINI_PROGRAM_URL_QUERY_PARAM = 'mini_url';
|
|
571
|
+
const MINI_PROGRAM_RUNTIME_URL_QUERY_PARAM = 'runtime_url';
|
|
572
|
+
const MINI_PROGRAM_DEV_SHELL_URL = 'heybox-mini-dev://sandbox';
|
|
573
|
+
const OPEN_IN_APP_URL = 'https://api.xiaoheihe.cn/open_inapp/';
|
|
574
|
+
function createMacAppProtocol(appUrl, options = {}) {
|
|
575
|
+
return createHeyboxProtocol(createMiniProgramDevShellOpenWindowPayload(appUrl, options));
|
|
576
|
+
}
|
|
577
|
+
function createMobileAppQrPayload(appUrl, options = {}) {
|
|
578
|
+
return `${OPEN_IN_APP_URL}#${createHeyboxProtocol(createMiniProgramDevShellOpenWindowPayload(appUrl, { ...options, encodeMiniUrl: false }))}`;
|
|
579
|
+
}
|
|
580
|
+
function createMiniProgramDevShellOpenWindowPayload(appUrl, options = {}) {
|
|
581
|
+
const devShellUrl = options.encodeMiniUrl === false
|
|
582
|
+
? createPartiallyEncodedMiniProgramDevShellUrl(appUrl, options.runtimeUrl)
|
|
583
|
+
: createEncodedMiniProgramDevShellUrl(appUrl, options.runtimeUrl);
|
|
584
|
+
return {
|
|
585
|
+
protocol_type: 'openWindow',
|
|
586
|
+
full_screen: true,
|
|
587
|
+
mini_program: '1',
|
|
588
|
+
navigation_bar: {
|
|
589
|
+
title: '',
|
|
590
|
+
},
|
|
591
|
+
webview: {
|
|
592
|
+
url: devShellUrl,
|
|
593
|
+
pull: false,
|
|
594
|
+
refresh: false,
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function createPartiallyEncodedMiniProgramDevShellUrl(appUrl, runtimeUrl) {
|
|
599
|
+
let devShellUrl = `${MINI_PROGRAM_DEV_SHELL_URL}?${MINI_PROGRAM_URL_QUERY_PARAM}=${appUrl}`;
|
|
600
|
+
if (hasRuntimeUrl(runtimeUrl)) {
|
|
601
|
+
devShellUrl += `&${MINI_PROGRAM_RUNTIME_URL_QUERY_PARAM}=${encodeURIComponent(runtimeUrl)}`;
|
|
602
|
+
}
|
|
603
|
+
return devShellUrl;
|
|
604
|
+
}
|
|
605
|
+
function createEncodedMiniProgramDevShellUrl(appUrl, runtimeUrl) {
|
|
606
|
+
const devShellUrl = new URL(MINI_PROGRAM_DEV_SHELL_URL);
|
|
607
|
+
devShellUrl.searchParams.set(MINI_PROGRAM_URL_QUERY_PARAM, appUrl);
|
|
608
|
+
if (hasRuntimeUrl(runtimeUrl)) {
|
|
609
|
+
devShellUrl.searchParams.set(MINI_PROGRAM_RUNTIME_URL_QUERY_PARAM, runtimeUrl);
|
|
610
|
+
}
|
|
611
|
+
return devShellUrl.toString();
|
|
612
|
+
}
|
|
613
|
+
function hasRuntimeUrl(runtimeUrl) {
|
|
614
|
+
return runtimeUrl !== undefined && runtimeUrl !== '';
|
|
615
|
+
}
|
|
616
|
+
function createHeyboxProtocol(payload) {
|
|
617
|
+
return `heybox://${encodeURIComponent(JSON.stringify(payload))}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const DEFAULT_APP_PORT = 5173;
|
|
621
|
+
const DEFAULT_MOCK_PORT = 5174;
|
|
622
|
+
const DEV_LISTEN_HOST = '0.0.0.0';
|
|
623
|
+
const LOCAL_DEV_URL_HOST = '127.0.0.1';
|
|
624
|
+
// Shared with Heybox H5 Vite plugins so hb-sdk can own the launch output.
|
|
625
|
+
const MANAGED_DEV_OUTPUT_ENV = 'HEYBOX_DEV_SERVER_MANAGED_OUTPUT';
|
|
626
|
+
const VITE_LOG_LEVEL = 'warn';
|
|
627
|
+
async function runDevCommand(options, runtime = {}) {
|
|
628
|
+
const logger = runtime.logger ?? index.createCliLogger();
|
|
629
|
+
const fetchImpl = runtime.fetchImpl ?? fetch;
|
|
630
|
+
const openUrl = runtime.openExternalUrl ?? browser.openExternalUrl;
|
|
631
|
+
const projectRoot = findProjectRoot(runtime.cwd ?? process.cwd());
|
|
632
|
+
const vite = await logger.task('正在加载项目 Vite', () => loadProjectVite(projectRoot), { successText: '已加载项目 Vite' });
|
|
633
|
+
const appServer = await logger.task('正在创建 Vite dev server', () => withManagedDevOutputEnv(() => vite.createServer(createViteServerOptions(projectRoot, options)), runtime.env), { successText: '已创建 Vite dev server' });
|
|
634
|
+
await logger.task('正在启动 Vite dev server', () => appServer.listen(options.port ?? DEFAULT_APP_PORT), {
|
|
635
|
+
successText: 'Vite dev server 已启动',
|
|
636
|
+
});
|
|
637
|
+
const appUrl = await logger.task('正在探测小程序入口', () => resolveViteAppUrl(appServer, options, fetchImpl), {
|
|
638
|
+
successText: '已确定小程序入口',
|
|
639
|
+
});
|
|
640
|
+
const lanAddresses = createLanAddressCandidates({
|
|
641
|
+
appUrl,
|
|
642
|
+
interfaces: (runtime.networkInterfaces ?? os.networkInterfaces)(),
|
|
643
|
+
runtimeUrl: options.runtimeUrl,
|
|
644
|
+
viteNetworkUrls: appServer.resolvedUrls?.network ?? [],
|
|
645
|
+
});
|
|
646
|
+
const closers = [() => appServer.close()];
|
|
647
|
+
let mockHost;
|
|
648
|
+
try {
|
|
649
|
+
mockHost = await logger.task('正在启动 Mock runtime host', () => startMiniProgramMockHostServer({
|
|
650
|
+
appUrl,
|
|
651
|
+
fetchImpl,
|
|
652
|
+
getPort: runtime.getPort ?? getPorts,
|
|
653
|
+
defaultLanAddressId: lanAddresses[0]?.id,
|
|
654
|
+
lanAddresses,
|
|
655
|
+
macAppProtocol: createMacAppProtocol(appUrl, { runtimeUrl: options.runtimeUrl }),
|
|
656
|
+
port: options.mockPort ?? DEFAULT_MOCK_PORT,
|
|
657
|
+
root: runtime.mockHostRoot,
|
|
658
|
+
rootCandidates: runtime.mockHostRootCandidates,
|
|
659
|
+
}), { successText: 'Mock runtime host 已启动' });
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
await closeAll(closers);
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
closers.push(() => mockHost.close());
|
|
666
|
+
logger.success('Mock runtime host 已就绪');
|
|
667
|
+
logger.info(`Mock runtime host: ${mockHost.url}`);
|
|
668
|
+
logger.info(`Mini program URL: ${appUrl}`);
|
|
669
|
+
logger.info('Mac APP: use the button in Mock runtime host');
|
|
670
|
+
logger.info('Mobile APP: scan the QR code in Mock runtime host');
|
|
671
|
+
if (options.open !== false) {
|
|
672
|
+
logger.debug('正在打开浏览器调试页');
|
|
673
|
+
void openUrl(mockHost.url);
|
|
674
|
+
}
|
|
675
|
+
installShutdownHandlers(closers, runtime.process ?? process);
|
|
676
|
+
}
|
|
677
|
+
function findProjectRoot(startDir) {
|
|
678
|
+
let current = path.resolve(startDir);
|
|
679
|
+
while (true) {
|
|
680
|
+
if (fs.existsSync(path.join(current, 'package.json'))) {
|
|
681
|
+
return current;
|
|
682
|
+
}
|
|
683
|
+
const parent = path.dirname(current);
|
|
684
|
+
if (parent === current) {
|
|
685
|
+
throw new Error('当前目录或父目录未找到 package.json');
|
|
686
|
+
}
|
|
687
|
+
current = parent;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function loadProjectVite(projectRoot) {
|
|
691
|
+
const projectRequire = node_module.createRequire(path.join(projectRoot, 'package.json'));
|
|
692
|
+
try {
|
|
693
|
+
const packageJson = JSON.parse(await fs$1.readFile(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
694
|
+
if (!hasPackageDependency(packageJson, 'vite')) {
|
|
695
|
+
throw new Error('package.json 未声明 vite');
|
|
696
|
+
}
|
|
697
|
+
const viteEntry = projectRequire.resolve('vite');
|
|
698
|
+
return (await import(node_url.pathToFileURL(viteEntry).href));
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
throw new Error(`当前项目未安装 vite。请先安装项目依赖,或将 vite 放到项目 devDependencies。原始错误: ${index.readErrorMessage(error)}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function hasPackageDependency(packageJson, name) {
|
|
705
|
+
return Boolean(packageJson.dependencies?.[name] ||
|
|
706
|
+
packageJson.devDependencies?.[name] ||
|
|
707
|
+
packageJson.optionalDependencies?.[name] ||
|
|
708
|
+
packageJson.peerDependencies?.[name]);
|
|
709
|
+
}
|
|
710
|
+
function createViteServerOptions(projectRoot, options) {
|
|
711
|
+
const configFile = resolveProjectViteConfig(projectRoot);
|
|
712
|
+
const server = {
|
|
713
|
+
cors: true,
|
|
714
|
+
host: DEV_LISTEN_HOST,
|
|
715
|
+
port: options.port ?? DEFAULT_APP_PORT,
|
|
716
|
+
strictPort: false,
|
|
717
|
+
};
|
|
718
|
+
if (configFile) {
|
|
719
|
+
return {
|
|
720
|
+
configFile,
|
|
721
|
+
logLevel: VITE_LOG_LEVEL,
|
|
722
|
+
server,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
logLevel: VITE_LOG_LEVEL,
|
|
727
|
+
root: projectRoot,
|
|
728
|
+
server,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function resolveProjectViteConfig(projectRoot) {
|
|
732
|
+
const candidates = ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs'];
|
|
733
|
+
return candidates.map((file) => path.join(projectRoot, file)).find((file) => fs.existsSync(file));
|
|
734
|
+
}
|
|
735
|
+
async function resolveViteAppUrl(server, options, fetchImpl) {
|
|
736
|
+
const candidates = createViteAppUrlCandidates(server, options);
|
|
737
|
+
for (const candidate of candidates) {
|
|
738
|
+
if (await canReachDocumentUrl(candidate, fetchImpl)) {
|
|
739
|
+
return candidate;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return candidates[0];
|
|
743
|
+
}
|
|
744
|
+
function createViteAppUrlCandidates(server, options) {
|
|
745
|
+
const localUrl = rewriteLocalhostUrl(server.resolvedUrls?.local[0]);
|
|
746
|
+
const origin = readViteOrigin(localUrl, options);
|
|
747
|
+
const candidates = [localUrl, createH5EntryUrl(origin, readViteBase(server, localUrl))].filter((url) => Boolean(url));
|
|
748
|
+
return Array.from(new Set(candidates));
|
|
749
|
+
}
|
|
750
|
+
function readViteOrigin(localUrl, options) {
|
|
751
|
+
if (localUrl) {
|
|
752
|
+
try {
|
|
753
|
+
return new URL(localUrl).origin;
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
/* fall through */
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const port = options.port ?? DEFAULT_APP_PORT;
|
|
760
|
+
return `http://${LOCAL_DEV_URL_HOST}:${port}`;
|
|
761
|
+
}
|
|
762
|
+
function readViteBase(server, localUrl) {
|
|
763
|
+
const base = server.config?.base;
|
|
764
|
+
if (base && base !== '/') {
|
|
765
|
+
return base;
|
|
766
|
+
}
|
|
767
|
+
if (localUrl) {
|
|
768
|
+
try {
|
|
769
|
+
return new URL(localUrl).pathname;
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
/* fall through */
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return '/';
|
|
776
|
+
}
|
|
777
|
+
function createH5EntryUrl(origin, base) {
|
|
778
|
+
const pathname = `${ensureTrailingSlash(base)}src/index.html`;
|
|
779
|
+
return new URL(pathname, `${origin}/`).toString();
|
|
780
|
+
}
|
|
781
|
+
async function canReachDocumentUrl(url, fetchImpl) {
|
|
782
|
+
try {
|
|
783
|
+
const headResponse = await fetchImpl(url, {
|
|
784
|
+
method: 'HEAD',
|
|
785
|
+
redirect: 'follow',
|
|
786
|
+
});
|
|
787
|
+
if (headResponse.ok) {
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
if (headResponse.status !== 404 && headResponse.status !== 405) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
const getResponse = await fetchImpl(url, {
|
|
794
|
+
method: 'GET',
|
|
795
|
+
redirect: 'follow',
|
|
796
|
+
});
|
|
797
|
+
return getResponse.ok;
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function ensureTrailingSlash(value) {
|
|
804
|
+
return value.endsWith('/') ? value : `${value}/`;
|
|
805
|
+
}
|
|
806
|
+
function rewriteLocalhostUrl(input) {
|
|
807
|
+
if (!input) {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const url = new URL(input);
|
|
812
|
+
if (isLocalhostHost(url.hostname)) {
|
|
813
|
+
url.hostname = LOCAL_DEV_URL_HOST;
|
|
814
|
+
return url.toString();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
return input;
|
|
819
|
+
}
|
|
820
|
+
return input;
|
|
821
|
+
}
|
|
822
|
+
function isLocalhostHost(hostname) {
|
|
823
|
+
return hostname === 'localhost' || hostname === '::1' || hostname === '[::1]';
|
|
824
|
+
}
|
|
825
|
+
async function withManagedDevOutputEnv(action, env = process.env) {
|
|
826
|
+
const previous = env[MANAGED_DEV_OUTPUT_ENV];
|
|
827
|
+
env[MANAGED_DEV_OUTPUT_ENV] = '1';
|
|
828
|
+
try {
|
|
829
|
+
return await action();
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
if (previous === undefined) {
|
|
833
|
+
delete env[MANAGED_DEV_OUTPUT_ENV];
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
env[MANAGED_DEV_OUTPUT_ENV] = previous;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function installShutdownHandlers(closers, processLike) {
|
|
841
|
+
const shutdown = async () => {
|
|
842
|
+
await closeAll(closers);
|
|
843
|
+
processLike.exit(0);
|
|
844
|
+
};
|
|
845
|
+
processLike.once('SIGINT', () => {
|
|
846
|
+
void shutdown();
|
|
847
|
+
});
|
|
848
|
+
processLike.once('SIGTERM', () => {
|
|
849
|
+
void shutdown();
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
async function closeAll(closers) {
|
|
853
|
+
await Promise.allSettled(closers.map((close) => close()));
|
|
854
|
+
}
|
|
855
|
+
function createLanAddressCandidates(options) {
|
|
856
|
+
const viteNetworkUrlByHost = new Map(options.viteNetworkUrls
|
|
857
|
+
.map((url) => [readUrlHost(url), url])
|
|
858
|
+
.filter((entry) => Boolean(entry[0])));
|
|
859
|
+
const candidates = Object.entries(options.interfaces)
|
|
860
|
+
.flatMap(([name, infos]) => (infos ?? []).map((info) => ({
|
|
861
|
+
info,
|
|
862
|
+
name,
|
|
863
|
+
})))
|
|
864
|
+
.filter(({ info }) => info.family === 'IPv4' && !info.internal && isPrivateIPv4(info.address))
|
|
865
|
+
.map(({ info, name }) => {
|
|
866
|
+
const appUrl = viteNetworkUrlByHost.get(info.address) ?? replaceUrlHost(options.appUrl, info.address);
|
|
867
|
+
if (!appUrl) {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
const candidate = {
|
|
871
|
+
address: info.address,
|
|
872
|
+
appUrl,
|
|
873
|
+
id: `${name}-${info.address}`,
|
|
874
|
+
name,
|
|
875
|
+
};
|
|
876
|
+
const runtimeUrl = rewriteLoopbackUrlHost(options.runtimeUrl, info.address);
|
|
877
|
+
if (runtimeUrl !== undefined) {
|
|
878
|
+
candidate.mobileAppQrPayload = createMobileAppQrPayload(appUrl, { runtimeUrl });
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
candidate.mobileAppQrPayload = createMobileAppQrPayload(appUrl);
|
|
882
|
+
}
|
|
883
|
+
return candidate;
|
|
884
|
+
})
|
|
885
|
+
.filter((candidate) => Boolean(candidate));
|
|
886
|
+
return candidates.sort((left, right) => {
|
|
887
|
+
const leftNetworkMatch = viteNetworkUrlByHost.has(left.address) ? 0 : 1;
|
|
888
|
+
const rightNetworkMatch = viteNetworkUrlByHost.has(right.address) ? 0 : 1;
|
|
889
|
+
if (leftNetworkMatch !== rightNetworkMatch) {
|
|
890
|
+
return leftNetworkMatch - rightNetworkMatch;
|
|
891
|
+
}
|
|
892
|
+
return readInterfacePriority(left.name) - readInterfacePriority(right.name) || left.name.localeCompare(right.name);
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function rewriteLoopbackUrlHost(input, host) {
|
|
896
|
+
if (input === undefined || input === '') {
|
|
897
|
+
return undefined;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const url = new URL(input);
|
|
901
|
+
if (isLoopbackHost(url.hostname)) {
|
|
902
|
+
url.hostname = host;
|
|
903
|
+
return url.toString();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
return input;
|
|
908
|
+
}
|
|
909
|
+
return input;
|
|
910
|
+
}
|
|
911
|
+
function isLoopbackHost(hostname) {
|
|
912
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
|
|
913
|
+
}
|
|
914
|
+
function readUrlHost(input) {
|
|
915
|
+
try {
|
|
916
|
+
return new URL(input).hostname;
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function replaceUrlHost(input, host) {
|
|
923
|
+
try {
|
|
924
|
+
const url = new URL(input);
|
|
925
|
+
url.hostname = host;
|
|
926
|
+
return url.toString();
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function readInterfacePriority(name) {
|
|
933
|
+
const normalized = name.toLowerCase();
|
|
934
|
+
if (normalized.includes('wi-fi') || normalized.includes('wifi') || normalized.includes('wlan')) {
|
|
935
|
+
return 0;
|
|
936
|
+
}
|
|
937
|
+
if (normalized === 'en0') {
|
|
938
|
+
return 1;
|
|
939
|
+
}
|
|
940
|
+
if (normalized.startsWith('en') || normalized.includes('ethernet')) {
|
|
941
|
+
return 2;
|
|
942
|
+
}
|
|
943
|
+
return 3;
|
|
944
|
+
}
|
|
945
|
+
function isPrivateIPv4(address) {
|
|
946
|
+
const parts = address.split('.').map((part) => Number(part));
|
|
947
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
return (parts[0] === 10 ||
|
|
951
|
+
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
|
|
952
|
+
(parts[0] === 192 && parts[1] === 168));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
exports.runDevCommand = runDevCommand;
|