@celsian/vura-cli 0.5.13 → 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.
package/dist/commands/build.js
CHANGED
|
@@ -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
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
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/dist/commands/deploy.js
CHANGED
|
@@ -13,10 +13,43 @@
|
|
|
13
13
|
* --api-url <url> API base URL (else VURA_API_URL, else https://api.vura.io)
|
|
14
14
|
* --project-id <id> Project id (else VURA_PROJECT_ID, else .vura/project.json)
|
|
15
15
|
*/
|
|
16
|
-
import { readFile } from 'node:fs/promises';
|
|
17
16
|
import { existsSync } from 'node:fs';
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { resolveApiUrl, resolveProjectId, resolveToken } from '../vura-client.js';
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function invalidManifest(manifestPath, detail) {
|
|
24
|
+
return new Error(`Build manifest at ${manifestPath} is invalid (${detail}). ` +
|
|
25
|
+
'Run `vura build` again before deploying.');
|
|
26
|
+
}
|
|
27
|
+
async function readDeployManifest(manifestPath) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw invalidManifest(manifestPath, 'malformed JSON');
|
|
34
|
+
}
|
|
35
|
+
if (!isRecord(parsed))
|
|
36
|
+
throw invalidManifest(manifestPath, 'expected a JSON object');
|
|
37
|
+
if (!Array.isArray(parsed.api))
|
|
38
|
+
throw invalidManifest(manifestPath, 'api must be an array');
|
|
39
|
+
if (!Array.isArray(parsed.pages))
|
|
40
|
+
throw invalidManifest(manifestPath, 'pages must be an array');
|
|
41
|
+
for (const [index, route] of parsed.api.entries()) {
|
|
42
|
+
if (!isRecord(route) || !['serverless', 'hot', 'task'].includes(String(route.kind))) {
|
|
43
|
+
throw invalidManifest(manifestPath, `api[${index}] must have a supported kind`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [index, page] of parsed.pages.entries()) {
|
|
47
|
+
if (!isRecord(page) || !['static', 'server', 'client', 'hybrid'].includes(String(page.mode))) {
|
|
48
|
+
throw invalidManifest(manifestPath, `pages[${index}] must have a supported mode`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
20
53
|
function parseFlags(args) {
|
|
21
54
|
const flags = { production: false };
|
|
22
55
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -70,16 +103,18 @@ export async function deployCommand(args) {
|
|
|
70
103
|
return;
|
|
71
104
|
}
|
|
72
105
|
const apiUrl = resolveApiUrl(flags.apiUrl);
|
|
73
|
-
//
|
|
74
|
-
// re-scanning the artifact.
|
|
106
|
+
// 4. Refuse stale/corrupt build output before loading or calling the adapter.
|
|
75
107
|
let manifest;
|
|
76
108
|
try {
|
|
77
|
-
manifest =
|
|
109
|
+
manifest = await readDeployManifest(manifestPath);
|
|
78
110
|
}
|
|
79
|
-
catch {
|
|
80
|
-
|
|
111
|
+
catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
console.error(` ${message}`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
81
116
|
}
|
|
82
|
-
//
|
|
117
|
+
// 5. Deploy via the adapter's independently validated shared flow.
|
|
83
118
|
let deployToVura;
|
|
84
119
|
try {
|
|
85
120
|
({ deployToVura } = await import('@celsian/vura-adapter-vura'));
|
|
@@ -52,7 +52,9 @@ function computeDetails(route) {
|
|
|
52
52
|
timeout,
|
|
53
53
|
providerRecommendation: 'serverless-function',
|
|
54
54
|
confidence: 'medium',
|
|
55
|
-
reasons: [
|
|
55
|
+
reasons: [memory && memory !== '1gb'
|
|
56
|
+
? `The route explicitly selects ${memory} of scale-to-zero Function memory.`
|
|
57
|
+
: 'Stateless endpoints and tasks default to scale-to-zero Function compute at 1gb.'],
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
function prefersHotTask(config) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celsian/vura-cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
18
|
+
"@celsian/vura-core": "0.6.0",
|
|
19
19
|
"esbuild": "^0.28.1",
|
|
20
|
-
"what-framework": "^0.
|
|
20
|
+
"what-framework": "^0.13.2"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@celsian/vura-adapter-vura": "0.
|
|
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.
|
|
35
|
+
"@celsian/vura-adapter-vura": "0.6.0",
|
|
36
36
|
"@types/ws": "^8.18.1",
|
|
37
37
|
"ws": "^8.21.0"
|
|
38
38
|
},
|