@zenithbuild/cli 0.7.10 → 0.7.11

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.
Files changed (37) hide show
  1. package/README.md +4 -1
  2. package/dist/build/compiler-runtime.js +3 -0
  3. package/dist/build/page-loop-state.d.ts +1 -4
  4. package/dist/build/page-loop-state.js +10 -9
  5. package/dist/build/page-loop.js +8 -7
  6. package/dist/build/server-script.js +13 -36
  7. package/dist/build/type-declarations.js +1 -54
  8. package/dist/dev-build-session/helpers.js +27 -7
  9. package/dist/dev-build-session/session.js +19 -10
  10. package/dist/dev-server/build-error-response.d.ts +21 -0
  11. package/dist/dev-server/build-error-response.js +48 -0
  12. package/dist/dev-server/port-fallback.d.ts +15 -0
  13. package/dist/dev-server/port-fallback.js +61 -0
  14. package/dist/dev-server/request-handler.js +12 -1
  15. package/dist/dev-server/watcher.js +15 -0
  16. package/dist/dev-server.d.ts +5 -2
  17. package/dist/dev-server.js +37 -45
  18. package/dist/images/remote-fetch.d.ts +12 -0
  19. package/dist/images/remote-fetch.js +257 -0
  20. package/dist/images/service.d.ts +10 -0
  21. package/dist/images/service.js +9 -46
  22. package/dist/index.js +12 -2
  23. package/dist/manifest.js +6 -1
  24. package/dist/resource-response.js +25 -8
  25. package/dist/resource-route-module.js +5 -22
  26. package/dist/route-classification.d.ts +10 -0
  27. package/dist/route-classification.js +17 -0
  28. package/dist/route-handler-export-analysis.d.ts +22 -0
  29. package/dist/route-handler-export-analysis.js +41 -0
  30. package/dist/server-output.js +6 -16
  31. package/dist/server-route-names.d.ts +2 -0
  32. package/dist/server-route-names.js +38 -0
  33. package/dist/server-runtime/node-server.js +5 -2
  34. package/dist/types/generate-env-dts.js +2 -44
  35. package/dist/types/zenith-env-dts.d.ts +4 -0
  36. package/dist/types/zenith-env-dts.js +96 -0
  37. package/package.json +3 -6
@@ -1,27 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { relative, sep } from 'node:path';
3
+ import { readRouteHandlerExport } from './route-handler-export-analysis.js';
3
4
  const RESOURCE_EXTENSIONS = ['.resource.ts', '.resource.js', '.resource.mts', '.resource.cts', '.resource.mjs', '.resource.cjs'];
4
5
  const FORBIDDEN_RESOURCE_EXPORT_RE = /\bexport\s+const\s+(?:data|prerender|exportPaths|ssr_data|props|ssr)\b/;
