@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.
@@ -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
- const esmResolvePlugin = {
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: [esmResolvePlugin],
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: [esmResolvePlugin],
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
- plugins: [vuraBrowserResolvePlugin(), esmResolvePlugin],
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: [esmResolvePlugin],
465
+ plugins: [serverEsmResolvePlugin],
416
466
  external: sharedRuntimeExternals,
417
467
  });
418
468
  return nativeImport(pathToFileURL(tmpFile).href);
@@ -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 { buildManifest, compilePageRoutes, matchPageRoute, compileRoutes, matchApiPath, getLogger, wrapDocument, escapeHtml, parseNodeBody, reportError, getMimeType, runTaskOnce, buildTaskEnvelope, createApiApp, createWsUpgradeHandler, createNoServerWebSocketServer, GLOBAL_HOOKS_FILENAMES, nodeToWebRequest, writeWebResponse, generateClientPageEntry, vuraBrowserResolvePlugin, } from '@celsian/vura-core';
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 { app: createApiApp({ routes, globalHooks: mergedHooks }), compiledApiRoutes };
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, { dev: true }),
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
- plugins: [vuraBrowserResolvePlugin()],
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
- let serverData = {};
540
- if (typeof mod.getServerData === 'function') {
541
- serverData = await mod.getServerData({
542
- params: pageMatch.params,
543
- url: url.pathname,
544
- query: Object.fromEntries(url.searchParams.entries()),
545
- });
546
- }
547
- let vnode = Component({ ...serverData, params: pageMatch.params });
548
- // Wrap in layout chain if layouts are defined (outermost first)
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 bodyHtml = builtinRenderToString(vnode);
560
- const html = wrapDocument(bodyHtml, {
561
- title: pageConfig.title ?? 'Vura App',
562
- meta: pageConfig.meta ?? [],
563
- styles: pageConfig.styles ?? [],
564
- // Hybrid pages also load their browser bundle so hydrate() runs
565
- // against the SSR'd DOM — same contract as the production build.
566
- scripts: [
567
- ...(pageConfig.scripts ?? []),
568
- ...(pageMatch.page.mode === 'hybrid' ? [browserScriptPath(pageMatch.page)] : []),
569
- ],
570
- head: pageConfig.head ?? '',
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
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
573
- res.end(html);
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 watchDirs = [apiDir, pagesDir];
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();
@@ -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.6.1",
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.6.1",
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.6.1",
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.6.1",
35
+ "@celsian/vura-adapter-vura": "0.7.0",
36
36
  "@types/ws": "^8.18.1",
37
37
  "ws": "^8.21.0"
38
38
  },