@celsian/vura-cli 0.6.0 → 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.
@@ -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 { Buffer } from 'node:buffer';
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,7 +62,6 @@ 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
  * Write dist/package.json.
66
67
  *
@@ -86,7 +87,6 @@ async function emitDeployPackageJson(distDir, projectRoot, hasWsRoutes) {
86
87
  const { writeFile, readFile, mkdir } = await import('node:fs/promises');
87
88
  const { existsSync } = await import('node:fs');
88
89
  const { join } = await import('node:path');
89
- const { createRequire } = await import('node:module');
90
90
  await mkdir(distDir, { recursive: true });
91
91
  const pkgPath = join(distDir, 'package.json');
92
92
  let existing = {};
@@ -99,7 +99,7 @@ async function emitDeployPackageJson(distDir, projectRoot, hasWsRoutes) {
99
99
  }
100
100
  }
101
101
  const deps = { ...(existing.dependencies ?? {}) };
102
- const whatVersion = resolveWhatFrameworkVersion(createRequire(join(projectRoot, 'package.json')));
102
+ const whatVersion = resolveWhatFrameworkVersion(projectRoot);
103
103
  if (whatVersion) {
104
104
  deps['what-framework'] = whatVersion;
105
105
  }
@@ -116,14 +116,34 @@ 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
- 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;
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;
126
145
  }
146
+ return null;
127
147
  }
128
148
  /**
129
149
  * Emit Dockerfile and fly.toml into dist/ when the project has hot routes.
@@ -327,7 +347,10 @@ export async function buildCommand(_args) {
327
347
  outfile: outPath,
328
348
  jsx: 'automatic',
329
349
  jsxImportSource,
330
- plugins: [esmResolvePlugin],
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],
331
354
  external: [],
332
355
  });
333
356
  const bundleHash = createHash('sha256')
@@ -355,23 +378,44 @@ export async function buildCommand(_args) {
355
378
  console.log(` Rendering ${staticPages.length} build-time pages...`);
356
379
  const tmpDir = join(root, 'dist', '.page-tmp');
357
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;
358
403
  const loadModule = async (filePath) => {
359
404
  const absPath = resolve(root, filePath);
360
- const result = await esbuild({
405
+ const tmpFile = join(tmpDir, `page-${pageModuleSeq++}.mjs`);
406
+ await esbuild({
361
407
  entryPoints: [absPath],
362
408
  bundle: true,
363
409
  format: 'esm',
364
410
  target: 'es2022',
365
411
  platform: 'node',
366
- write: false,
367
- outfile: 'page.mjs',
412
+ outfile: tmpFile,
368
413
  jsx: 'automatic',
369
414
  jsxImportSource,
370
415
  plugins: [esmResolvePlugin],
371
- external: [],
416
+ external: sharedRuntimeExternals,
372
417
  });
373
- const bundledSource = result.outputFiles[0].text;
374
- return nativeImport(moduleSourceToDataUrl(bundledSource));
418
+ return nativeImport(pathToFileURL(tmpFile).href);
375
419
  };
376
420
  const outDir = join(root, 'dist');
377
421
  const rendered = await renderStaticPages(staticPages, loadModule, outDir, { clientScripts });
@@ -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.6.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.6.0",
18
+ "@celsian/vura-core": "0.6.1",
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.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.6.0",
35
+ "@celsian/vura-adapter-vura": "0.6.1",
36
36
  "@types/ws": "^8.18.1",
37
37
  "ws": "^8.21.0"
38
38
  },