@celsian/vura-cli 0.8.0 → 0.8.1

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 CHANGED
@@ -52,10 +52,8 @@ and 12 GiB profiles.
52
52
 
53
53
  ## Documentation
54
54
 
55
- _vura.io docs site launches with v0.5 — until then, see the repo README and CHANGELOG._
56
-
57
55
  - [Quick start — /ladder/0-create/](https://vura.io/ladder/0-create/)
58
- - [Task routes — /reference/tasks/](https://vura.io/ladder/5-tasks/)
56
+ - [Task routes — /ladder/5-tasks/](https://vura.io/ladder/5-tasks/)
59
57
  - [Self-host — /self-host/](https://vura.io/self-host/)
60
58
 
61
59
  ## License
@@ -2,14 +2,16 @@
2
2
  * `vura build` — Build the project for deployment.
3
3
  *
4
4
  * 1. Scan routes → build manifest
5
- * 2. Generate server entry (for hot server)
6
- * 3. Generate function entries (for serverless)
7
- * 4. Generate task entries (for task routes)
8
- * 5. Bundle server-mode pages with esbuild
9
- * 6. Render static pages (mode: 'static')
10
- * 7. Run adapter.buildEnd() if configured
11
- * 8. Write manifest.json
12
- * 9. Emit hot deploy templates (Dockerfile, fly.toml, package.json) when hot routes present
5
+ * 2. Bundle browser entries for client and hybrid pages
6
+ * 3. Render build-time pages (static, client shells, hybrid HTML) → dist/static
7
+ * 4. Copy public/ dist/public
8
+ * 5. core build(): server entry, function entries, task entries, manifest.json,
9
+ * then adapter.buildEnd()
10
+ * 6. Emit hot deploy templates (Dockerfile, fly.toml, package.json)
11
+ *
12
+ * The adapter runs last, and the two steps that write dist/static and
13
+ * dist/public run before it, because an adapter that serves prerendered pages
14
+ * has to be able to read them.
13
15
  */
14
16
  /**
15
17
  * Remove hashed page bundles that this build did not emit.
@@ -19,9 +21,11 @@
19
21
  * `dist/static/_then/pages` grew with every incremental build and the dead
20
22
  * copies shipped to whatever the project deployed to.
21
23
  *
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.
24
+ * The sweep itself now lives in core as `pruneStaleOutputs`, because the same
25
+ * accretion happens to `dist/server`, `dist/functions` and each adapter's own
26
+ * output directory, and four copies of a recursive delete is four chances to
27
+ * fix one and not the others. This wrapper is the client-bundle name and the
28
+ * client-bundle reason; the behaviour is shared.
25
29
  */
26
30
  export declare function pruneStaleBundles(dir: string, keep: Set<string>, fs: {
27
31
  readdir: (p: string, o: {
@@ -2,16 +2,18 @@
2
2
  * `vura build` — Build the project for deployment.
3
3
  *
4
4
  * 1. Scan routes → build manifest
5
- * 2. Generate server entry (for hot server)
6
- * 3. Generate function entries (for serverless)
7
- * 4. Generate task entries (for task routes)
8
- * 5. Bundle server-mode pages with esbuild
9
- * 6. Render static pages (mode: 'static')
10
- * 7. Run adapter.buildEnd() if configured
11
- * 8. Write manifest.json
12
- * 9. Emit hot deploy templates (Dockerfile, fly.toml, package.json) when hot routes present
5
+ * 2. Bundle browser entries for client and hybrid pages
6
+ * 3. Render build-time pages (static, client shells, hybrid HTML) → dist/static
7
+ * 4. Copy public/ dist/public
8
+ * 5. core build(): server entry, function entries, task entries, manifest.json,
9
+ * then adapter.buildEnd()
10
+ * 6. Emit hot deploy templates (Dockerfile, fly.toml, package.json)
11
+ *
12
+ * The adapter runs last, and the two steps that write dist/static and
13
+ * dist/public run before it, because an adapter that serves prerendered pages
14
+ * has to be able to read them.
13
15
  */
14
- import { buildManifest, build, renderStaticPages, generateClientPageEntry, vuraBrowserResolvePlugin, vuraActionsStubPlugin } from '@celsian/vura-core';
16
+ import { buildManifest, build, renderStaticPages, generateClientPageEntry, vuraBrowserResolvePlugin, vuraActionsStubPlugin, pruneStaleOutputs } from '@celsian/vura-core';
15
17
  import { createRequire } from 'node:module';
16
18
  import { existsSync, readFileSync } from 'node:fs';
17
19
  import { join as pathJoin, resolve as pathResolve, relative } from 'node:path';
@@ -25,40 +27,14 @@ import { loadConfig } from '../config-loader.js';
25
27
  * `dist/static/_then/pages` grew with every incremental build and the dead
26
28
  * copies shipped to whatever the project deployed to.
27
29
  *
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.
30
+ * The sweep itself now lives in core as `pruneStaleOutputs`, because the same
31
+ * accretion happens to `dist/server`, `dist/functions` and each adapter's own
32
+ * output directory, and four copies of a recursive delete is four chances to
33
+ * fix one and not the others. This wrapper is the client-bundle name and the
34
+ * client-bundle reason; the behaviour is shared.
31
35
  */
32
36
  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;
37
+ return pruneStaleOutputs(dir, keep, fs);
62
38
  }
63
39
  // ---------------------------------------------------------------------------
64
40
  // Deploy template strings — inlined so they survive tsc compilation (tsc does
@@ -343,7 +319,7 @@ export async function buildCommand(_args) {
343
319
  //
344
320
  // Page and layout bundling now happens once, in core. `serverEsmResolvePlugin`
345
321
  // below is still used for the standalone entry.
346
- // 3c. Bundle browser entries for client and hybrid pages.
322
+ // 2. Bundle browser entries for client and hybrid pages.
347
323
  // Each bundle is a generated wrapper (generateClientPageEntry) that imports
348
324
  // the page module and calls mount() (client) / hydrate() (hybrid) — bundling
349
325
  // the raw page module alone leaves the shell at "Loading..." forever because
@@ -382,6 +358,7 @@ export async function buildCommand(_args) {
382
358
  outfile: outPath,
383
359
  jsx: 'automatic',
384
360
  jsxImportSource,
361
+ absWorkingDir: root,
385
362
  // Browser-resolve first: a page that imports `@celsian/vura-core` for
386
363
  // useLoaderData must get the pure client module here, not the package
387
364
  // root, which reaches node:fs and cannot be bundled for a browser.
@@ -420,15 +397,7 @@ export async function buildCommand(_args) {
420
397
  console.log(` ✓ removed ${pruned} stale bundle${pruned === 1 ? '' : 's'} from earlier builds`);
421
398
  }
422
399
  }
423
- // 4. Build API routes + task entries
424
- console.log(' Building...');
425
- const result = await build(manifest, config, root);
426
- console.log(` Server entry: ${result.serverEntry}`);
427
- console.log(` Functions: ${result.functions.length} serverless bundles`);
428
- if (result.taskEntries.length > 0) {
429
- console.log(` Tasks: ${result.taskEntries.length} task entries`);
430
- }
431
- // 5. Render build-time pages (static, client shells, and hybrid prerendered HTML)
400
+ // 3. Render build-time pages (static, client shells, and hybrid prerendered HTML)
432
401
  const staticPages = manifest.pages.filter(p => p.mode !== 'server');
433
402
  if (staticPages.length > 0) {
434
403
  console.log(` Rendering ${staticPages.length} build-time pages...`);
@@ -468,6 +437,12 @@ export async function buildCommand(_args) {
468
437
  outfile: tmpFile,
469
438
  jsx: 'automatic',
470
439
  jsxImportSource,
440
+ // esbuild anchors resolution to the working directory its service
441
+ // captured, which is not necessarily the cwd at call time. Core's
442
+ // bundlers pass this for the same reason; this one did not, and a
443
+ // second build in one process resolved against the first build's
444
+ // directory — which fails outright once that directory is gone.
445
+ absWorkingDir: root,
471
446
  plugins: [serverEsmResolvePlugin],
472
447
  external: sharedRuntimeExternals,
473
448
  });
@@ -482,7 +457,7 @@ export async function buildCommand(_args) {
482
457
  const { rm } = await import('node:fs/promises');
483
458
  await rm(tmpDir, { recursive: true, force: true }).catch(() => { });
484
459
  }
485
- // 6. Copy public assets to dist/public/ for production static serving.
460
+ // 4. Copy public assets to dist/public/ for production static serving.
486
461
  // Prefer the framework-standard root public/ directory, but support the
487
462
  // historical starter/runbook layout src/public/ when root public/ is absent.
488
463
  const publicDir = join(root, 'public');
@@ -499,10 +474,25 @@ export async function buildCommand(_args) {
499
474
  const label = publicSourceDir === publicDir ? 'public/' : 'src/public/';
500
475
  console.log(` Copied ${label} → dist/public/`);
501
476
  }
477
+ // 5. Build API routes, task entries and the adapter artifacts.
478
+ //
479
+ // This runs AFTER the page render and the public copy, not before. The
480
+ // adapter's buildEnd is the last step by design — this file's own header has
481
+ // said so since it was written — and an adapter that reads dist/static, which
482
+ // is what serving prerendered pages on Cloudflare and Lambda requires, saw
483
+ // either nothing on a clean build or the PREVIOUS build's HTML on a dirty
484
+ // one. It ran fourth of nine only because the page render was added later.
485
+ console.log(' Building...');
486
+ const result = await build(manifest, config, root);
487
+ console.log(` Server entry: ${result.serverEntry}`);
488
+ console.log(` Functions: ${result.functions.length} serverless bundles`);
489
+ if (result.taskEntries.length > 0) {
490
+ console.log(` Tasks: ${result.taskEntries.length} task entries`);
491
+ }
502
492
  if (config.adapter) {
503
493
  console.log(` Adapter: ${config.adapter.name}`);
504
494
  }
505
- // 9. Emit hot deploy templates when the project has hot routes
495
+ // 6. Emit hot deploy templates when the project has hot routes
506
496
  const hotRoutes = manifest.api.filter(r => r.kind === 'hot');
507
497
  const hasWsRoutes = hotRoutes.some(r => r.hasWebsocket === true);
508
498
  const distDir = join(root, 'dist');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celsian/vura-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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.8.0",
18
+ "@celsian/vura-core": "0.8.1",
19
19
  "esbuild": "^0.28.2",
20
- "what-framework": "^0.13.4"
20
+ "what-framework": "^0.13.6"
21
21
  },
22
22
  "peerDependencies": {
23
- "@celsian/vura-adapter-vura": "0.8.0",
23
+ "@celsian/vura-adapter-vura": "0.8.1",
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.8.0",
35
+ "@celsian/vura-adapter-vura": "0.8.1",
36
36
  "@types/ws": "^8.18.1",
37
37
  "ws": "^8.21.3"
38
38
  },