@neuxnet/neux-cli 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +438 -0
  2. package/README.zh-CN.md +554 -0
  3. package/assets/container/assets/index.css +1 -0
  4. package/assets/container/assets/index.js +137 -0
  5. package/assets/container/assets/pageFrame.css +1 -0
  6. package/assets/container/assets/pageFrame.js +56 -0
  7. package/assets/container/assets/service.js +6 -0
  8. package/assets/container/assets/vconsole.js +931 -0
  9. package/assets/container/favicon.ico +0 -0
  10. package/assets/container/images/icon-arrow.png +0 -0
  11. package/assets/container/images/mini-action-white.png +0 -0
  12. package/assets/container/images/mini-action.png +0 -0
  13. package/assets/container/images/mini-arrow-left-white.png +0 -0
  14. package/assets/container/images/mini-arrow-left.jpg +0 -0
  15. package/assets/container/images/mini-arrow-left.png +0 -0
  16. package/assets/container/images/mini-close-white.png +0 -0
  17. package/assets/container/images/mini-close.png +0 -0
  18. package/assets/container/images/more.png +0 -0
  19. package/assets/container/images/search.jpg +0 -0
  20. package/assets/container/index.html +1 -0
  21. package/assets/container/pageFrame.html +1 -0
  22. package/assets/init/tabbar/home-active.png +0 -0
  23. package/assets/init/tabbar/home.png +0 -0
  24. package/assets/init/tabbar/list-active.png +0 -0
  25. package/assets/init/tabbar/list.png +0 -0
  26. package/assets/init/types/neux-api.d.ts +1402 -0
  27. package/package.json +68 -0
  28. package/scripts/generate-init-types.js +210 -0
  29. package/scripts/sync-compiler.js +22 -0
  30. package/scripts/sync-web-container.js +34 -0
  31. package/src/bin/cli.js +646 -0
  32. package/src/core/brand.js +11 -0
  33. package/src/core/compiler.js +305 -0
  34. package/src/core/defaults.js +12 -0
  35. package/src/core/dev.js +15 -0
  36. package/src/core/errors.js +17 -0
  37. package/src/core/fs.js +66 -0
  38. package/src/core/i18n.js +37 -0
  39. package/src/core/init.js +866 -0
  40. package/src/core/lifecycle.js +32 -0
  41. package/src/core/manifest.js +72 -0
  42. package/src/core/pack.js +156 -0
  43. package/src/core/package-info.js +27 -0
  44. package/src/core/preview-server.js +123 -0
  45. package/src/core/project.js +101 -0
  46. package/src/core/prompts.js +71 -0
  47. package/src/core/proxy-security.js +141 -0
  48. package/src/core/proxy.js +156 -0
  49. package/src/core/qr.js +41 -0
  50. package/src/core/terminal-qr.js +72 -0
  51. package/src/core/update.js +278 -0
  52. package/src/core/watch.js +113 -0
  53. package/src/core/web.js +649 -0
  54. package/src/core/zip.js +120 -0
  55. package/src/index.js +20 -0
  56. package/src/providers/service-client.js +149 -0
  57. package/src/providers/service-config.js +264 -0
  58. package/src/providers/service.js +146 -0
  59. package/src/providers/upload.js +112 -0
  60. package/vendor/dimina-compiler/bin/index.cjs +265 -0
  61. package/vendor/dimina-compiler/bin/index.js +263 -0
  62. package/vendor/dimina-compiler/compatibility-B-DoZtUX.cjs +395 -0
  63. package/vendor/dimina-compiler/compatibility-Cl3-DO6V.js +366 -0
  64. package/vendor/dimina-compiler/core/logic-compiler.cjs +378 -0
  65. package/vendor/dimina-compiler/core/logic-compiler.js +374 -0
  66. package/vendor/dimina-compiler/core/style-compiler.cjs +392 -0
  67. package/vendor/dimina-compiler/core/style-compiler.js +377 -0
  68. package/vendor/dimina-compiler/core/view-compiler.cjs +1601 -0
  69. package/vendor/dimina-compiler/core/view-compiler.js +1582 -0
  70. package/vendor/dimina-compiler/index.cjs +762 -0
  71. package/vendor/dimina-compiler/index.js +751 -0
  72. package/vendor/dimina-compiler/sourcemap-BgtIgqkC.cjs +1377 -0
  73. package/vendor/dimina-compiler/sourcemap-CKjhV9h7.js +1135 -0