5
- function readSingleExport(source, name) {
6
- const fnMatch = source.match(new RegExp(`\\bexport\\s+(?:async\\s+)?function\\s+${name}\\s*\\(([^)]*)\\)`));
7
- const constParenMatch = source.match(new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*(?:async\\s*)?\\(([^)]*)\\)\\s*=>`));
8
- const constSingleArgMatch = source.match(new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*(?:async\\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=>`));
9
- const matchCount = Number(Boolean(fnMatch)) +
10
- Number(Boolean(constParenMatch)) +
11
- Number(Boolean(constSingleArgMatch));
12
- return {
13
- fnMatch,
14
- constParenMatch,
15
- constSingleArgMatch,
16
- hasExport: matchCount > 0,
17
- matchCount
18
- };
19
- }
20
6
  function assertSingleCtxArg(sourceFile, name, exportMatch) {
21
- const singleArg = String(exportMatch.constSingleArgMatch?.[1] || '').trim();
22
- const paramsText = String((exportMatch.fnMatch || exportMatch.constParenMatch)?.[1] || '').trim();
23
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
24
- if (arity !== 1) {
7
+ if (exportMatch.arity !== null && exportMatch.arity !== 1) {
25
8
  throw new Error(`Zenith resource route contract violation:\n` +
26
9
  ` File: ${sourceFile}\n` +
27
10
  ` Reason: ${name}(ctx) must accept exactly one argument\n` +
@@ -80,9 +63,9 @@ export function analyzeResourceRouteModule(fullPath, root) {
80
63
  ` Reason: resource routes may only export guard(ctx), load(ctx), and action(ctx)\n` +
81
64
  ` Example: remove page-only exports such as data/prerender/exportPaths`);
82
65
  }
83
- const guardExport = readSingleExport(source, 'guard');
84
- const loadExport = readSingleExport(source, 'load');
85
- const actionExport = readSingleExport(source, 'action');
66
+ const guardExport = readRouteHandlerExport(source, 'guard');
67
+ const loadExport = readRouteHandlerExport(source, 'load');
68
+ const actionExport = readRouteHandlerExport(source, 'action');
86
69
  for (const [name, exportMatch] of [
87
70
  ['guard', guardExport],
88
71
  ['load', loadExport],
@@ -0,0 +1,10 @@
1
+ export function classifyPageRoute({ file, serverScript }: {
2
+ file: any;
3
+ serverScript: any;
4
+ }): {
5
+ prerender: boolean;
6
+ renderMode: string;
7
+ hasGuard: boolean;
8
+ hasLoad: boolean;
9
+ hasAction: boolean;
10
+ };
@@ -0,0 +1,17 @@
1
+ export function classifyPageRoute({ file, serverScript }) {
2
+ const hasGuard = serverScript?.has_guard === true;
3
+ const hasLoad = serverScript?.has_load === true;
4
+ const hasAction = serverScript?.has_action === true;
5
+ const prerender = serverScript?.prerender === true;
6
+ if (prerender && (hasGuard || hasLoad || hasAction)) {
7
+ throw new Error(`[zenith] Build failed for ${file}: protected routes require SSR/runtime. ` +
8
+ 'Cannot prerender a static route with a `guard`, `load`, or `action` function.');
9
+ }
10
+ return {
11
+ prerender,
12
+ renderMode: serverScript && !prerender ? 'server' : 'prerender',
13
+ hasGuard,
14
+ hasLoad,
15
+ hasAction
16
+ };
17
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @param {string} source
3
+ * @param {'guard' | 'load' | 'action'} name
4
+ * @returns {{
5
+ * fnMatch: RegExpMatchArray | null,
6
+ * constParenMatch: RegExpMatchArray | null,
7
+ * constSingleArgMatch: RegExpMatchArray | null,
8
+ * constMiddlewareMatch: RegExpMatchArray | null,
9
+ * hasExport: boolean,
10
+ * matchCount: number,
11
+ * arity: number | null
12
+ * }}
13
+ */
14
+ export function readRouteHandlerExport(source: string, name: "guard" | "load" | "action"): {
15
+ fnMatch: RegExpMatchArray | null;
16
+ constParenMatch: RegExpMatchArray | null;
17
+ constSingleArgMatch: RegExpMatchArray | null;
18
+ constMiddlewareMatch: RegExpMatchArray | null;
19
+ hasExport: boolean;
20
+ matchCount: number;
21
+ arity: number | null;
22
+ };
@@ -0,0 +1,41 @@
1
+ function routeHandlerExportPattern(name, tail) {
2
+ return new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*${tail}`);
3
+ }
4
+ /**
5
+ * @param {string} source
6
+ * @param {'guard' | 'load' | 'action'} name
7
+ * @returns {{
8
+ * fnMatch: RegExpMatchArray | null,
9
+ * constParenMatch: RegExpMatchArray | null,
10
+ * constSingleArgMatch: RegExpMatchArray | null,
11
+ * constMiddlewareMatch: RegExpMatchArray | null,
12
+ * hasExport: boolean,
13
+ * matchCount: number,
14
+ * arity: number | null
15
+ * }}
16
+ */
17
+ export function readRouteHandlerExport(source, name) {
18
+ const fnMatch = source.match(new RegExp(`\\bexport\\s+(?:async\\s+)?function\\s+${name}\\s*\\(([^)]*)\\)`));
19
+ const constParenMatch = source.match(routeHandlerExportPattern(name, '(?:async\\s*)?\\(([^)]*)\\)\\s*=>'));
20
+ const constSingleArgMatch = source.match(routeHandlerExportPattern(name, '(?:async\\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=>'));
21
+ const constMiddlewareMatch = source.match(routeHandlerExportPattern(name, 'withMiddleware\\s*\\('));
22
+ const matchCount = Number(Boolean(fnMatch)) +
23
+ Number(Boolean(constParenMatch)) +
24
+ Number(Boolean(constSingleArgMatch)) +
25
+ Number(Boolean(constMiddlewareMatch));
26
+ let arity = null;
27
+ if (!constMiddlewareMatch) {
28
+ const singleArg = String(constSingleArgMatch?.[1] || '').trim();
29
+ const paramsText = String((fnMatch || constParenMatch)?.[1] || '').trim();
30
+ arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
31
+ }
32
+ return {
33
+ fnMatch,
34
+ constParenMatch,
35
+ constSingleArgMatch,
36
+ constMiddlewareMatch,
37
+ hasExport: matchCount > 0,
38
+ matchCount,
39
+ arity
40
+ };
41
+ }
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
4
4
  import { basename, dirname, extname, join, relative, resolve } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { loadResourceRouteManifest } from './resource-manifest.js';
7
+ import { assignServerRouteNames } from './server-route-names.js';
7
8
  const PACKAGE_REQUIRE = createRequire(import.meta.url);
8
9
  const RELATIVE_SPECIFIER_RE = /((?:import|export)\s+(?:[^'"]*?\s+from\s+)?|import\s*\()\s*(['"])([^'"]+)\2/g;
9
10
  const SERVER_RUNTIME_FILES = [
@@ -44,6 +45,10 @@ const SERVER_RUNTIME_FILES = [
44
45
  from: new URL('./images/shared.js', import.meta.url),
45
46
  to: 'images/shared.js'
46
47
  },
48
+ {
49
+ from: new URL('./images/remote-fetch.js', import.meta.url),
50
+ to: 'images/remote-fetch.js'
51
+ },
47
52
  {
48
53
  from: new URL('./images/runtime.js', import.meta.url),
49
54
  to: 'images/runtime.js'
@@ -69,20 +74,6 @@ const SPECIAL_SERVER_SPECIFIERS = new Map([
69
74
  ['zenith:server-contract', 'server-contract.js'],
70
75
  ['zenith:route-auth', 'auth/route-auth.js']
71
76
  ]);
72
- function normalizeRouteName(routePath) {
73
- if (routePath === '/') {
74
- return 'index';
75
- }
76
- return routePath
77
- .replace(/^\//, '')
78
- .replace(/\/+/g, '_')
79
- .replace(/:/g, 'param_')
80
- .replace(/\*/g, 'splat_')
81
- .replace(/\?/g, 'opt')
82
- .replace(/[^a-zA-Z0-9_]/g, '_')
83
- .replace(/_+/g, '_')
84
- .replace(/^_+|_+$/g, '');
85
- }
86
77
  function resolveTypeScriptApi(projectRoot) {
87
78
  try {
88
79
  const projectRequire = createRequire(join(projectRoot, '__zenith_server_output_loader__.js'));
@@ -298,8 +289,7 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
298
289
  await copyRuntimeFiles(serverDir);
299
290
  const imageManifestSource = join(staticDir, '_zenith', 'image', 'manifest.json');
300
291
  const emittedRoutes = [];
301
- for (const route of serverRoutes) {
302
- const name = normalizeRouteName(route.path);
292
+ for (const { route, name } of assignServerRouteNames(serverRoutes)) {
303
293
  const routeDir = join(serverDir, 'routes', name);
304
294
  await mkdir(routeDir, { recursive: true });
305
295
  if (route.route_kind !== 'resource') {
@@ -0,0 +1,2 @@
1
+ export function normalizeRouteName(routePath: any): any;
2
+ export function assignServerRouteNames(routes: any): any;
@@ -0,0 +1,38 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function normalizeRouteName(routePath) {
3
+ if (routePath === '/') {
4
+ return 'index';
5
+ }
6
+ return routePath
7
+ .replace(/^\//, '')
8
+ .replace(/\/+/g, '_')
9
+ .replace(/:/g, 'param_')
10
+ .replace(/\*/g, 'splat_')
11
+ .replace(/\?/g, 'opt')
12
+ .replace(/[^a-zA-Z0-9_]/g, '_')
13
+ .replace(/_+/g, '_')
14
+ .replace(/^_+|_+$/g, '') || 'route';
15
+ }
16
+ function routeNameHash(route) {
17
+ return createHash('sha256')
18
+ .update(`${route.route_kind || 'page'}:${route.path || ''}`)
19
+ .digest('hex')
20
+ .slice(0, 8);
21
+ }
22
+ export function assignServerRouteNames(routes) {
23
+ const used = new Set();
24
+ return routes.map((route) => {
25
+ const baseName = normalizeRouteName(route.path);
26
+ let name = baseName;
27
+ if (used.has(name)) {
28
+ name = `${baseName}_${routeNameHash(route)}`;
29
+ }
30
+ let suffix = 2;
31
+ while (used.has(name)) {
32
+ name = `${baseName}_${routeNameHash(route)}_${suffix}`;
33
+ suffix += 1;
34
+ }
35
+ used.add(name);
36
+ return { route, name };
37
+ });
38
+ }
@@ -11,6 +11,7 @@ import { executeRouteRequest, renderResourceRouteRequest, renderRouteRequest } f
11
11
  import { resolveRequestRoute } from './resolve-request-route.js';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
14
+ const PUBLIC_ORIGIN_ENV = 'ZENITH_PUBLIC_ORIGIN';
14
15
  const MIME_TYPES = {
15
16
  '.html': 'text/html; charset=utf-8',
16
17
  '.js': 'application/javascript; charset=utf-8',
@@ -375,8 +376,9 @@ function createNodeRequestHandler(context, resolveServerOrigin) {
375
376
  }
376
377
  export async function createRequestHandler(options = {}) {
377
378
  const context = await loadRuntimeContext(options);
379
+ const publicOrigin = options.publicOrigin ?? process.env[PUBLIC_ORIGIN_ENV];
378
380
  const resolveServerOrigin = createTrustedOriginResolver({
379
- publicOrigin: options.publicOrigin,
381
+ publicOrigin,
380
382
  host: options.host || '127.0.0.1',
381
383
  port: Number.isInteger(options.port) ? options.port : undefined,
382
384
  label: 'createRequestHandler()'
@@ -388,8 +390,9 @@ export async function createNodeServer(options = {}) {
388
390
  const { port = 3000, host = '127.0.0.1' } = options;
389
391
  const context = await loadRuntimeContext(options);
390
392
  let actualPort = Number.isInteger(port) && port > 0 ? port : 0;
393
+ const publicOrigin = options.publicOrigin ?? process.env[PUBLIC_ORIGIN_ENV];
391
394
  const resolveServerOrigin = createTrustedOriginResolver({
392
- publicOrigin: options.publicOrigin,
395
+ publicOrigin,
393
396
  host,
394
397
  getPort: () => actualPort,
395
398
  label: 'createNodeServer()'
@@ -1,54 +1,12 @@
1
1
  import { writeFile, mkdir } from 'node:fs/promises';
2
2
  import { join, dirname } from 'node:path';
3
+ import { renderZenithEnvDts } from './zenith-env-dts.js';
3
4
  /**
4
5
  * @param {string} projectRoot
5
6
  * @returns {Promise<void>}
6
7
  */
7
8
  export async function generateEnvDts(projectRoot) {
8
- const content = `// .zenith/zenith-env.d.ts
9
- // Auto-generated by Zenith. Do not edit.
10
-
11
- export {};
12
-
13
- declare global {
14
- namespace Zenith {
15
- type Params = Record<string, string>;
16
-
17
- interface LoadContext {
18
- params: Params;
19
- url: URL;
20
- request: Request;
21
- route: {
22
- id: string;
23
- file: string;
24
- pattern: string;
25
- };
26
- }
27
-
28
- interface ErrorState {
29
- status?: number;
30
- code?: string;
31
- message: string;
32
- }
33
-
34
- type PageData = Record<string, unknown> & {
35
- __zenith_error?: ErrorState;
36
- };
37
-
38
- type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<T> | T;
39
-
40
- type Renderable =
41
- | string
42
- | number
43
- | boolean
44
- | null
45
- | undefined
46
- | Renderable[]
47
- | { __zenith_fragment: true };
48
- }
49
- }
50
- `;
51
9
  const outPath = join(projectRoot, '.zenith', 'zenith-env.d.ts');
52
10
  await mkdir(dirname(outPath), { recursive: true });
53
- await writeFile(outPath, content, 'utf8');
11
+ await writeFile(outPath, renderZenithEnvDts(), 'utf8');
54
12
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @returns {string}
3
+ */
4
+ export function renderZenithEnvDts(): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @returns {string}
3
+ */
4
+ export function renderZenithEnvDts() {
5
+ return [
6
+ '// Auto-generated by Zenith CLI. Do not edit manually.',
7
+ 'export {};',
8
+ '',
9
+ 'declare global {',
10
+ ' namespace Zenith {',
11
+ ' type Params = Record<string, string>;',
12
+ '',
13
+ ' interface ErrorState {',
14
+ ' status?: number;',
15
+ ' code?: string;',
16
+ ' message: string;',
17
+ ' }',
18
+ '',
19
+ ' type PageData = Record<string, unknown> & { __zenith_error?: ErrorState };',
20
+ '',
21
+ ' interface RouteMeta {',
22
+ ' id: string;',
23
+ ' file: string;',
24
+ ' pattern: string;',
25
+ ' }',
26
+ '',
27
+ ' type RouteSession = Record<string, unknown>;',
28
+ '',
29
+ ' type RequireSessionOptions =',
30
+ ' | { redirectTo: string; status?: 302 | 303 | 307 }',
31
+ ' | { deny: 401 | 403 | 404; message?: string };',
32
+ '',
33
+ ' type ActionState =',
34
+ ' | null',
35
+ ' | { ok: true; status: 200; data: unknown }',
36
+ ' | { ok: false; status: 400 | 422; data: unknown };',
37
+ '',
38
+ ' type AllowResult = { kind: "allow" };',
39
+ ' type RedirectResult = { kind: "redirect"; location: string; status: number };',
40
+ ' type DenyResult = { kind: "deny"; status: 401 | 403 | 404; message?: string };',
41
+ ' type DataResult<T extends PageData = PageData> = { kind: "data"; data: T };',
42
+ ' type InvalidResult<T = unknown> = { kind: "invalid"; data: T; status: 400 | 422 };',
43
+ ' type JsonResult<T = unknown> = { kind: "json"; data: T; status: number };',
44
+ ' type TextResult = { kind: "text"; body: string; status: number };',
45
+ ' type RouteDownloadBody = string | Uint8Array | ArrayBuffer;',
46
+ ' type RouteDownloadOptions = { filename: string; contentType?: string };',
47
+ ' type DownloadResult = { kind: "download"; body: string; bodyEncoding: "utf8" | "base64"; bodySize: number; filename: string; contentType: string; status: 200 };',
48
+ '',
49
+ ' interface LoadContext {',
50
+ ' params: Params;',
51
+ ' url: URL;',
52
+ ' headers: Record<string, string>;',
53
+ ' cookies: Record<string, string>;',
54
+ ' request: Request;',
55
+ ' method: string;',
56
+ ' route: RouteMeta;',
57
+ ' env: Record<string, unknown>;',
58
+ ' action: ActionState;',
59
+ ' auth: {',
60
+ ' getSession(): Promise<RouteSession | null>;',
61
+ ' requireSession(options: RequireSessionOptions): Promise<RouteSession>;',
62
+ ' signIn(sessionObject: RouteSession): Promise<void>;',
63
+ ' signOut(): Promise<void>;',
64
+ ' };',
65
+ ' allow(): AllowResult;',
66
+ ' redirect(location: string, status?: number): RedirectResult;',
67
+ ' deny(status: 401 | 403 | 404, message?: string): DenyResult;',
68
+ ' data<T extends PageData = PageData>(payload: T): DataResult<T>;',
69
+ ' invalid<T = unknown>(payload: T, status?: 400 | 422): InvalidResult<T>;',
70
+ ' json<T = unknown>(payload: T, status?: number): JsonResult<T>;',
71
+ ' text(body: string, status?: number): TextResult;',
72
+ ' download(body: RouteDownloadBody, options: RouteDownloadOptions): DownloadResult;',
73
+ ' }',
74
+ '',
75
+ ' type PageRouteResult<T extends PageData = PageData> = T | DataResult<T> | RedirectResult | DenyResult;',
76
+ ' type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<PageRouteResult<T>> | PageRouteResult<T>;',
77
+ '',
78
+ ' interface Fragment {',
79
+ ' __zenith_fragment: true;',
80
+ ' mount: (anchor: Node | null) => void;',
81
+ ' unmount: () => void;',
82
+ ' }',
83
+ '',
84
+ ' type Renderable =',
85
+ ' | string',
86
+ ' | number',
87
+ ' | boolean',
88
+ ' | null',
89
+ ' | undefined',
90
+ ' | Renderable[]',
91
+ ' | Fragment;',
92
+ ' }',
93
+ '}',
94
+ ''
95
+ ].join('\n');
96
+ }
package/package.json CHANGED
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
- "bin": {
9
- "zenith": "./dist/index.js"
10
- },
11
8
  "exports": {
12
9
  ".": "./dist/index.js"
13
10
  },
@@ -38,8 +35,8 @@
38
35
  "prepublishOnly": "npm run build"
39
36
  },
40
37
  "dependencies": {
41
- "@zenithbuild/compiler": "0.7.10",
42
- "@zenithbuild/bundler": "0.7.10",
38
+ "@zenithbuild/compiler": "0.7.11",
39
+ "@zenithbuild/bundler": "0.7.11",
43
40
  "picocolors": "^1.1.1",
44
41
  "sharp": "^0.34.4"
45
42
  },