@celsian/vura-cli 0.5.14 → 0.6.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/dist/commands/build.js +127 -29
- package/dist/commands/dev.js +4 -1
- package/package.json +5 -5
package/dist/commands/build.js
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
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 } from '@celsian/vura-core';
|
|
14
|
+
import { buildManifest, build, renderStaticPages, generateClientPageEntry, vuraBrowserResolvePlugin } from '@celsian/vura-core';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
|
-
import {
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join as pathJoin, resolve as pathResolve } from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
17
19
|
import { loadConfig } from '../config-loader.js';
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// Deploy template strings — inlined so they survive tsc compilation (tsc does
|
|
@@ -60,23 +62,32 @@ kill_timeout = "30s"
|
|
|
60
62
|
min_machines_running = 1
|
|
61
63
|
`;
|
|
62
64
|
const nativeImport = (specifier) => import(/* @vite-ignore */ specifier);
|
|
63
|
-
const moduleSourceToDataUrl = (source) => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
|
|
64
65
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* Write dist/package.json.
|
|
67
|
+
*
|
|
68
|
+
* `npm install --omit=dev` inside the Docker build resolves against this file,
|
|
69
|
+
* so it has to list every bare specifier the emitted bundles still import at
|
|
70
|
+
* runtime. Two do:
|
|
71
|
+
*
|
|
72
|
+
* - `what-framework`, because API and page route modules are bundled with it
|
|
73
|
+
* kept external (see bundleRouteModule's `keepWhatFwExternal`). Without the
|
|
74
|
+
* dependency the container starts and dies on the first request to any API
|
|
75
|
+
* route with `ERR_MODULE_NOT_FOUND: what-framework/server`. It was only ever
|
|
76
|
+
* absent from this file, never from the imports.
|
|
77
|
+
* - `ws`, when the project has WebSocket routes.
|
|
78
|
+
*
|
|
79
|
+
* It is pinned to the version the project actually resolved, so the container
|
|
80
|
+
* runs the What the app was built and tested against rather than whatever
|
|
81
|
+
* `latest` is on deploy day.
|
|
82
|
+
*
|
|
83
|
+
* This runs for EVERY build. It used to run only for projects with hot routes,
|
|
84
|
+
* which is unrelated to whether the bundles import anything.
|
|
68
85
|
*/
|
|
69
|
-
async function
|
|
86
|
+
async function emitDeployPackageJson(distDir, projectRoot, hasWsRoutes) {
|
|
70
87
|
const { writeFile, readFile, mkdir } = await import('node:fs/promises');
|
|
71
88
|
const { existsSync } = await import('node:fs');
|
|
72
89
|
const { join } = await import('node:path');
|
|
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,66 @@ 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(projectRoot);
|
|
103
|
+
if (whatVersion) {
|
|
104
|
+
deps['what-framework'] = whatVersion;
|
|
94
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.');
|
|
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(projectRoot) {
|
|
120
|
+
// Read the installed package.json off disk instead of resolving the
|
|
121
|
+
// specifier. `require('what-framework/package.json')` looks like the obvious
|
|
122
|
+
// way to do this and cannot work: what-framework's `exports` map lists `.`,
|
|
123
|
+
// `./server`, `./router` and friends, and Node refuses any subpath an
|
|
124
|
+
// exports map does not name — including './package.json'. Every real install
|
|
125
|
+
// therefore threw ERR_PACKAGE_PATH_NOT_EXPORTED, the version came back null,
|
|
126
|
+
// and dist/package.json shipped without the dependency the container needs.
|
|
127
|
+
let dir = projectRoot;
|
|
128
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
129
|
+
const candidate = pathJoin(dir, 'node_modules', 'what-framework', 'package.json');
|
|
130
|
+
if (existsSync(candidate)) {
|
|
131
|
+
try {
|
|
132
|
+
const manifest = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
133
|
+
if (typeof manifest.version === 'string')
|
|
134
|
+
return manifest.version;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const parent = pathResolve(dir, '..');
|
|
142
|
+
if (parent === dir)
|
|
143
|
+
break;
|
|
144
|
+
dir = parent;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Emit Dockerfile and fly.toml into dist/ when the project has hot routes.
|
|
150
|
+
*/
|
|
151
|
+
async function emitHotDeployTemplates(distDir, appName) {
|
|
152
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
153
|
+
const { join } = await import('node:path');
|
|
154
|
+
await mkdir(distDir, { recursive: true });
|
|
155
|
+
// Dockerfile
|
|
156
|
+
await writeFile(join(distDir, 'Dockerfile'), DOCKERFILE_HOT, 'utf8');
|
|
157
|
+
// fly.toml — replace {{APP_NAME}} placeholder
|
|
158
|
+
const flyToml = FLY_TOML_TMPL.replace('{{APP_NAME}}', appName);
|
|
159
|
+
await writeFile(join(distDir, 'fly.toml'), flyToml, 'utf8');
|
|
160
|
+
}
|
|
97
161
|
export async function buildCommand(_args) {
|
|
98
162
|
const startTime = Date.now();
|
|
99
163
|
const projectRoot = process.cwd();
|
|
@@ -113,8 +177,9 @@ export async function buildCommand(_args) {
|
|
|
113
177
|
// Shared esbuild helpers
|
|
114
178
|
const { build: esbuild } = await import('esbuild');
|
|
115
179
|
const { join, resolve } = await import('node:path');
|
|
116
|
-
const { mkdir } = await import('node:fs/promises');
|
|
180
|
+
const { mkdir, readFile, rename } = await import('node:fs/promises');
|
|
117
181
|
const { existsSync } = await import('node:fs');
|
|
182
|
+
const { createHash } = await import('node:crypto');
|
|
118
183
|
const cliRequire = createRequire(import.meta.url);
|
|
119
184
|
const projectRequire = createRequire(join(root, 'package.json'));
|
|
120
185
|
// Determine JSX import source from the user's project, not from the CLI's own
|
|
@@ -282,10 +347,19 @@ export async function buildCommand(_args) {
|
|
|
282
347
|
outfile: outPath,
|
|
283
348
|
jsx: 'automatic',
|
|
284
349
|
jsxImportSource,
|
|
285
|
-
|
|
350
|
+
// Browser-resolve first: a page that imports `@celsian/vura-core` for
|
|
351
|
+
// useLoaderData must get the pure client module here, not the package
|
|
352
|
+
// root, which reaches node:fs and cannot be bundled for a browser.
|
|
353
|
+
plugins: [vuraBrowserResolvePlugin(), esmResolvePlugin],
|
|
286
354
|
external: [],
|
|
287
355
|
});
|
|
288
|
-
const
|
|
356
|
+
const bundleHash = createHash('sha256')
|
|
357
|
+
.update(await readFile(outPath))
|
|
358
|
+
.digest('hex')
|
|
359
|
+
.slice(0, 12);
|
|
360
|
+
const hashedOutFile = outFile.replace(/\.js$/, `.${bundleHash}.js`);
|
|
361
|
+
await rename(outPath, join(clientPagesDir, hashedOutFile));
|
|
362
|
+
const scriptPath = `/_then/pages/${hashedOutFile.replace(/\\/g, '/')}`;
|
|
289
363
|
clientScripts[page.filePath] = scriptPath;
|
|
290
364
|
console.log(` ◇ ${page.urlPattern} → dist/static${scriptPath}`);
|
|
291
365
|
}
|
|
@@ -304,23 +378,44 @@ export async function buildCommand(_args) {
|
|
|
304
378
|
console.log(` Rendering ${staticPages.length} build-time pages...`);
|
|
305
379
|
const tmpDir = join(root, 'dist', '.page-tmp');
|
|
306
380
|
await mkdir(tmpDir, { recursive: true });
|
|
381
|
+
// A build-time page is imported into THIS process and rendered by core's
|
|
382
|
+
// own `renderToString`. Two rules follow, and breaking either one produces
|
|
383
|
+
// a failure that looks like a framework bug rather than a bundling one:
|
|
384
|
+
//
|
|
385
|
+
// 1. `what-framework` and `@celsian/vura-core` stay external. Inlining
|
|
386
|
+
// them gives the page a second copy of the framework, with its own
|
|
387
|
+
// "currently rendering component" and its own context registry — so
|
|
388
|
+
// every hook the page calls, `useLoaderData` included, reads a
|
|
389
|
+
// registry that the renderer never wrote to and reports being called
|
|
390
|
+
// outside a render.
|
|
391
|
+
// 2. The bundle is written to a real file rather than imported as a
|
|
392
|
+
// `data:` URL. A data: module has no parent path, so it cannot resolve
|
|
393
|
+
// the bare specifiers rule 1 just created, and anything it does inline
|
|
394
|
+
// that calls `fileURLToPath(import.meta.url)` at module scope throws
|
|
395
|
+
// "The URL must be of scheme file".
|
|
396
|
+
const sharedRuntimeExternals = [
|
|
397
|
+
'what-framework',
|
|
398
|
+
'what-framework/*',
|
|
399
|
+
'@celsian/vura-core',
|
|
400
|
+
'@celsian/vura-core/*',
|
|
401
|
+
];
|
|
402
|
+
let pageModuleSeq = 0;
|
|
307
403
|
const loadModule = async (filePath) => {
|
|
308
404
|
const absPath = resolve(root, filePath);
|
|
309
|
-
const
|
|
405
|
+
const tmpFile = join(tmpDir, `page-${pageModuleSeq++}.mjs`);
|
|
406
|
+
await esbuild({
|
|
310
407
|
entryPoints: [absPath],
|
|
311
408
|
bundle: true,
|
|
312
409
|
format: 'esm',
|
|
313
410
|
target: 'es2022',
|
|
314
411
|
platform: 'node',
|
|
315
|
-
|
|
316
|
-
outfile: 'page.mjs',
|
|
412
|
+
outfile: tmpFile,
|
|
317
413
|
jsx: 'automatic',
|
|
318
414
|
jsxImportSource,
|
|
319
415
|
plugins: [esmResolvePlugin],
|
|
320
|
-
external:
|
|
416
|
+
external: sharedRuntimeExternals,
|
|
321
417
|
});
|
|
322
|
-
|
|
323
|
-
return nativeImport(moduleSourceToDataUrl(bundledSource));
|
|
418
|
+
return nativeImport(pathToFileURL(tmpFile).href);
|
|
324
419
|
};
|
|
325
420
|
const outDir = join(root, 'dist');
|
|
326
421
|
const rendered = await renderStaticPages(staticPages, loadModule, outDir, { clientScripts });
|
|
@@ -353,15 +448,18 @@ export async function buildCommand(_args) {
|
|
|
353
448
|
}
|
|
354
449
|
// 9. Emit hot deploy templates when the project has hot routes
|
|
355
450
|
const hotRoutes = manifest.api.filter(r => r.kind === 'hot');
|
|
451
|
+
const hasWsRoutes = hotRoutes.some(r => r.hasWebsocket === true);
|
|
452
|
+
const distDir = join(root, 'dist');
|
|
453
|
+
// Always: the emitted bundles import `what-framework` at runtime whether or
|
|
454
|
+
// not the project has hot routes.
|
|
455
|
+
await emitDeployPackageJson(distDir, root, hasWsRoutes);
|
|
356
456
|
if (hotRoutes.length > 0) {
|
|
357
457
|
const { basename } = await import('node:path');
|
|
358
458
|
const rawName = basename(root);
|
|
359
459
|
// sanitize to lowercase [a-z0-9-], truncate to Fly's ~30-char DNS label limit,
|
|
360
460
|
// then strip any trailing dashes introduced by truncation
|
|
361
461
|
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);
|
|
462
|
+
await emitHotDeployTemplates(distDir, appName);
|
|
365
463
|
console.log(` Emitted dist/Dockerfile, dist/fly.toml, dist/package.json (app: ${appName}${hasWsRoutes ? ', ws: true' : ''})`);
|
|
366
464
|
}
|
|
367
465
|
const elapsed = Date.now() - startTime;
|
package/dist/commands/dev.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { renderToString as builtinRenderToString } from 'what-framework/server';
|
|
16
16
|
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, } from '@celsian/vura-core';
|
|
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';
|
|
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');
|
|
@@ -276,6 +276,9 @@ export async function startStandaloneServer(manifest, opts) {
|
|
|
276
276
|
outfile: 'page.js',
|
|
277
277
|
jsx: 'automatic',
|
|
278
278
|
jsxImportSource,
|
|
279
|
+
// Same redirect the production build applies: `@celsian/vura-core` in a
|
|
280
|
+
// browser bundle resolves to the pure client module.
|
|
281
|
+
plugins: [vuraBrowserResolvePlugin()],
|
|
279
282
|
nodePaths: [join(opts.projectRoot, 'node_modules')],
|
|
280
283
|
});
|
|
281
284
|
const text = result.outputFiles[0].text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celsian/vura-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.
|
|
18
|
+
"@celsian/vura-core": "0.6.1",
|
|
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.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.
|
|
35
|
+
"@celsian/vura-adapter-vura": "0.6.1",
|
|
36
36
|
"@types/ws": "^8.18.1",
|
|
37
37
|
"ws": "^8.21.0"
|
|
38
38
|
},
|