@@ -0,0 +1,649 @@
1
+ import fs from 'node:fs'
2
+ import fsp from 'node:fs/promises'
3
+ import http from 'node:http'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { spawn } from 'node:child_process'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { DiminaCliError } from './errors.js'
9
+ import { buildProject, buildLogicOnlyProject, buildStyleOnlyProject, buildViewOnlyProject } from './compiler.js'
10
+ import { inspectProject } from './project.js'
11
+ import { resolvePath, writeJson } from './fs.js'
12
+ import { createProjectWatcher } from './watch.js'
13
+ import { defaultOut } from './defaults.js'
14
+ import { emitLifecycleEvent } from './lifecycle.js'
15
+ import { createProxyHandler } from './proxy.js'
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const bundledContainerDir = path.resolve(__dirname, '../../assets/container')
19
+ const MAX_PORT_RETRY = 20
20
+ const DEFAULT_WEB_SERVER_URL = 'https://demo-c.paas.superapp.neuvision.cn'
21
+ const STYLE_HOT_UPDATE_EXTENSIONS = new Set([
22
+ '.wxss',
23
+ '.nxss',
24
+ '.ddss',
25
+ '.acss',
26
+ '.css',
27
+ '.less',
28
+ '.scss',
29
+ '.sass',
30
+ ])
31
+ const VIEW_HOT_UPDATE_EXTENSIONS = new Set([
32
+ '.wxml',
33
+ '.nxml',
34
+ '.ddml',
35
+ '.axml',
36
+ ])
37
+ const LOGIC_HOT_UPDATE_EXTENSIONS = new Set([
38
+ '.js',
39
+ '.ts',
40
+ ])
41
+ const WATCH_LOCK_DIR = 'neux-dev-locks'
42
+
43
+ function contentType(filePath) {
44
+ const ext = path.extname(filePath)
45
+ if (ext === '.html') return 'text/html; charset=utf-8'
46
+ if (ext === '.json') return 'application/json; charset=utf-8'
47
+ if (ext === '.js') return 'text/javascript; charset=utf-8'
48
+ if (ext === '.css') return 'text/css; charset=utf-8'
49
+ if (ext === '.png') return 'image/png'
50
+ if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg'
51
+ if (ext === '.svg') return 'image/svg+xml'
52
+ return 'application/octet-stream'
53
+ }
54
+
55
+ function candidateContainerDirs() {
56
+ return [
57
+ process.env.DIMINA_WEB_CONTAINER_DIR,
58
+ bundledContainerDir,
59
+ path.resolve(process.cwd(), 'fe/packages/container/dist'),
60
+ path.resolve(process.cwd(), '../fe/packages/container/dist'),
61
+ path.resolve(process.cwd(), 'packages/container/dist'),
62
+ ].filter(Boolean)
63
+ }
64
+
65
+ async function findDefaultContainerDir() {
66
+ for (const dir of candidateContainerDirs()) {
67
+ if (fs.existsSync(path.join(dir, 'index.html'))) return dir
68
+ }
69
+ throw new DiminaCliError(
70
+ 'DIMINA_WEB_CONTAINER_NOT_FOUND',
71
+ 'Dimina web container dist not found. Run `npm run sync:container` in the CLI package, run `pnpm --dir fe --filter @dimina/container build`, or pass --container-dir.',
72
+ )
73
+ }
74
+
75
+ function openBrowser(url) {
76
+ const command = process.platform === 'darwin'
77
+ ? 'open'
78
+ : process.platform === 'win32'
79
+ ? 'cmd'
80
+ : 'xdg-open'
81
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
82
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' })
83
+ child.unref()
84
+ }
85
+
86
+ function resolveRequest(root, requestPath) {
87
+ const resolved = path.resolve(path.join(root, path.normalize(requestPath)))
88
+ if (resolved === root || resolved.startsWith(root + path.sep)) return resolved
89
+ return null
90
+ }
91
+
92
+ function sendFile(res, filePath, headers = {}) {
93
+ fs.stat(filePath, (error, stat) => {
94
+ if (error || !stat.isFile()) {
95
+ res.writeHead(404)
96
+ res.end('Not found')
97
+ return
98
+ }
99
+ res.writeHead(200, { 'Content-Type': contentType(filePath), ...headers })
100
+ fs.createReadStream(filePath).pipe(res)
101
+ })
102
+ }
103
+
104
+ async function acquireWatchLock(outputDir) {
105
+ // Keep the lock outside the project and compiler output trees.
106
+ const lockName = `${Buffer.from(path.resolve(outputDir)).toString('base64url')}.lock`
107
+ const lockPath = path.join(os.tmpdir(), WATCH_LOCK_DIR, lockName)
108
+ await fsp.mkdir(path.dirname(lockPath), { recursive: true })
109
+
110
+ for (let attempt = 0; attempt < 2; attempt++) {
111
+ try {
112
+ const handle = await fsp.open(lockPath, 'wx')
113
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }))
114
+ await handle.close()
115
+ return async () => {
116
+ try {
117
+ await fsp.unlink(lockPath)
118
+ }
119
+ catch (error) {
120
+ if (error.code !== 'ENOENT') throw error
121
+ }
122
+ }
123
+ }
124
+ catch (error) {
125
+ if (error.code !== 'EEXIST') throw error
126
+ let lock = null
127
+ try {
128
+ lock = JSON.parse(await fsp.readFile(lockPath, 'utf8'))
129
+ }
130
+ catch {
131
+ // A partially written lock can be removed and recreated once.
132
+ }
133
+ const pid = Number(lock?.pid)
134
+ let running = false
135
+ if (Number.isInteger(pid) && pid > 0) {
136
+ try {
137
+ process.kill(pid, 0)
138
+ running = true
139
+ }
140
+ catch (probeError) {
141
+ running = probeError.code !== 'ESRCH'
142
+ }
143
+ }
144
+ if (running) {
145
+ throw new DiminaCliError(
146
+ 'DIMINA_DEV_ALREADY_RUNNING',
147
+ `Another dev server is already using ${outputDir}. Stop it before starting a second watcher.`,
148
+ { outputDir, pid },
149
+ )
150
+ }
151
+ await fsp.unlink(lockPath).catch(unlinkError => {
152
+ if (unlinkError.code !== 'ENOENT') throw unlinkError
153
+ })
154
+ }
155
+ }
156
+
157
+ throw new DiminaCliError('DIMINA_DEV_LOCK_FAILED', `Unable to lock dev output directory: ${outputDir}`)
158
+ }
159
+
160
+ export function classifyHotUpdate(event = {}) {
161
+ const ext = path.extname(event.path || '').toLowerCase()
162
+ // .nxs 可能影响多个页面/组件的模板脚本依赖,当前没有可靠的增量影响面分析,保持整页 reload。
163
+ if (ext === '.nxs') return 'reload'
164
+ if (VIEW_HOT_UPDATE_EXTENSIONS.has(ext)) return 'view-update'
165
+ if (LOGIC_HOT_UPDATE_EXTENSIONS.has(ext)) return 'logic-update'
166
+ return STYLE_HOT_UPDATE_EXTENSIONS.has(ext) ? 'style-update' : 'reload'
167
+ }
168
+
169
+ function getHotUpdateTypes(event = {}) {
170
+ const events = Array.isArray(event.events) && event.events.length ? event.events : [event]
171
+ const types = [...new Set(events.map(item => classifyHotUpdate(item)))]
172
+ if (types.length === 0) return ['reload']
173
+ if (types.includes('reload')) return ['reload']
174
+ return ['style-update', 'view-update', 'logic-update'].filter(type => types.includes(type))
175
+ }
176
+
177
+ function getEventForHotUpdateType(event = {}, type) {
178
+ const events = Array.isArray(event.events) && event.events.length ? event.events : [event]
179
+ return [...events].reverse().find(item => classifyHotUpdate(item) === type) || event
180
+ }
181
+
182
+ export function createHotUpdatePayloads(event = {}, buildResults = []) {
183
+ return buildResults.map(({ updateType, buildResult, appInfo, event: typedEvent }) => ({
184
+ type: ['style-update', 'view-update', 'logic-update'].includes(updateType) ? updateType : 'reload',
185
+ changedPath: typedEvent?.path || event?.path,
186
+ appId: appInfo?.appId,
187
+ updatedPages: buildResult?.updatedPages,
188
+ }))
189
+ }
190
+
191
+ export function liveReloadScript() {
192
+ return `<script>
193
+ (() => {
194
+ const source = new EventSource('/__dimina_live_reload');
195
+ const refreshStylesheets = (doc, timestamp) => {
196
+ if (!doc) return;
197
+ for (const link of doc.querySelectorAll('link[rel="stylesheet"][href]')) {
198
+ const url = new URL(link.href, doc.baseURI);
199
+ url.searchParams.set('__dimina_hmr', timestamp);
200
+ link.href = url.toString();
201
+ }
202
+ };
203
+ const refreshAllStylesheets = () => {
204
+ const timestamp = String(Date.now());
205
+ refreshStylesheets(document, timestamp);
206
+ for (const frame of document.querySelectorAll('iframe')) {
207
+ try {
208
+ refreshStylesheets(frame.contentDocument, timestamp);
209
+ } catch {}
210
+ }
211
+ };
212
+ const applyHotUpdate = (handler, payload) => {
213
+ try {
214
+ if (typeof handler !== 'function') {
215
+ window.location.reload();
216
+ return;
217
+ }
218
+ const result = handler(payload);
219
+ if (result && typeof result.then === 'function') {
220
+ result.then((handled) => {
221
+ if (handled === false || handled == null) window.location.reload();
222
+ }).catch(() => window.location.reload());
223
+ return;
224
+ }
225
+ if (result === false || result == null) {
226
+ window.location.reload();
227
+ }
228
+ } catch {
229
+ window.location.reload();
230
+ }
231
+ };
232
+ source.addEventListener('style-update', refreshAllStylesheets);
233
+ source.addEventListener('view-update', (event) => {
234
+ const payload = JSON.parse(event.data || '{}');
235
+ applyHotUpdate(window.__diminaHotUpdateView, payload);
236
+ });
237
+ source.addEventListener('logic-update', (event) => {
238
+ const payload = JSON.parse(event.data || '{}');
239
+ applyHotUpdate(window.__diminaHotUpdateLogic, payload);
240
+ });
241
+ source.addEventListener('reload', () => location.reload());
242
+ })();
243
+ </script>`
244
+ }
245
+
246
+ async function sendIndex(res, filePath, liveReload) {
247
+ if (!liveReload) return sendFile(res, filePath)
248
+ try {
249
+ const html = await fsp.readFile(filePath, 'utf8')
250
+ const body = html.includes('</body>')
251
+ ? html.replace('</body>', `${liveReloadScript()}</body>`)
252
+ : `${html}${liveReloadScript()}`
253
+ res.writeHead(200, {
254
+ 'Content-Type': 'text/html; charset=utf-8',
255
+ 'Cache-Control': 'no-store',
256
+ })
257
+ res.end(body)
258
+ }
259
+ catch {
260
+ res.writeHead(404)
261
+ res.end('Not found')
262
+ }
263
+ }
264
+
265
+ export async function startWebServer({
266
+ containerDir,
267
+ outputDir,
268
+ host,
269
+ port,
270
+ liveReload = false,
271
+ proxy = true,
272
+ proxyAllowedOrigins,
273
+ }) {
274
+ const containerRoot = path.resolve(containerDir)
275
+ const outputRoot = path.resolve(outputDir)
276
+ const clients = new Set()
277
+ const proxyHandler = proxy ? createProxyHandler({ allowedOrigins: proxyAllowedOrigins }) : null
278
+ const server = http.createServer(async (req, res) => {
279
+ let pathname
280
+ try {
281
+ pathname = decodeURIComponent(new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`).pathname)
282
+ }
283
+ catch {
284
+ res.writeHead(400)
285
+ res.end('Bad request')
286
+ return
287
+ }
288
+
289
+ if (pathname === '/proxy') {
290
+ if (!proxyHandler) {
291
+ res.writeHead(404)
292
+ res.end('Not Found')
293
+ return
294
+ }
295
+ await proxyHandler(req, res)
296
+ return
297
+ }
298
+
299
+ if (liveReload && pathname === '/__dimina_live_reload') {
300
+ res.writeHead(200, {
301
+ 'Content-Type': 'text/event-stream; charset=utf-8',
302
+ 'Cache-Control': 'no-cache, no-transform',
303
+ Connection: 'keep-alive',
304
+ })
305
+ res.write('\n')
306
+ clients.add(res)
307
+ req.on('close', () => clients.delete(res))
308
+ return
309
+ }
310
+
311
+ const outputFile = resolveRequest(outputRoot, pathname)
312
+ if (outputFile && fs.existsSync(outputFile) && fs.statSync(outputFile).isFile()) {
313
+ sendFile(res, outputFile, liveReload ? { 'Cache-Control': 'no-store' } : {})
314
+ return
315
+ }
316
+
317
+ const containerFile = resolveRequest(containerRoot, pathname)
318
+ if (containerFile && fs.existsSync(containerFile) && fs.statSync(containerFile).isFile()) {
319
+ sendFile(res, containerFile, liveReload ? { 'Cache-Control': 'no-store' } : {})
320
+ return
321
+ }
322
+
323
+ const ext = path.extname(pathname).toLowerCase()
324
+ if (ext && ext !== '.html') {
325
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' })
326
+ res.end('Not Found')
327
+ return
328
+ }
329
+
330
+ sendIndex(res, path.join(containerRoot, 'index.html'), liveReload)
331
+ })
332
+
333
+ server.reload = (payload = {}) => {
334
+ const type = ['style-update', 'view-update', 'logic-update'].includes(payload.type) ? payload.type : 'reload'
335
+ const data = JSON.stringify({
336
+ ...payload,
337
+ type,
338
+ })
339
+ for (const client of clients) client.write(`event: ${type}\ndata: ${data}\n\n`)
340
+ }
341
+
342
+ try {
343
+ await listenWithPortFallback(server, host, port)
344
+ }
345
+ catch (error) {
346
+ throw new DiminaCliError('DIMINA_WEB_SERVER_FAILED', `Failed to start web server: ${error.message}`, {
347
+ host,
348
+ port,
349
+ cause: error.code,
350
+ attemptedPorts: error.attemptedPorts,
351
+ })
352
+ }
353
+
354
+ return server
355
+ }
356
+
357
+ async function listenOnce(server, host, port) {
358
+ await new Promise((resolve, reject) => {
359
+ server.once('error', reject)
360
+ server.listen(port, host, () => {
361
+ server.off('error', reject)
362
+ resolve()
363
+ })
364
+ })
365
+ }
366
+
367
+ async function listenWithPortFallback(server, host, initialPort) {
368
+ const port = Number.parseInt(initialPort, 10)
369
+ const startPort = Number.isFinite(port) ? port : 7788
370
+ if (startPort === 0) {
371
+ await listenOnce(server, host, 0)
372
+ return
373
+ }
374
+
375
+ const attemptedPorts = []
376
+ for (let offset = 0; offset <= MAX_PORT_RETRY; offset++) {
377
+ const nextPort = startPort + offset
378
+ attemptedPorts.push(nextPort)
379
+ try {
380
+ await listenOnce(server, host, nextPort)
381
+ return
382
+ }
383
+ catch (error) {
384
+ if (error.code !== 'EADDRINUSE' || offset === MAX_PORT_RETRY) {
385
+ error.attemptedPorts = attemptedPorts
386
+ throw error
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ async function buildWebProject(options, outputDir, projectInfo) {
393
+ const runBuild = options.withBuildOutputRedirect || (action => action())
394
+ const buildResult = await runBuild(() => buildProject({
395
+ ...options,
396
+ out: outputDir,
397
+ }))
398
+ const appInfo = buildResult.compilerResult || {
399
+ appId: projectInfo.appId,
400
+ name: projectInfo.name,
401
+ path: projectInfo.entryPath,
402
+ }
403
+
404
+ await fsp.mkdir(outputDir, { recursive: true })
405
+ await writeJson(path.join(outputDir, 'appList.json'), [appInfo])
406
+
407
+ return { buildResult, appInfo }
408
+ }
409
+
410
+ async function buildWebStyleOnlyProject(options, outputDir, appInfo, event) {
411
+ const runBuild = options.withBuildOutputRedirect || (action => action())
412
+ const buildResult = await runBuild(() => buildStyleOnlyProject({
413
+ ...options,
414
+ out: outputDir,
415
+ changedPath: event?.path,
416
+ }))
417
+
418
+ return {
419
+ buildResult,
420
+ appInfo,
421
+ }
422
+ }
423
+
424
+ async function buildWebViewOnlyProject(options, outputDir, appInfo, event) {
425
+ const runBuild = options.withBuildOutputRedirect || (action => action())
426
+ const buildResult = await runBuild(() => buildViewOnlyProject({
427
+ ...options,
428
+ out: outputDir,
429
+ changedPath: event?.path,
430
+ }))
431
+
432
+ return {
433
+ buildResult,
434
+ appInfo,
435
+ }
436
+ }
437
+
438
+ async function buildWebLogicOnlyProject(options, outputDir, appInfo, event) {
439
+ const runBuild = options.withBuildOutputRedirect || (action => action())
440
+ const buildResult = await runBuild(() => buildLogicOnlyProject({
441
+ ...options,
442
+ out: outputDir,
443
+ changedPath: event?.path,
444
+ }))
445
+
446
+ return {
447
+ buildResult,
448
+ appInfo,
449
+ }
450
+ }
451
+
452
+ export async function startWeb(options = {}) {
453
+ const command = options.command || 'web'
454
+ const projectInfo = await inspectProject(options)
455
+ const outputDir = resolvePath(
456
+ options.out || options.output || defaultOut(command),
457
+ )
458
+ const containerDir = options.containerDir
459
+ ? resolvePath(options.containerDir)
460
+ : await findDefaultContainerDir()
461
+ const host = options.host || '127.0.0.1'
462
+ const port = Number.parseInt(options.port ?? 7788, 10)
463
+ const releaseWatchLock = options.watch ? await acquireWatchLock(outputDir) : null
464
+ await emitLifecycleEvent(options, 'build-start', { command, projectPath: projectInfo.projectPath, outputDir })
465
+ let buildResult
466
+ let appInfo
467
+ try {
468
+ const initial = await buildWebProject(options, outputDir, projectInfo)
469
+ buildResult = initial.buildResult
470
+ appInfo = initial.appInfo
471
+ await emitLifecycleEvent(options, 'build-success', { command, buildResult, appInfo, outputDir })
472
+ }
473
+ catch (error) {
474
+ await releaseWatchLock?.()
475
+ await emitLifecycleEvent(options, 'build-error', { command, error, outputDir })
476
+ throw error
477
+ }
478
+
479
+ let server
480
+ try {
481
+ server = await startWebServer({
482
+ containerDir,
483
+ outputDir,
484
+ host,
485
+ port,
486
+ liveReload: !!options.watch,
487
+ proxy: options.proxy !== false,
488
+ proxyAllowedOrigins: options.proxyAllowedOrigins,
489
+ })
490
+ }
491
+ catch (error) {
492
+ await releaseWatchLock?.()
493
+ throw error
494
+ }
495
+ const address = server.address()
496
+ const resolvedPort = typeof address === 'object' && address ? address.port : port
497
+ const serverUrl = `http://${host}:${resolvedPort}`
498
+ const entry = appInfo.path || projectInfo.entryPath
499
+ const documentationServerUrl = projectInfo.neuxCli?.serverUrl || DEFAULT_WEB_SERVER_URL
500
+ const locale = options.lang || process.env.NEUX_CLI_LANG || 'en'
501
+ const url = `${serverUrl}/?appId=${encodeURIComponent(appInfo.appId)}&entry=${encodeURIComponent(entry)}&page=${encodeURIComponent(entry)}&neuxCliServerUrl=${encodeURIComponent(documentationServerUrl)}&lang=${encodeURIComponent(locale)}`
502
+
503
+ if (options.open !== false) {
504
+ openBrowser(url)
505
+ }
506
+
507
+ let watcher = null
508
+ let rebuilding = false
509
+ let pendingRebuildEvent = null
510
+
511
+ async function rebuild(event) {
512
+ if (rebuilding) {
513
+ pendingRebuildEvent = event
514
+ return
515
+ }
516
+ rebuilding = true
517
+ try {
518
+ await options.onBuildStart?.({ event, outputDir })
519
+ await emitLifecycleEvent(options, 'build-start', { command, changedPath: event?.path, event, outputDir })
520
+ const reloadPayloads = []
521
+ const updateTypes = getHotUpdateTypes(event)
522
+ let lastUpdateType = updateTypes[updateTypes.length - 1] || 'reload'
523
+ for (const requestedType of updateTypes) {
524
+ let updateType = requestedType
525
+ const typedEvent = getEventForHotUpdateType(event, updateType)
526
+ let next
527
+ if (updateType === 'style-update') {
528
+ try {
529
+ next = await buildWebStyleOnlyProject(options, outputDir, appInfo, typedEvent)
530
+ }
531
+ catch {
532
+ updateType = 'reload'
533
+ next = await buildWebProject(options, outputDir, projectInfo)
534
+ }
535
+ }
536
+ else if (updateType === 'view-update') {
537
+ try {
538
+ next = await buildWebViewOnlyProject(options, outputDir, appInfo, typedEvent)
539
+ }
540
+ catch {
541
+ updateType = 'reload'
542
+ next = await buildWebProject(options, outputDir, projectInfo)
543
+ }
544
+ }
545
+ else if (updateType === 'logic-update') {
546
+ try {
547
+ next = await buildWebLogicOnlyProject(options, outputDir, appInfo, typedEvent)
548
+ }
549
+ catch {
550
+ updateType = 'reload'
551
+ next = await buildWebProject(options, outputDir, projectInfo)
552
+ }
553
+ }
554
+ else {
555
+ next = await buildWebProject(options, outputDir, projectInfo)
556
+ }
557
+
558
+ buildResult = next.buildResult
559
+ appInfo = next.appInfo
560
+ lastUpdateType = updateType
561
+ reloadPayloads.push(...createHotUpdatePayloads(event, [{
562
+ updateType,
563
+ buildResult,
564
+ appInfo,
565
+ event: typedEvent,
566
+ }]))
567
+ if (updateType === 'reload') {
568
+ break
569
+ }
570
+ }
571
+ await options.onRebuild?.({ event, buildResult, appInfo, outputDir })
572
+ await emitLifecycleEvent(options, 'build-success', { command, changedPath: event?.path, event, buildResult, appInfo, outputDir })
573
+ for (const payload of reloadPayloads) server.reload?.(payload)
574
+ await options.onReload?.({ event, buildResult, appInfo, outputDir })
575
+ await emitLifecycleEvent(options, 'reload', { command, changedPath: event?.path, event, buildResult, appInfo, updateType: lastUpdateType, outputDir })
576
+ }
577
+ catch (error) {
578
+ await options.onBuildError?.(error)
579
+ await emitLifecycleEvent(options, 'build-error', { command, changedPath: event?.path, event, error, outputDir })
580
+ }
581
+ finally {
582
+ rebuilding = false
583
+ if (pendingRebuildEvent) {
584
+ const nextEvent = pendingRebuildEvent
585
+ pendingRebuildEvent = null
586
+ await rebuild(nextEvent)
587
+ }
588
+ }
589
+ }
590
+
591
+ if (options.watch) {
592
+ try {
593
+ watcher = await createProjectWatcher({
594
+ project: projectInfo.projectPath,
595
+ debounceMs: options.debounceMs,
596
+ ignorePaths: [outputDir],
597
+ onChange: rebuild,
598
+ })
599
+ }
600
+ catch (error) {
601
+ try {
602
+ await new Promise((resolve, reject) => server.close(closeError => closeError ? reject(closeError) : resolve()))
603
+ }
604
+ finally {
605
+ await releaseWatchLock?.()
606
+ }
607
+ throw error
608
+ }
609
+ }
610
+
611
+ let closePromise
612
+ const session = {
613
+ ok: true,
614
+ command,
615
+ projectPath: projectInfo.projectPath,
616
+ outputDir,
617
+ containerDir,
618
+ appId: appInfo.appId,
619
+ entryPath: entry,
620
+ serverUrl,
621
+ url,
622
+ buildResult,
623
+ watch: !!options.watch,
624
+ close: async () => {
625
+ if (closePromise) return closePromise
626
+
627
+ closePromise = (async () => {
628
+ try {
629
+ await watcher?.close()
630
+ await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
631
+ }
632
+ finally {
633
+ await releaseWatchLock?.()
634
+ }
635
+ await emitLifecycleEvent(options, 'close', {
636
+ command,
637
+ projectPath: projectInfo.projectPath,
638
+ appId: appInfo.appId,
639
+ serverUrl,
640
+ url,
641
+ })
642
+ })()
643
+
644
+ return closePromise
645
+ },
646
+ }
647
+ await emitLifecycleEvent(options, 'ready', session)
648
+ return session
649
+ }