@celsian/vura-cli 0.6.1 → 0.7.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/build.js +61 -11
- package/dist/commands/dev.js +155 -42
- package/dist/commands/shared.js +19 -0
- package/package.json +4 -4
package/dist/commands/build.js
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
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
|
-
import { buildManifest, build, renderStaticPages, generateClientPageEntry, vuraBrowserResolvePlugin } from '@celsian/vura-core';
|
|
14
|
+
import { buildManifest, build, renderStaticPages, generateClientPageEntry, vuraBrowserResolvePlugin, vuraActionsStubPlugin } from '@celsian/vura-core';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
-
import { join as pathJoin, resolve as pathResolve } from 'node:path';
|
|
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
20
|
// ---------------------------------------------------------------------------
|
|
@@ -116,6 +116,17 @@ async function emitDeployPackageJson(distDir, projectRoot, hasWsRoutes) {
|
|
|
116
116
|
await writeFile(pkgPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
117
117
|
}
|
|
118
118
|
/** The exact what-framework version installed in the project, or null. */
|
|
119
|
+
/**
|
|
120
|
+
* Import specifiers for a page's layout chain, relative to the page's own
|
|
121
|
+
* directory (the resolveDir the generated browser entry is bundled with).
|
|
122
|
+
*/
|
|
123
|
+
function layoutSpecifiersFor(page, projectRoot) {
|
|
124
|
+
const pageDir = pathResolve(projectRoot, page.filePath, '..');
|
|
125
|
+
return (page.layouts ?? []).map((layoutPath) => {
|
|
126
|
+
const rel = relative(pageDir, pathResolve(projectRoot, layoutPath)).replace(/\\/g, '/');
|
|
127
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
119
130
|
function resolveWhatFrameworkVersion(projectRoot) {
|
|
120
131
|
// Read the installed package.json off disk instead of resolving the
|
|
121
132
|
// specifier. `require('what-framework/package.json')` looks like the obvious
|
|
@@ -215,7 +226,24 @@ export async function buildCommand(_args) {
|
|
|
215
226
|
}
|
|
216
227
|
return join(root, 'node_modules', pkg);
|
|
217
228
|
}
|
|
218
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Resolve the bare specifiers esbuild cannot.
|
|
231
|
+
*
|
|
232
|
+
* `inlineWhatFramework` decides what happens to `what-framework` and
|
|
233
|
+
* `what-core`. A browser bundle needs them inlined — there is no module
|
|
234
|
+
* resolution in a browser, and their exports maps are import-condition-only,
|
|
235
|
+
* so `require.resolve` cannot find them. A **server** bundle must keep them
|
|
236
|
+
* external, and this is the switch that says which.
|
|
237
|
+
*
|
|
238
|
+
* It exists because an `onResolve` that returns a path *beats* esbuild's
|
|
239
|
+
* `external` list. Setting both, as the build-time page loader did, silently
|
|
240
|
+
* loses: the page module inlined its own copy of what-core while
|
|
241
|
+
* `renderToString` ran from the installed one, the two disagreed about which
|
|
242
|
+
* component was rendering, and `useSignal()` threw "can only be called
|
|
243
|
+
* inside a component function" in every `static` and `hybrid` page. Loaders
|
|
244
|
+
* escaped it only because `@celsian/vura-core` is not intercepted here.
|
|
245
|
+
*/
|
|
246
|
+
const makeEsmResolvePlugin = ({ inlineWhatFramework }) => ({
|
|
219
247
|
name: 'esm-resolve',
|
|
220
248
|
setup(build) {
|
|
221
249
|
build.onResolve({ filter: /^@celsian\/vura-core\/(jsx-runtime|jsx-dev-runtime)$/ }, (args) => {
|
|
@@ -226,6 +254,8 @@ export async function buildCommand(_args) {
|
|
|
226
254
|
return { path: cliRequire.resolve(args.path) };
|
|
227
255
|
}
|
|
228
256
|
});
|
|
257
|
+
if (!inlineWhatFramework)
|
|
258
|
+
return;
|
|
229
259
|
// Bare what-framework/what-core imports (the generated client entry
|
|
230
260
|
// imports { h, mount, hydrate } from 'what-framework'). The exports map
|
|
231
261
|
// is import-condition-only, so require.resolve can't find it — resolve
|
|
@@ -253,7 +283,13 @@ export async function buildCommand(_args) {
|
|
|
253
283
|
return null;
|
|
254
284
|
});
|
|
255
285
|
},
|
|
256
|
-
};
|
|
286
|
+
});
|
|
287
|
+
/** Externals every server-side bundle shares: one framework copy per process. */
|
|
288
|
+
const serverRuntimeExternals = ['what-framework', 'what-framework/*', 'what-core', 'what-core/*'];
|
|
289
|
+
/** Browser bundles: what-framework is inlined, because a browser has no resolver. */
|
|
290
|
+
const browserEsmResolvePlugin = makeEsmResolvePlugin({ inlineWhatFramework: true });
|
|
291
|
+
/** Server bundles: what-framework stays external, so the process holds one copy. */
|
|
292
|
+
const serverEsmResolvePlugin = makeEsmResolvePlugin({ inlineWhatFramework: false });
|
|
257
293
|
// 3. Bundle server-mode pages
|
|
258
294
|
const serverPages = manifest.pages.filter(p => p.mode === 'server' || p.mode === 'hybrid');
|
|
259
295
|
if (serverPages.length > 0) {
|
|
@@ -274,8 +310,10 @@ export async function buildCommand(_args) {
|
|
|
274
310
|
outfile: outPath,
|
|
275
311
|
jsx: 'automatic',
|
|
276
312
|
jsxImportSource,
|
|
277
|
-
plugins: [
|
|
278
|
-
external
|
|
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,
|
|
279
317
|
});
|
|
280
318
|
console.log(` ◈ ${page.urlPattern} → dist/server/pages/${outFile}`);
|
|
281
319
|
}
|
|
@@ -309,8 +347,8 @@ export async function buildCommand(_args) {
|
|
|
309
347
|
outfile: outPath,
|
|
310
348
|
jsx: 'automatic',
|
|
311
349
|
jsxImportSource,
|
|
312
|
-
plugins: [
|
|
313
|
-
external:
|
|
350
|
+
plugins: [serverEsmResolvePlugin],
|
|
351
|
+
external: serverRuntimeExternals,
|
|
314
352
|
});
|
|
315
353
|
console.log(` ⊟ layout ${layout.dirPattern || '(root)'} → dist/server/pages/${outFile}`);
|
|
316
354
|
}
|
|
@@ -335,7 +373,11 @@ export async function buildCommand(_args) {
|
|
|
335
373
|
await mkdir(join(outPath, '..'), { recursive: true });
|
|
336
374
|
await esbuild({
|
|
337
375
|
stdin: {
|
|
338
|
-
contents: generateClientPageEntry(`./${basename(absPath)}`, page.mode
|
|
376
|
+
contents: generateClientPageEntry(`./${basename(absPath)}`, page.mode,
|
|
377
|
+
// The server rendered this page inside its layouts, so the browser
|
|
378
|
+
// has to hydrate the same tree. Specifiers are relative to the
|
|
379
|
+
// page's own directory, which is the entry's resolveDir.
|
|
380
|
+
{ layoutImportSpecifiers: layoutSpecifiersFor(page, root) }),
|
|
339
381
|
resolveDir: dirname(absPath),
|
|
340
382
|
sourcefile: '__vura-client-entry__.js',
|
|
341
383
|
loader: 'js',
|
|
@@ -350,7 +392,15 @@ export async function buildCommand(_args) {
|
|
|
350
392
|
// Browser-resolve first: a page that imports `@celsian/vura-core` for
|
|
351
393
|
// useLoaderData must get the pure client module here, not the package
|
|
352
394
|
// root, which reaches node:fs and cannot be bundled for a browser.
|
|
353
|
-
|
|
395
|
+
//
|
|
396
|
+
// The actions stub plugin is the security boundary for `src/actions/`:
|
|
397
|
+
// it answers onResolve, so esbuild never opens an action file for this
|
|
398
|
+
// bundle and nothing inside one can reach the browser.
|
|
399
|
+
plugins: [
|
|
400
|
+
vuraBrowserResolvePlugin(),
|
|
401
|
+
vuraActionsStubPlugin({ projectRoot: root }),
|
|
402
|
+
browserEsmResolvePlugin,
|
|
403
|
+
],
|
|
354
404
|
external: [],
|
|
355
405
|
});
|
|
356
406
|
const bundleHash = createHash('sha256')
|
|
@@ -412,7 +462,7 @@ export async function buildCommand(_args) {
|
|
|
412
462
|
outfile: tmpFile,
|
|
413
463
|
jsx: 'automatic',
|
|
414
464
|
jsxImportSource,
|
|
415
|
-
plugins: [
|
|
465
|
+
plugins: [serverEsmResolvePlugin],
|
|
416
466
|
external: sharedRuntimeExternals,
|
|
417
467
|
});
|
|
418
468
|
return nativeImport(pathToFileURL(tmpFile).href);
|
package/dist/commands/dev.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* vura dev — Start dev server on port 3000
|
|
13
13
|
* vura dev --port 8080 — Start on custom port
|
|
14
14
|
*/
|
|
15
|
-
import { renderToString as builtinRenderToString } from 'what-framework/server';
|
|
16
15
|
import { importRouteModule } from './shared.js';
|
|
17
|
-
import {
|
|
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';
|
|
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');
|
|
@@ -141,6 +141,15 @@ function printRouteTable(manifest) {
|
|
|
141
141
|
console.log(` ${icon} ${page.mode.padEnd(18)} ${page.urlPattern}`);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
const actions = manifest.actions ?? [];
|
|
145
|
+
if (actions.length > 0) {
|
|
146
|
+
console.log(' Actions:');
|
|
147
|
+
for (const mod of actions) {
|
|
148
|
+
for (const name of mod.exports) {
|
|
149
|
+
console.log(` ⚡ ${'server action'.padEnd(18)} ${mod.moduleId}#${name}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
144
153
|
console.log();
|
|
145
154
|
}
|
|
146
155
|
/**
|
|
@@ -181,7 +190,45 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
181
190
|
return mod;
|
|
182
191
|
}
|
|
183
192
|
const logger = getLogger();
|
|
193
|
+
/**
|
|
194
|
+
* The render callback for one page request.
|
|
195
|
+
*
|
|
196
|
+
* A hybrid page's browser bundle lives at a dev-server path that does not
|
|
197
|
+
* exist at build time, so it is injected here rather than read off the page.
|
|
198
|
+
*
|
|
199
|
+
* Built per request, closing over the matched page, because the RuntimePage
|
|
200
|
+
* handed to the renderer has its `mode` forced to `'server'` (dev SSRs every
|
|
201
|
+
* non-client page per request, with no build output and no ISR in front of
|
|
202
|
+
* it). A predicate reading the mode off *that* object can never see
|
|
203
|
+
* `'hybrid'`, so it silently stopped injecting the bundle and every hybrid
|
|
204
|
+
* page in dev rendered its markup and then never hydrated. The real mode
|
|
205
|
+
* lives on the matched page, so that is what decides.
|
|
206
|
+
*/
|
|
207
|
+
const devRenderRouteFor = (page) => createVuraRenderRoute({
|
|
208
|
+
extraScripts: () => (page.mode === 'hybrid' ? [browserScriptPath(page)] : []),
|
|
209
|
+
});
|
|
184
210
|
const { existsSync } = await import('node:fs');
|
|
211
|
+
// ── Middleware ──
|
|
212
|
+
// Loaded through the same cached loader as routes so an edit is picked up on
|
|
213
|
+
// the next request, and so module-level state in a middleware behaves the way
|
|
214
|
+
// it does in a route. `manifest.middleware` is refreshed by the fs-watcher
|
|
215
|
+
// rescan, so adding or deleting the file mid-session is picked up too.
|
|
216
|
+
async function currentMiddlewareRunner(forManifest) {
|
|
217
|
+
if (!forManifest.middleware)
|
|
218
|
+
return createMiddlewareRunner(null);
|
|
219
|
+
try {
|
|
220
|
+
const mod = await loadHandlerCached(forManifest.middleware);
|
|
221
|
+
return createMiddlewareRunner(mod);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
// A syntax error in middleware must not take the whole dev server down,
|
|
225
|
+
// and it must not silently disable the auth guard the developer is
|
|
226
|
+
// relying on either. Say so, loudly, on every request until it is fixed.
|
|
227
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
228
|
+
logger.error(`[vura] middleware failed to load: ${error.message}`);
|
|
229
|
+
return createMiddlewareRunner(null);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
185
232
|
function findGlobalHooksFile() {
|
|
186
233
|
for (const filename of GLOBAL_HOOKS_FILENAMES) {
|
|
187
234
|
if (existsSync(join(opts.projectRoot, filename)))
|
|
@@ -233,12 +280,29 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
233
280
|
onError: [...(globalHooks?.onError ?? []), devErrorHook],
|
|
234
281
|
onResponse: globalHooks?.onResponse ?? [],
|
|
235
282
|
};
|
|
283
|
+
// Server actions. Loaded and registered on every rebuild, so editing an
|
|
284
|
+
// action file takes effect on the next request the way editing a route
|
|
285
|
+
// does. Registration is keyed by id, so a re-register replaces rather than
|
|
286
|
+
// duplicates; an action deleted from a file stops being callable only once
|
|
287
|
+
// the process restarts, which is the same limitation dev has for a deleted
|
|
288
|
+
// route module and is not worth a registry generation counter.
|
|
289
|
+
const actionModules = {};
|
|
290
|
+
for (const mod of forManifest.actions ?? []) {
|
|
291
|
+
actionModules[mod.moduleId] = await loadHandler(mod.filePath);
|
|
292
|
+
}
|
|
293
|
+
const hasActions = Object.keys(actionModules).length > 0;
|
|
294
|
+
if (hasActions)
|
|
295
|
+
registerActionModules(actionModules);
|
|
236
296
|
// Compile route regexes for the path-existence pre-check (method-agnostic).
|
|
237
297
|
const compiledApiRoutes = compileRoutes(routes);
|
|
238
|
-
return {
|
|
298
|
+
return {
|
|
299
|
+
app: createApiApp({ routes, globalHooks: mergedHooks, enableActions: hasActions }),
|
|
300
|
+
compiledApiRoutes,
|
|
301
|
+
internalPaths: hasActions ? [ACTION_ENDPOINT] : [],
|
|
302
|
+
};
|
|
239
303
|
}
|
|
240
304
|
// Build initial CelsianApp and page route table
|
|
241
|
-
let { app: apiApp, compiledApiRoutes } = await buildStandaloneApiApp(manifest);
|
|
305
|
+
let { app: apiApp, compiledApiRoutes, internalPaths } = await buildStandaloneApiApp(manifest);
|
|
242
306
|
// In dev mode, compile ALL page routes — not just server/hybrid.
|
|
243
307
|
// Static and server pages are SSR'd on the fly; client pages are served as
|
|
244
308
|
// a shell + on-demand browser bundle (SSR'ing them would run hooks like
|
|
@@ -263,7 +327,10 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
263
327
|
catch { /* not installed — keep default */ }
|
|
264
328
|
const result = await esbuild({
|
|
265
329
|
stdin: {
|
|
266
|
-
contents: generateClientPageEntry(`./${basename(absPath)}`, page.mode,
|
|
330
|
+
contents: generateClientPageEntry(`./${basename(absPath)}`, page.mode,
|
|
331
|
+
// Same layout chain the dev renderer wraps the page in, so hydration
|
|
332
|
+
// walks the tree that is actually in the document.
|
|
333
|
+
{ dev: true, layoutImportSpecifiers: devLayoutSpecifiers(page) }),
|
|
267
334
|
resolveDir: dirname(absPath),
|
|
268
335
|
sourcefile: '__vura-client-entry__.js',
|
|
269
336
|
loader: 'js',
|
|
@@ -278,13 +345,28 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
278
345
|
jsxImportSource,
|
|
279
346
|
// Same redirect the production build applies: `@celsian/vura-core` in a
|
|
280
347
|
// browser bundle resolves to the pure client module.
|
|
281
|
-
|
|
348
|
+
// The actions stub plugin is what keeps `src/actions/` source out of a
|
|
349
|
+
// browser bundle in dev as well as in a build. Without it `vura dev`
|
|
350
|
+
// would happily bundle a database client into the page and only the
|
|
351
|
+
// production build would catch it.
|
|
352
|
+
plugins: [vuraBrowserResolvePlugin(), vuraActionsStubPlugin({ projectRoot: opts.projectRoot })],
|
|
282
353
|
nodePaths: [join(opts.projectRoot, 'node_modules')],
|
|
283
354
|
});
|
|
284
355
|
const text = result.outputFiles[0].text;
|
|
285
356
|
browserBundleCache.set(page.filePath, text);
|
|
286
357
|
return text;
|
|
287
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Import specifiers for a page's layouts, relative to the page's directory,
|
|
361
|
+
* which is the resolveDir the browser entry is bundled with.
|
|
362
|
+
*/
|
|
363
|
+
function devLayoutSpecifiers(page) {
|
|
364
|
+
const pageDir = nodeResolve(opts.projectRoot, page.filePath, '..');
|
|
365
|
+
return (page.layouts ?? []).map((layoutPath) => {
|
|
366
|
+
const rel = nodeRelative(pageDir, nodeResolve(opts.projectRoot, layoutPath)).replace(/\\/g, '/');
|
|
367
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
288
370
|
const server = createServer(async (req, res) => {
|
|
289
371
|
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
290
372
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
@@ -308,6 +390,34 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
308
390
|
res.end();
|
|
309
391
|
return;
|
|
310
392
|
}
|
|
393
|
+
// ── Middleware ──
|
|
394
|
+
// Before static serving, before routes: an auth guard has to be able to
|
|
395
|
+
// keep a visitor away from a page, and a page may be a prerendered file.
|
|
396
|
+
let middlewareHeaders;
|
|
397
|
+
{
|
|
398
|
+
const runner = await currentMiddlewareRunner(manifest);
|
|
399
|
+
if (runner.enabled) {
|
|
400
|
+
const webReq = new Request(url.toString(), {
|
|
401
|
+
method,
|
|
402
|
+
headers: req.headers,
|
|
403
|
+
});
|
|
404
|
+
const outcome = await runner.run(webReq, url);
|
|
405
|
+
if (outcome.response) {
|
|
406
|
+
const headers = {};
|
|
407
|
+
outcome.response.headers.forEach((v, k) => { headers[k] = v; });
|
|
408
|
+
res.writeHead(outcome.response.status, headers);
|
|
409
|
+
res.end(outcome.response.body ? await outcome.response.text() : '');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
middlewareHeaders = outcome.headers;
|
|
413
|
+
}
|
|
414
|
+
if (middlewareHeaders) {
|
|
415
|
+
for (const [k, v] of middlewareHeaders) {
|
|
416
|
+
if (!res.hasHeader(k))
|
|
417
|
+
res.setHeader(k, v);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
311
421
|
// Static file serving from public/ directory
|
|
312
422
|
if (method === 'GET' || method === 'HEAD') {
|
|
313
423
|
const publicDir = join(opts.projectRoot, 'public');
|
|
@@ -470,7 +580,7 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
470
580
|
// pattern matches this pathname, skip celsian entirely and fall through to
|
|
471
581
|
// pages/404. This also correctly passes through intentional handler 404s —
|
|
472
582
|
// if the route exists but returns 404, that response is delivered as-is.
|
|
473
|
-
if (matchApiPath(compiledApiRoutes, url.pathname)) {
|
|
583
|
+
if (matchApiPath(compiledApiRoutes, url.pathname) || internalPaths.includes(url.pathname)) {
|
|
474
584
|
try {
|
|
475
585
|
const webReq = nodeToWebRequest(req, url);
|
|
476
586
|
const webRes = await apiApp.handle(webReq);
|
|
@@ -536,41 +646,43 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
536
646
|
return;
|
|
537
647
|
}
|
|
538
648
|
if (typeof Component === 'function') {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (pageMatch.page.layouts && pageMatch.page.layouts.length > 0) {
|
|
550
|
-
// Load layouts innermost-last, wrap from inside out
|
|
551
|
-
for (let li = pageMatch.page.layouts.length - 1; li >= 0; li--) {
|
|
552
|
-
const layoutMod = await loadHandler(pageMatch.page.layouts[li]);
|
|
553
|
-
const LayoutComponent = layoutMod.default;
|
|
554
|
-
if (typeof LayoutComponent === 'function') {
|
|
555
|
-
vnode = LayoutComponent({ children: vnode, params: pageMatch.params });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
649
|
+
// Rendered by the SAME function the production server uses.
|
|
650
|
+
// The dev server used to carry its own copy of this logic, and it
|
|
651
|
+
// drifted: it called the component directly instead of through
|
|
652
|
+
// `h()`, it knew only `getServerData` and not `loader`, and it had
|
|
653
|
+
// no layout data, no payload and no notFound/redirect. So RFC 0001
|
|
654
|
+
// loaders worked in a built app and failed in `vura dev`, which is
|
|
655
|
+
// where a developer meets the feature first.
|
|
656
|
+
const layoutModules = [];
|
|
657
|
+
for (const layoutPath of pageMatch.page.layouts ?? []) {
|
|
658
|
+
layoutModules.push(await loadHandler(layoutPath));
|
|
558
659
|
}
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
660
|
+
const runtimePage = {
|
|
661
|
+
...pageMatch.page,
|
|
662
|
+
// Dev renders every non-client page per request: there is no
|
|
663
|
+
// build output to serve, and no ISR cache in front of it.
|
|
664
|
+
mode: 'server',
|
|
665
|
+
config: { ...(pageMatch.page.config ?? {}), revalidate: undefined },
|
|
666
|
+
module: mod,
|
|
667
|
+
layoutModules,
|
|
668
|
+
};
|
|
669
|
+
const webReq = new Request(url.toString(), {
|
|
670
|
+
method,
|
|
671
|
+
headers: req.headers,
|
|
571
672
|
});
|
|
572
|
-
|
|
573
|
-
|
|
673
|
+
const result = await devRenderRouteFor(pageMatch.page)({
|
|
674
|
+
path: url.pathname,
|
|
675
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
676
|
+
config: { mode: 'server' },
|
|
677
|
+
route: { path: url.pathname, page: { mode: 'server' }, vura: runtimePage },
|
|
678
|
+
params: pageMatch.params,
|
|
679
|
+
request: webReq,
|
|
680
|
+
});
|
|
681
|
+
res.writeHead(result.status, {
|
|
682
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
683
|
+
...(result.headers ?? {}),
|
|
684
|
+
});
|
|
685
|
+
res.end(result.html);
|
|
574
686
|
return;
|
|
575
687
|
}
|
|
576
688
|
}
|
|
@@ -627,7 +739,8 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
627
739
|
// Watch for file changes and re-scan manifest
|
|
628
740
|
const apiDir = join(opts.projectRoot, 'src', 'api');
|
|
629
741
|
const pagesDir = join(opts.projectRoot, 'src', 'pages');
|
|
630
|
-
const
|
|
742
|
+
const actionsDir = join(opts.projectRoot, 'src', 'actions');
|
|
743
|
+
const watchDirs = [apiDir, pagesDir, actionsDir];
|
|
631
744
|
const watchers = [];
|
|
632
745
|
for (const dir of watchDirs) {
|
|
633
746
|
try {
|
|
@@ -652,7 +765,7 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
652
765
|
// Rebuild against the FRESH manifest (nextManifest) — `manifest`
|
|
653
766
|
// is only swapped after success, so building from the closure
|
|
654
767
|
// variable would use the stale route set (see buildStandaloneApiApp).
|
|
655
|
-
({ app: apiApp, compiledApiRoutes } = await buildStandaloneApiApp(nextManifest));
|
|
768
|
+
({ app: apiApp, compiledApiRoutes, internalPaths } = await buildStandaloneApiApp(nextManifest));
|
|
656
769
|
}
|
|
657
770
|
catch (err) {
|
|
658
771
|
moduleCache.clear();
|
package/dist/commands/shared.js
CHANGED
|
@@ -44,6 +44,25 @@ export async function importRouteModule(projectRoot, filePath) {
|
|
|
44
44
|
platform: 'node',
|
|
45
45
|
write: false,
|
|
46
46
|
outfile: 'handler.mjs',
|
|
47
|
+
// The framework stays external, for the same reason it does in a
|
|
48
|
+
// production build: this module is imported into the dev server's own
|
|
49
|
+
// process and rendered by the dev server's `renderToString`. Inlining
|
|
50
|
+
// what-framework gives the page a second copy with its own "currently
|
|
51
|
+
// rendering component" and its own context registry, so every hook it
|
|
52
|
+
// calls reads a registry the renderer never wrote to. That is what made
|
|
53
|
+
// `useLoaderData()` fail in `vura dev` while working in a built app: the
|
|
54
|
+
// page reported "found no loader data" and What warned that useContext had
|
|
55
|
+
// been called outside a render.
|
|
56
|
+
//
|
|
57
|
+
// Safe to mark external here because the output is written to a real file
|
|
58
|
+
// inside the project, so Node resolves these specifiers from the project's
|
|
59
|
+
// own node_modules.
|
|
60
|
+
external: [
|
|
61
|
+
'what-framework',
|
|
62
|
+
'what-framework/*',
|
|
63
|
+
'@celsian/vura-core',
|
|
64
|
+
'@celsian/vura-core/*',
|
|
65
|
+
],
|
|
47
66
|
...(isPage ? { jsx: 'automatic', jsxImportSource } : {}),
|
|
48
67
|
});
|
|
49
68
|
const tmpDir = join(projectRoot, DEV_CACHE_SUBPATH);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celsian/vura-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.
|
|
18
|
+
"@celsian/vura-core": "0.7.0",
|
|
19
19
|
"esbuild": "^0.28.1",
|
|
20
20
|
"what-framework": "^0.13.2"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@celsian/vura-adapter-vura": "0.
|
|
23
|
+
"@celsian/vura-adapter-vura": "0.7.0",
|
|
24
24
|
"ws": "^8.0.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@celsian/vura-adapter-vura": "0.
|
|
35
|
+
"@celsian/vura-adapter-vura": "0.7.0",
|
|
36
36
|
"@types/ws": "^8.18.1",
|
|
37
37
|
"ws": "^8.21.0"
|
|
38
38
|
},
|