@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.
- package/README.md +438 -0
- package/README.zh-CN.md +554 -0
- package/assets/container/assets/index.css +1 -0
- package/assets/container/assets/index.js +137 -0
- package/assets/container/assets/pageFrame.css +1 -0
- package/assets/container/assets/pageFrame.js +56 -0
- package/assets/container/assets/service.js +6 -0
- package/assets/container/assets/vconsole.js +931 -0
- package/assets/container/favicon.ico +0 -0
- package/assets/container/images/icon-arrow.png +0 -0
- package/assets/container/images/mini-action-white.png +0 -0
- package/assets/container/images/mini-action.png +0 -0
- package/assets/container/images/mini-arrow-left-white.png +0 -0
- package/assets/container/images/mini-arrow-left.jpg +0 -0
- package/assets/container/images/mini-arrow-left.png +0 -0
- package/assets/container/images/mini-close-white.png +0 -0
- package/assets/container/images/mini-close.png +0 -0
- package/assets/container/images/more.png +0 -0
- package/assets/container/images/search.jpg +0 -0
- package/assets/container/index.html +1 -0
- package/assets/container/pageFrame.html +1 -0
- package/assets/init/tabbar/home-active.png +0 -0
- package/assets/init/tabbar/home.png +0 -0
- package/assets/init/tabbar/list-active.png +0 -0
- package/assets/init/tabbar/list.png +0 -0
- package/assets/init/types/neux-api.d.ts +1402 -0
- package/package.json +68 -0
- package/scripts/generate-init-types.js +210 -0
- package/scripts/sync-compiler.js +22 -0
- package/scripts/sync-web-container.js +34 -0
- package/src/bin/cli.js +646 -0
- package/src/core/brand.js +11 -0
- package/src/core/compiler.js +305 -0
- package/src/core/defaults.js +12 -0
- package/src/core/dev.js +15 -0
- package/src/core/errors.js +17 -0
- package/src/core/fs.js +66 -0
- package/src/core/i18n.js +37 -0
- package/src/core/init.js +866 -0
- package/src/core/lifecycle.js +32 -0
- package/src/core/manifest.js +72 -0
- package/src/core/pack.js +156 -0
- package/src/core/package-info.js +27 -0
- package/src/core/preview-server.js +123 -0
- package/src/core/project.js +101 -0
- package/src/core/prompts.js +71 -0
- package/src/core/proxy-security.js +141 -0
- package/src/core/proxy.js +156 -0
- package/src/core/qr.js +41 -0
- package/src/core/terminal-qr.js +72 -0
- package/src/core/update.js +278 -0
- package/src/core/watch.js +113 -0
- package/src/core/web.js +649 -0
- package/src/core/zip.js +120 -0
- package/src/index.js +20 -0
- package/src/providers/service-client.js +149 -0
- package/src/providers/service-config.js +264 -0
- package/src/providers/service.js +146 -0
- package/src/providers/upload.js +112 -0
- package/vendor/dimina-compiler/bin/index.cjs +265 -0
- package/vendor/dimina-compiler/bin/index.js +263 -0
- package/vendor/dimina-compiler/compatibility-B-DoZtUX.cjs +395 -0
- package/vendor/dimina-compiler/compatibility-Cl3-DO6V.js +366 -0
- package/vendor/dimina-compiler/core/logic-compiler.cjs +378 -0
- package/vendor/dimina-compiler/core/logic-compiler.js +374 -0
- package/vendor/dimina-compiler/core/style-compiler.cjs +392 -0
- package/vendor/dimina-compiler/core/style-compiler.js +377 -0
- package/vendor/dimina-compiler/core/view-compiler.cjs +1601 -0
- package/vendor/dimina-compiler/core/view-compiler.js +1582 -0
- package/vendor/dimina-compiler/index.cjs +762 -0
- package/vendor/dimina-compiler/index.js +751 -0
- package/vendor/dimina-compiler/sourcemap-BgtIgqkC.cjs +1377 -0
- package/vendor/dimina-compiler/sourcemap-CKjhV9h7.js +1135 -0
package/src/core/web.js
ADDED
|
@@ -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
|
+
}
|