@celsian/vura-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/admin.d.ts +16 -0
- package/dist/commands/admin.js +61 -19
- package/dist/commands/build.d.ts +24 -0
- package/dist/commands/build.js +72 -66
- package/dist/commands/dev.js +30 -2
- package/package.json +7 -7
package/dist/commands/admin.d.ts
CHANGED
|
@@ -23,6 +23,22 @@ export declare function isAllowedAdminRequest(headers: {
|
|
|
23
23
|
host?: string | string[];
|
|
24
24
|
origin?: string | string[];
|
|
25
25
|
}, bindHost: string, port: number): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Start the admin server and return once it is listening.
|
|
28
|
+
*
|
|
29
|
+
* Split out of `adminCommand` so tests can drive a real server: the command
|
|
30
|
+
* itself never returns, and the bug this exists to pin lived in the wiring
|
|
31
|
+
* rather than in any function a unit test could reach. `vura admin --port 0`
|
|
32
|
+
* built its same-origin allowlist from the *requested* port, so the allowlist
|
|
33
|
+
* held `localhost:0` while the browser sent the real one and every API request
|
|
34
|
+
* was refused as cross-origin with a valid token in hand.
|
|
35
|
+
*/
|
|
36
|
+
export declare function startAdminServer(opts: AdminOptions): Promise<{
|
|
37
|
+
server: import('node:http').Server;
|
|
38
|
+
port: number;
|
|
39
|
+
token: string;
|
|
40
|
+
close: () => Promise<void>;
|
|
41
|
+
}>;
|
|
26
42
|
export declare function adminCommand(args: string[]): Promise<void>;
|
|
27
43
|
export declare function renderDashboardHtml(adminToken: string): string;
|
|
28
44
|
export {};
|
package/dist/commands/admin.js
CHANGED
|
@@ -60,8 +60,17 @@ export function isAllowedAdminRequest(headers, bindHost, port) {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Start the admin server and return once it is listening.
|
|
65
|
+
*
|
|
66
|
+
* Split out of `adminCommand` so tests can drive a real server: the command
|
|
67
|
+
* itself never returns, and the bug this exists to pin lived in the wiring
|
|
68
|
+
* rather than in any function a unit test could reach. `vura admin --port 0`
|
|
69
|
+
* built its same-origin allowlist from the *requested* port, so the allowlist
|
|
70
|
+
* held `localhost:0` while the browser sent the real one and every API request
|
|
71
|
+
* was refused as cross-origin with a valid token in hand.
|
|
72
|
+
*/
|
|
73
|
+
export async function startAdminServer(opts) {
|
|
65
74
|
assertSafeAdminBindHost(opts.host);
|
|
66
75
|
const { createServer } = await import('node:http');
|
|
67
76
|
const { randomBytes } = await import('node:crypto');
|
|
@@ -72,11 +81,17 @@ export async function adminCommand(args) {
|
|
|
72
81
|
const config = await loadConfig(opts.projectRoot);
|
|
73
82
|
const projectName = basename(opts.projectRoot);
|
|
74
83
|
const adminToken = randomBytes(32).toString('base64url');
|
|
84
|
+
// The port the server actually bound. `--port 0` means "pick a free port",
|
|
85
|
+
// and the same-origin allowlist below is built from it: with the *requested*
|
|
86
|
+
// port the allowlist held `localhost:0` while the browser sent the real one,
|
|
87
|
+
// so every request to a `--port 0` dashboard was refused as cross-origin.
|
|
88
|
+
// Assigned in the listen callback, before any request can arrive.
|
|
89
|
+
let boundPort = opts.port;
|
|
75
90
|
const server = createServer(async (req, res) => {
|
|
76
91
|
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
77
92
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
78
93
|
const isAdminApi = url.pathname.startsWith('/__admin/api/');
|
|
79
|
-
const sameOrigin = isAllowedAdminRequest(req.headers, opts.host,
|
|
94
|
+
const sameOrigin = isAllowedAdminRequest(req.headers, opts.host, boundPort);
|
|
80
95
|
const apiHeaders = adminApiHeaders();
|
|
81
96
|
if (method === 'OPTIONS') {
|
|
82
97
|
res.writeHead(sameOrigin ? 204 : 403, {
|
|
@@ -311,25 +326,52 @@ export async function adminCommand(args) {
|
|
|
311
326
|
res.writeHead(404, apiHeaders);
|
|
312
327
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
313
328
|
});
|
|
314
|
-
|
|
329
|
+
const onListening = () => {
|
|
315
330
|
const displayHost = opts.host === '127.0.0.1' ? 'localhost' : opts.host;
|
|
331
|
+
const addr = server.address();
|
|
332
|
+
boundPort = addr && typeof addr === 'object' ? addr.port : opts.port;
|
|
316
333
|
const displayUrl = displayHost.includes(':')
|
|
317
|
-
? `http://[${displayHost}]:${
|
|
318
|
-
: `http://${displayHost}:${
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
334
|
+
? `http://[${displayHost}]:${boundPort}`
|
|
335
|
+
: `http://${displayHost}:${boundPort}`;
|
|
336
|
+
// Every line is padded to one width. They used to be padded to four
|
|
337
|
+
// different ones (36, 39, 39 and 38 inside a 41-wide box), so the box only
|
|
338
|
+
// ever looked square by accident of the default port's length.
|
|
339
|
+
const W = 41;
|
|
340
|
+
const line = (text = '') => ` │${text.padEnd(W).slice(0, W)}│`;
|
|
341
|
+
const rule = (l, r) => ` ${l}${'─'.repeat(W)}${r}`;
|
|
342
|
+
const field = (label, value) => line(` ${label.padEnd(12)}${value}`);
|
|
343
|
+
console.log([
|
|
344
|
+
'',
|
|
345
|
+
rule('┌', '┐'),
|
|
346
|
+
line(),
|
|
347
|
+
line(' vura admin'),
|
|
348
|
+
line(),
|
|
349
|
+
field('Dashboard:', displayUrl),
|
|
350
|
+
field('Token:', `${adminToken.slice(0, 8)}...`),
|
|
351
|
+
field('Project:', projectName),
|
|
352
|
+
line(),
|
|
353
|
+
line(` ${manifest.api.length} API routes · ${manifest.pages.length} pages`),
|
|
354
|
+
line(),
|
|
355
|
+
rule('└', '┘'),
|
|
356
|
+
'',
|
|
357
|
+
].join('\n'));
|
|
358
|
+
};
|
|
359
|
+
await new Promise((resolve, reject) => {
|
|
360
|
+
server.once('error', reject);
|
|
361
|
+
server.listen(opts.port, opts.host, () => {
|
|
362
|
+
onListening();
|
|
363
|
+
resolve();
|
|
364
|
+
});
|
|
332
365
|
});
|
|
366
|
+
return {
|
|
367
|
+
server,
|
|
368
|
+
port: boundPort,
|
|
369
|
+
token: adminToken,
|
|
370
|
+
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
export async function adminCommand(args) {
|
|
374
|
+
await startAdminServer(parseAdminOptions(args));
|
|
333
375
|
await new Promise(() => { });
|
|
334
376
|
}
|
|
335
377
|
// ─── Dashboard HTML ───
|
package/dist/commands/build.d.ts
CHANGED
|
@@ -11,5 +11,29 @@
|
|
|
11
11
|
* 8. Write manifest.json
|
|
12
12
|
* 9. Emit hot deploy templates (Dockerfile, fly.toml, package.json) when hot routes present
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Remove hashed page bundles that this build did not emit.
|
|
16
|
+
*
|
|
17
|
+
* Bundle filenames carry a content hash, so every edit to a client or hybrid
|
|
18
|
+
* page produces a new name and orphans the old one. Nothing removed those, so
|
|
19
|
+
* `dist/static/_then/pages` grew with every incremental build and the dead
|
|
20
|
+
* copies shipped to whatever the project deployed to.
|
|
21
|
+
*
|
|
22
|
+
* Recursive, because pages nest (`loaders/island.js`). Empty directories left
|
|
23
|
+
* behind by a deleted page are removed too. `fs` is injected so this can be
|
|
24
|
+
* tested without touching a disk.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pruneStaleBundles(dir: string, keep: Set<string>, fs: {
|
|
27
|
+
readdir: (p: string, o: {
|
|
28
|
+
withFileTypes: true;
|
|
29
|
+
}) => Promise<Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
isDirectory: () => boolean;
|
|
32
|
+
}>>;
|
|
33
|
+
rm: (p: string, o?: {
|
|
34
|
+
recursive?: boolean;
|
|
35
|
+
force?: boolean;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
}): Promise<number>;
|
|
14
38
|
export declare function buildCommand(_args: string[]): Promise<void>;
|
|
15
39
|
//# sourceMappingURL=build.d.ts.map
|
package/dist/commands/build.js
CHANGED
|
@@ -17,6 +17,49 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
17
17
|
import { join as pathJoin, resolve as pathResolve, relative } from 'node:path';
|
|
18
18
|
import { pathToFileURL } from 'node:url';
|
|
19
19
|
import { loadConfig } from '../config-loader.js';
|
|
20
|
+
/**
|
|
21
|
+
* Remove hashed page bundles that this build did not emit.
|
|
22
|
+
*
|
|
23
|
+
* Bundle filenames carry a content hash, so every edit to a client or hybrid
|
|
24
|
+
* page produces a new name and orphans the old one. Nothing removed those, so
|
|
25
|
+
* `dist/static/_then/pages` grew with every incremental build and the dead
|
|
26
|
+
* copies shipped to whatever the project deployed to.
|
|
27
|
+
*
|
|
28
|
+
* Recursive, because pages nest (`loaders/island.js`). Empty directories left
|
|
29
|
+
* behind by a deleted page are removed too. `fs` is injected so this can be
|
|
30
|
+
* tested without touching a disk.
|
|
31
|
+
*/
|
|
32
|
+
export async function pruneStaleBundles(dir, keep, fs) {
|
|
33
|
+
let removed = 0;
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 0; // nothing was written here
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const full = pathJoin(dir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
removed += await pruneStaleBundles(full, keep, fs);
|
|
45
|
+
// The page that lived here is gone; do not leave the empty shell.
|
|
46
|
+
try {
|
|
47
|
+
const rest = await fs.readdir(full, { withFileTypes: true });
|
|
48
|
+
if (rest.length === 0)
|
|
49
|
+
await fs.rm(full, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* raced or unreadable: leaving it is harmless */
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (keep.has(full))
|
|
57
|
+
continue;
|
|
58
|
+
await fs.rm(full, { force: true });
|
|
59
|
+
removed++;
|
|
60
|
+
}
|
|
61
|
+
return removed;
|
|
62
|
+
}
|
|
20
63
|
// ---------------------------------------------------------------------------
|
|
21
64
|
// Deploy template strings — inlined so they survive tsc compilation (tsc does
|
|
22
65
|
// not copy non-TS assets). Emitted by emitHotDeployTemplates() when hot
|
|
@@ -188,7 +231,7 @@ export async function buildCommand(_args) {
|
|
|
188
231
|
// Shared esbuild helpers
|
|
189
232
|
const { build: esbuild } = await import('esbuild');
|
|
190
233
|
const { join, resolve } = await import('node:path');
|
|
191
|
-
const { mkdir, readFile, rename } = await import('node:fs/promises');
|
|
234
|
+
const { mkdir, readFile, rename, rm, readdir } = await import('node:fs/promises');
|
|
192
235
|
const { existsSync } = await import('node:fs');
|
|
193
236
|
const { createHash } = await import('node:crypto');
|
|
194
237
|
const cliRequire = createRequire(import.meta.url);
|
|
@@ -285,75 +328,21 @@ export async function buildCommand(_args) {
|
|
|
285
328
|
},
|
|
286
329
|
});
|
|
287
330
|
/** Externals every server-side bundle shares: one framework copy per process. */
|
|
288
|
-
const serverRuntimeExternals = ['what-framework', 'what-framework/*', 'what-core', 'what-core/*'];
|
|
289
331
|
/** Browser bundles: what-framework is inlined, because a browser has no resolver. */
|
|
290
332
|
const browserEsmResolvePlugin = makeEsmResolvePlugin({ inlineWhatFramework: true });
|
|
291
333
|
/** Server bundles: what-framework stays external, so the process holds one copy. */
|
|
292
334
|
const serverEsmResolvePlugin = makeEsmResolvePlugin({ inlineWhatFramework: false });
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
await esbuild({
|
|
305
|
-
entryPoints: [absPath],
|
|
306
|
-
bundle: true,
|
|
307
|
-
format: 'esm',
|
|
308
|
-
target: 'es2022',
|
|
309
|
-
platform: 'node',
|
|
310
|
-
outfile: outPath,
|
|
311
|
-
jsx: 'automatic',
|
|
312
|
-
jsxImportSource,
|
|
313
|
-
plugins: [serverEsmResolvePlugin],
|
|
314
|
-
// Server bundles keep the framework external so the process holds one
|
|
315
|
-
// copy; core's builder rewrites these files with the same externals.
|
|
316
|
-
external: serverRuntimeExternals,
|
|
317
|
-
});
|
|
318
|
-
console.log(` ◈ ${page.urlPattern} → dist/server/pages/${outFile}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// 3b. Bundle layout files used by server-mode pages
|
|
322
|
-
if (manifest.layouts.length > 0 && serverPages.length > 0) {
|
|
323
|
-
// Only compile layouts that are actually referenced by server-mode pages
|
|
324
|
-
const usedLayoutPaths = new Set();
|
|
325
|
-
for (const page of serverPages) {
|
|
326
|
-
if (page.layouts) {
|
|
327
|
-
for (const lp of page.layouts)
|
|
328
|
-
usedLayoutPaths.add(lp);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
const layoutsToCompile = manifest.layouts.filter(l => usedLayoutPaths.has(l.filePath));
|
|
332
|
-
if (layoutsToCompile.length > 0) {
|
|
333
|
-
console.log(` Bundling ${layoutsToCompile.length} layout files...`);
|
|
334
|
-
const serverPagesDir = join(root, 'dist', 'server', 'pages');
|
|
335
|
-
await mkdir(serverPagesDir, { recursive: true });
|
|
336
|
-
for (const layout of layoutsToCompile) {
|
|
337
|
-
const absPath = resolve(root, layout.filePath);
|
|
338
|
-
const outFile = layout.filePath.replace(/^src\/pages\//, '').replace(/\.tsx?$/, '.js');
|
|
339
|
-
const outPath = join(serverPagesDir, outFile);
|
|
340
|
-
await mkdir(join(outPath, '..'), { recursive: true });
|
|
341
|
-
await esbuild({
|
|
342
|
-
entryPoints: [absPath],
|
|
343
|
-
bundle: true,
|
|
344
|
-
format: 'esm',
|
|
345
|
-
target: 'es2022',
|
|
346
|
-
platform: 'node',
|
|
347
|
-
outfile: outPath,
|
|
348
|
-
jsx: 'automatic',
|
|
349
|
-
jsxImportSource,
|
|
350
|
-
plugins: [serverEsmResolvePlugin],
|
|
351
|
-
external: serverRuntimeExternals,
|
|
352
|
-
});
|
|
353
|
-
console.log(` ⊟ layout ${layout.dirPattern || '(root)'} → dist/server/pages/${outFile}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
335
|
+
// Steps 3a and 3b used to live here: the CLI bundled every server-mode page
|
|
336
|
+
// and every layout into `dist/server/pages/`, and then core's `build()` ran
|
|
337
|
+
// `bundleServerPageModules()` over the same inputs and overwrote every one of
|
|
338
|
+
// those files. The output was byte-identical, so the only thing the CLI's
|
|
339
|
+
// copy produced was a second esbuild pass per page and per layout on every
|
|
340
|
+
// build, plus a second copy of the resolve configuration to keep in sync with
|
|
341
|
+
// core's. The `useSignal` bug shipped through exactly that kind of duplicate:
|
|
342
|
+
// one copy was fixed and the other was not.
|
|
343
|
+
//
|
|
344
|
+
// Page and layout bundling now happens once, in core. `serverEsmResolvePlugin`
|
|
345
|
+
// below is still used for the standalone entry.
|
|
357
346
|
// 3c. Bundle browser entries for client and hybrid pages.
|
|
358
347
|
// Each bundle is a generated wrapper (generateClientPageEntry) that imports
|
|
359
348
|
// the page module and calls mount() (client) / hydrate() (hybrid) — bundling
|
|
@@ -365,6 +354,10 @@ export async function buildCommand(_args) {
|
|
|
365
354
|
console.log(` Bundling ${browserPages.length} browser page entries...`);
|
|
366
355
|
const clientPagesDir = join(root, 'dist', 'static', '_then', 'pages');
|
|
367
356
|
await mkdir(clientPagesDir, { recursive: true });
|
|
357
|
+
// Every bundle this build writes. Anything else under the directory is a
|
|
358
|
+
// bundle from an earlier build whose content hash has since changed, and
|
|
359
|
+
// nothing references it any more.
|
|
360
|
+
const emittedBundles = new Set();
|
|
368
361
|
const { dirname, basename } = await import('node:path');
|
|
369
362
|
for (const page of browserPages) {
|
|
370
363
|
const absPath = resolve(root, page.filePath);
|
|
@@ -409,10 +402,23 @@ export async function buildCommand(_args) {
|
|
|
409
402
|
.slice(0, 12);
|
|
410
403
|
const hashedOutFile = outFile.replace(/\.js$/, `.${bundleHash}.js`);
|
|
411
404
|
await rename(outPath, join(clientPagesDir, hashedOutFile));
|
|
405
|
+
emittedBundles.add(join(clientPagesDir, hashedOutFile));
|
|
412
406
|
const scriptPath = `/_then/pages/${hashedOutFile.replace(/\\/g, '/')}`;
|
|
413
407
|
clientScripts[page.filePath] = scriptPath;
|
|
414
408
|
console.log(` ◇ ${page.urlPattern} → dist/static${scriptPath}`);
|
|
415
409
|
}
|
|
410
|
+
// Drop bundles left behind by earlier builds. The filename carries a
|
|
411
|
+
// content hash, so editing a page emits a new name and orphans the old
|
|
412
|
+
// one, which nothing then removes: `dist/` grew with every incremental
|
|
413
|
+
// build and shipped the dead copies to whatever you deployed to.
|
|
414
|
+
//
|
|
415
|
+
// Pruning *after* the writes rather than wiping the directory first is
|
|
416
|
+
// deliberate. A build that fails partway leaves the previous bundles
|
|
417
|
+
// intact instead of leaving nothing to serve.
|
|
418
|
+
const pruned = await pruneStaleBundles(clientPagesDir, emittedBundles, { readdir, rm });
|
|
419
|
+
if (pruned > 0) {
|
|
420
|
+
console.log(` ✓ removed ${pruned} stale bundle${pruned === 1 ? '' : 's'} from earlier builds`);
|
|
421
|
+
}
|
|
416
422
|
}
|
|
417
423
|
// 4. Build API routes + task entries
|
|
418
424
|
console.log(' Building...');
|
package/dist/commands/dev.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { importRouteModule } from './shared.js';
|
|
16
16
|
import { resolve as nodeResolve, relative as nodeRelative } from 'node:path';
|
|
17
|
-
import { buildManifest, compilePageRoutes, matchPageRoute, compileRoutes, matchApiPath, getLogger, wrapDocument, escapeHtml, parseNodeBody, reportError, getMimeType, runTaskOnce, buildTaskEnvelope, createApiApp, createWsUpgradeHandler, createNoServerWebSocketServer, GLOBAL_HOOKS_FILENAMES, nodeToWebRequest, writeWebResponse, generateClientPageEntry, vuraBrowserResolvePlugin, vuraActionsStubPlugin, registerActionModules, ACTION_ENDPOINT, createMiddlewareRunner, createVuraRenderRoute, } from '@celsian/vura-core';
|
|
17
|
+
import { buildManifest, compilePageRoutes, matchPageRoute, compileRoutes, matchApiPath, getLogger, wrapDocument, escapeHtml, parseNodeBody, reportError, getMimeType, runTaskOnce, buildTaskEnvelope, createApiApp, createWsUpgradeHandler, createNoServerWebSocketServer, GLOBAL_HOOKS_FILENAMES, nodeToWebRequest, writeWebResponse, generateClientPageEntry, vuraBrowserResolvePlugin, vuraActionsStubPlugin, registerActionModules, ACTION_ENDPOINT, createMiddlewareRunner, createVuraRenderRoute, createVuraStreamRoute, isStreamingPage, } from '@celsian/vura-core';
|
|
18
18
|
export function parseDevOptions(args, projectRoot = process.cwd()) {
|
|
19
19
|
const portArg = args.find((_, i) => args[i - 1] === '--port');
|
|
20
20
|
const hostArg = args.find((_, i) => args[i - 1] === '--host');
|
|
@@ -207,6 +207,13 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
207
207
|
const devRenderRouteFor = (page) => createVuraRenderRoute({
|
|
208
208
|
extraScripts: () => (page.mode === 'hybrid' ? [browserScriptPath(page)] : []),
|
|
209
209
|
});
|
|
210
|
+
/**
|
|
211
|
+
* The streaming counterpart, built from the same options as the string
|
|
212
|
+
* renderer above so the two cannot disagree about which scripts a page gets.
|
|
213
|
+
*/
|
|
214
|
+
const devStreamRouteFor = (page) => createVuraStreamRoute({
|
|
215
|
+
extraScripts: () => (page.mode === 'hybrid' ? [browserScriptPath(page)] : []),
|
|
216
|
+
});
|
|
210
217
|
const { existsSync } = await import('node:fs');
|
|
211
218
|
// ── Middleware ──
|
|
212
219
|
// Loaded through the same cached loader as routes so an edit is picked up on
|
|
@@ -670,6 +677,21 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
670
677
|
method,
|
|
671
678
|
headers: req.headers,
|
|
672
679
|
});
|
|
680
|
+
// Streaming pages take the same route in dev as in production,
|
|
681
|
+
// through the same helper and the same response writer. A dev
|
|
682
|
+
// server that renders a streamed page as a string would hide
|
|
683
|
+
// exactly the bugs streaming introduces.
|
|
684
|
+
if (method === 'GET' && isStreamingPage(runtimePage)) {
|
|
685
|
+
const streamed = await devStreamRouteFor(pageMatch.page)({
|
|
686
|
+
path: url.pathname,
|
|
687
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
688
|
+
route: { path: url.pathname, page: { mode: 'server' }, vura: runtimePage },
|
|
689
|
+
params: pageMatch.params,
|
|
690
|
+
request: webReq,
|
|
691
|
+
});
|
|
692
|
+
await writeWebResponse(res, streamed);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
673
695
|
const result = await devRenderRouteFor(pageMatch.page)({
|
|
674
696
|
path: url.pathname,
|
|
675
697
|
query: Object.fromEntries(url.searchParams.entries()),
|
|
@@ -802,7 +824,13 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
802
824
|
process.on('SIGINT', onSigint);
|
|
803
825
|
await new Promise((resolve) => {
|
|
804
826
|
server.listen(opts.port, opts.host, () => {
|
|
805
|
-
|
|
827
|
+
// The *bound* port, not the requested one. `--port 0` means "pick a free
|
|
828
|
+
// port", and printing the request back gave a URL of `:0`, which cannot
|
|
829
|
+
// be connected to. The address is also what tells you which port you got
|
|
830
|
+
// when the requested one was taken.
|
|
831
|
+
const addr = server.address();
|
|
832
|
+
const boundPort = addr && typeof addr === 'object' ? addr.port : opts.port;
|
|
833
|
+
console.log(` Server listening on http://${opts.host}:${boundPort}\n`);
|
|
806
834
|
// Warn once at startup when the user explicitly exposes the dev server beyond loopback.
|
|
807
835
|
if (isLanDevHost(opts.host)) {
|
|
808
836
|
console.warn(` [vura] Dev server exposed on ${opts.host}. Only use --host for trusted LAN testing.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celsian/vura-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Vura CLI — build and deploy full-stack apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"!dist/**/*.map"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@celsian/vura-core": "0.
|
|
19
|
-
"esbuild": "^0.28.
|
|
20
|
-
"what-framework": "^0.13.
|
|
18
|
+
"@celsian/vura-core": "0.8.0",
|
|
19
|
+
"esbuild": "^0.28.2",
|
|
20
|
+
"what-framework": "^0.13.4"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@celsian/vura-adapter-vura": "0.
|
|
23
|
+
"@celsian/vura-adapter-vura": "0.8.0",
|
|
24
24
|
"ws": "^8.0.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@celsian/vura-adapter-vura": "0.
|
|
35
|
+
"@celsian/vura-adapter-vura": "0.8.0",
|
|
36
36
|
"@types/ws": "^8.18.1",
|
|
37
|
-
"ws": "^8.21.
|
|
37
|
+
"ws": "^8.21.3"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"publishConfig": {
|