@celsian/vura-cli 0.5.14 → 0.6.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.
@@ -62,21 +62,32 @@ kill_timeout = "30s"
62
62
  const nativeImport = (specifier) => import(/* @vite-ignore */ specifier);
63
63
  const moduleSourceToDataUrl = (source) => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
64
64
  /**
65
- * Emit Dockerfile and fly.toml into dist/ when the project has hot routes.
66
- * Also writes/merges dist/package.json so that `npm install --omit=dev` in the
67
- * Docker build pulls the right runtime deps (ws when WS routes are present).
65
+ * Write dist/package.json.
66
+ *
67
+ * `npm install --omit=dev` inside the Docker build resolves against this file,
68
+ * so it has to list every bare specifier the emitted bundles still import at
69
+ * runtime. Two do:
70
+ *
71
+ * - `what-framework`, because API and page route modules are bundled with it
72
+ * kept external (see bundleRouteModule's `keepWhatFwExternal`). Without the
73
+ * dependency the container starts and dies on the first request to any API
74
+ * route with `ERR_MODULE_NOT_FOUND: what-framework/server`. It was only ever
75
+ * absent from this file, never from the imports.
76
+ * - `ws`, when the project has WebSocket routes.
77
+ *
78
+ * It is pinned to the version the project actually resolved, so the container
79
+ * runs the What the app was built and tested against rather than whatever
80
+ * `latest` is on deploy day.
81
+ *
82
+ * This runs for EVERY build. It used to run only for projects with hot routes,
83
+ * which is unrelated to whether the bundles import anything.
68
84
  */
69
- async function emitHotDeployTemplates(distDir, appName, hasWsRoutes) {
85
+ async function emitDeployPackageJson(distDir, projectRoot, hasWsRoutes) {
70
86
  const { writeFile, readFile, mkdir } = await import('node:fs/promises');
71
87
  const { existsSync } = await import('node:fs');
72
88
  const { join } = await import('node:path');
89
+ const { createRequire } = await import('node:module');
73
90
  await mkdir(distDir, { recursive: true });
74
- // Dockerfile
75
- await writeFile(join(distDir, 'Dockerfile'), DOCKERFILE_HOT, 'utf8');
76
- // fly.toml — replace {{APP_NAME}} placeholder
77
- const flyToml = FLY_TOML_TMPL.replace('{{APP_NAME}}', appName);
78
- await writeFile(join(distDir, 'fly.toml'), flyToml, 'utf8');
79
- // dist/package.json — create or merge
80
91
  const pkgPath = join(distDir, 'package.json');
81
92
  let existing = {};
82
93
  if (existsSync(pkgPath)) {
@@ -87,13 +98,46 @@ async function emitHotDeployTemplates(distDir, appName, hasWsRoutes) {
87
98
  console.warn(' Warning: dist/package.json is malformed JSON — regenerating from scratch.');
88
99
  }
89
100
  }
90
- const merged = { ...existing, type: 'module' };
91
- if (hasWsRoutes) {
92
- const existingDeps = existing.dependencies ?? {};
93
- merged.dependencies = { ...existingDeps, ws: '8.18.0' };
101
+ const deps = { ...(existing.dependencies ?? {}) };
102
+ const whatVersion = resolveWhatFrameworkVersion(createRequire(join(projectRoot, 'package.json')));
103
+ if (whatVersion) {
104
+ deps['what-framework'] = whatVersion;
105
+ }
106
+ else {
107
+ console.warn(' Warning: could not resolve what-framework in this project, so dist/package.json ' +
108
+ 'does not declare it. A container build will fail to resolve `what-framework/server` ' +
109
+ 'from the emitted API route bundles.');
94
110
  }
111
+ if (hasWsRoutes)
112
+ deps.ws = '8.18.0';
113
+ const merged = { ...existing, type: 'module' };
114
+ if (Object.keys(deps).length > 0)
115
+ merged.dependencies = deps;
95
116
  await writeFile(pkgPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
96
117
  }
118
+ /** The exact what-framework version installed in the project, or null. */
119
+ function resolveWhatFrameworkVersion(projectRequire) {
120
+ try {
121
+ const manifest = projectRequire('what-framework/package.json');
122
+ return typeof manifest.version === 'string' ? manifest.version : null;
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Emit Dockerfile and fly.toml into dist/ when the project has hot routes.
130
+ */
131
+ async function emitHotDeployTemplates(distDir, appName) {
132
+ const { writeFile, mkdir } = await import('node:fs/promises');
133
+ const { join } = await import('node:path');
134
+ await mkdir(distDir, { recursive: true });
135
+ // Dockerfile
136
+ await writeFile(join(distDir, 'Dockerfile'), DOCKERFILE_HOT, 'utf8');
137
+ // fly.toml — replace {{APP_NAME}} placeholder
138
+ const flyToml = FLY_TOML_TMPL.replace('{{APP_NAME}}', appName);
139
+ await writeFile(join(distDir, 'fly.toml'), flyToml, 'utf8');
140
+ }
97
141
  export async function buildCommand(_args) {
98
142
  const startTime = Date.now();
99
143
  const projectRoot = process.cwd();
@@ -113,8 +157,9 @@ export async function buildCommand(_args) {
113
157
  // Shared esbuild helpers
114
158
  const { build: esbuild } = await import('esbuild');
115
159
  const { join, resolve } = await import('node:path');
116
- const { mkdir } = await import('node:fs/promises');
160
+ const { mkdir, readFile, rename } = await import('node:fs/promises');
117
161
  const { existsSync } = await import('node:fs');
162
+ const { createHash } = await import('node:crypto');
118
163
  const cliRequire = createRequire(import.meta.url);
119
164
  const projectRequire = createRequire(join(root, 'package.json'));
120
165
  // Determine JSX import source from the user's project, not from the CLI's own
@@ -285,7 +330,13 @@ export async function buildCommand(_args) {
285
330
  plugins: [esmResolvePlugin],
286
331
  external: [],
287
332
  });
288
- const scriptPath = `/_then/pages/${outFile.replace(/\\/g, '/')}`;
333
+ const bundleHash = createHash('sha256')
334
+ .update(await readFile(outPath))
335
+ .digest('hex')
336
+ .slice(0, 12);
337
+ const hashedOutFile = outFile.replace(/\.js$/, `.${bundleHash}.js`);
338
+ await rename(outPath, join(clientPagesDir, hashedOutFile));
339
+ const scriptPath = `/_then/pages/${hashedOutFile.replace(/\\/g, '/')}`;
289
340
  clientScripts[page.filePath] = scriptPath;
290
341
  console.log(` ◇ ${page.urlPattern} → dist/static${scriptPath}`);
291
342
  }
@@ -353,15 +404,18 @@ export async function buildCommand(_args) {
353
404
  }
354
405
  // 9. Emit hot deploy templates when the project has hot routes
355
406
  const hotRoutes = manifest.api.filter(r => r.kind === 'hot');
407
+ const hasWsRoutes = hotRoutes.some(r => r.hasWebsocket === true);
408
+ const distDir = join(root, 'dist');
409
+ // Always: the emitted bundles import `what-framework` at runtime whether or
410
+ // not the project has hot routes.
411
+ await emitDeployPackageJson(distDir, root, hasWsRoutes);
356
412
  if (hotRoutes.length > 0) {
357
413
  const { basename } = await import('node:path');
358
414
  const rawName = basename(root);
359
415
  // sanitize to lowercase [a-z0-9-], truncate to Fly's ~30-char DNS label limit,
360
416
  // then strip any trailing dashes introduced by truncation
361
417
  const appName = rawName.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 30).replace(/-+$/, '') || 'vura-app';
362
- const hasWsRoutes = hotRoutes.some(r => r.hasWebsocket === true);
363
- const distDir = join(root, 'dist');
364
- await emitHotDeployTemplates(distDir, appName, hasWsRoutes);
418
+ await emitHotDeployTemplates(distDir, appName);
365
419
  console.log(` Emitted dist/Dockerfile, dist/fly.toml, dist/package.json (app: ${appName}${hasWsRoutes ? ', ws: true' : ''})`);
366
420
  }
367
421
  const elapsed = Date.now() - startTime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celsian/vura-cli",
3
- "version": "0.5.14",
3
+ "version": "0.6.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.5.14",
18
+ "@celsian/vura-core": "0.6.0",
19
19
  "esbuild": "^0.28.1",
20
- "what-framework": "^0.11.1"
20
+ "what-framework": "^0.13.2"
21
21
  },
22
22
  "peerDependencies": {
23
- "@celsian/vura-adapter-vura": "0.5.14",
23
+ "@celsian/vura-adapter-vura": "0.6.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.5.14",
35
+ "@celsian/vura-adapter-vura": "0.6.0",
36
36
  "@types/ws": "^8.18.1",
37
37
  "ws": "^8.21.0"
38
38
  },