@zenithbuild/core 0.6.5 → 0.6.7
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/config.d.ts +15 -0
- package/dist/config.js +78 -0
- package/dist/core-template.d.ts +1 -0
- package/{src → dist}/core-template.js +0 -6
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +19 -0
- package/dist/guards.d.ts +5 -0
- package/dist/guards.js +24 -0
- package/dist/hash.d.ts +2 -0
- package/dist/hash.js +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/ir/index.js +1 -0
- package/dist/order.d.ts +6 -0
- package/dist/order.js +21 -0
- package/dist/path.d.ts +6 -0
- package/dist/path.js +53 -0
- package/dist/schema.d.ts +5 -0
- package/{src → dist}/schema.js +0 -5
- package/dist/version.d.ts +8 -0
- package/dist/version.js +27 -0
- package/package.json +39 -23
- package/src/config.js +0 -136
- package/src/errors.js +0 -54
- package/src/guards.js +0 -61
- package/src/hash.js +0 -52
- package/src/index.js +0 -26
- package/src/order.js +0 -69
- package/src/path.js +0 -131
- package/src/version.js +0 -67
- /package/{src/ir/index.js → dist/ir/index.d.ts} +0 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ZenithConfig {
|
|
2
|
+
router: boolean;
|
|
3
|
+
embeddedMarkupExpressions: boolean;
|
|
4
|
+
types: boolean;
|
|
5
|
+
typescriptDefault: boolean;
|
|
6
|
+
outDir: string;
|
|
7
|
+
pagesDir: string;
|
|
8
|
+
experimental: Record<string, never>;
|
|
9
|
+
strictDomLints: boolean;
|
|
10
|
+
}
|
|
11
|
+
type ConfigInput = Partial<ZenithConfig> | null | undefined;
|
|
12
|
+
export declare function validateConfig(config: ConfigInput): ZenithConfig;
|
|
13
|
+
export declare function loadConfig(projectRoot: string): Promise<ZenithConfig>;
|
|
14
|
+
export declare function getDefaults(): ZenithConfig;
|
|
15
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
router: false,
|
|
5
|
+
embeddedMarkupExpressions: false,
|
|
6
|
+
types: true,
|
|
7
|
+
typescriptDefault: true,
|
|
8
|
+
outDir: 'dist',
|
|
9
|
+
pagesDir: 'pages',
|
|
10
|
+
experimental: {},
|
|
11
|
+
strictDomLints: false
|
|
12
|
+
};
|
|
13
|
+
const SCHEMA = {
|
|
14
|
+
router: 'boolean',
|
|
15
|
+
embeddedMarkupExpressions: 'boolean',
|
|
16
|
+
types: 'boolean',
|
|
17
|
+
typescriptDefault: 'boolean',
|
|
18
|
+
outDir: 'string',
|
|
19
|
+
pagesDir: 'string',
|
|
20
|
+
experimental: 'object',
|
|
21
|
+
strictDomLints: 'boolean'
|
|
22
|
+
};
|
|
23
|
+
export function validateConfig(config) {
|
|
24
|
+
if (config === null || config === undefined) {
|
|
25
|
+
return { ...DEFAULTS };
|
|
26
|
+
}
|
|
27
|
+
if (typeof config !== 'object' || Array.isArray(config)) {
|
|
28
|
+
throw new Error('[Zenith:Config] Config must be a plain object');
|
|
29
|
+
}
|
|
30
|
+
for (const key of Object.keys(config)) {
|
|
31
|
+
if (!(key in SCHEMA)) {
|
|
32
|
+
throw new Error(`[Zenith:Config] Unknown key: "${key}"`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const result = { ...DEFAULTS };
|
|
36
|
+
for (const [key, expectedType] of Object.entries(SCHEMA)) {
|
|
37
|
+
if (key in config) {
|
|
38
|
+
const value = config[key];
|
|
39
|
+
if (typeof value !== expectedType) {
|
|
40
|
+
throw new Error(`[Zenith:Config] Key "${key}" must be ${expectedType}, got ${typeof value}`);
|
|
41
|
+
}
|
|
42
|
+
if (expectedType === 'string' && typeof value === 'string' && value.trim() === '') {
|
|
43
|
+
throw new Error(`[Zenith:Config] Key "${key}" must be a non-empty string`);
|
|
44
|
+
}
|
|
45
|
+
if (key === 'experimental' && value) {
|
|
46
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
47
|
+
throw new Error('[Zenith:Config] Key "experimental" must be a plain object');
|
|
48
|
+
}
|
|
49
|
+
result[key] = { ...DEFAULTS.experimental };
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
result[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
export async function loadConfig(projectRoot) {
|
|
58
|
+
const configPath = join(projectRoot, 'zenith.config.js');
|
|
59
|
+
try {
|
|
60
|
+
const url = pathToFileURL(configPath).href;
|
|
61
|
+
const mod = await import(url);
|
|
62
|
+
const raw = mod.default || mod;
|
|
63
|
+
return validateConfig(raw);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const error = err;
|
|
67
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
68
|
+
error.code === 'ENOENT' ||
|
|
69
|
+
error.message?.includes('Cannot find module') ||
|
|
70
|
+
error.message?.includes('ENOENT')) {
|
|
71
|
+
return { ...DEFAULTS };
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function getDefaults() {
|
|
77
|
+
return { ...DEFAULTS };
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function coreModuleSource(runtimeImport: string): string;
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// core-template.js — source template for emitted core asset
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
1
|
function assertRuntimeImport(runtimeImport) {
|
|
6
2
|
if (typeof runtimeImport !== 'string' || runtimeImport.trim().length === 0) {
|
|
7
3
|
throw new Error('[Zenith Core] coreModuleSource(runtimeImport) requires non-empty runtimeImport');
|
|
8
4
|
}
|
|
9
5
|
}
|
|
10
|
-
|
|
11
6
|
export function coreModuleSource(runtimeImport) {
|
|
12
7
|
assertRuntimeImport(runtimeImport);
|
|
13
8
|
const runtimeImportLiteral = JSON.stringify(runtimeImport);
|
|
14
|
-
|
|
15
9
|
return [
|
|
16
10
|
`import { signal, state, zeneffect, zenEffect as __zenithZenEffect, zenMount as __zenithZenMount } from ${runtimeImportLiteral};`,
|
|
17
11
|
'',
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ZenithError extends Error {
|
|
2
|
+
zenithModule: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function createError(module: string, message: string): ZenithError;
|
|
5
|
+
export declare function formatError(module: string, message: string): string;
|
|
6
|
+
export declare function isZenithError(err: Error | unknown): err is ZenithError;
|
|
7
|
+
export declare const ErrorCodes: {
|
|
8
|
+
readonly CONFIG_UNKNOWN_KEY: "CONFIG_UNKNOWN_KEY";
|
|
9
|
+
readonly CONFIG_INVALID_TYPE: "CONFIG_INVALID_TYPE";
|
|
10
|
+
readonly CONFIG_EMPTY_VALUE: "CONFIG_EMPTY_VALUE";
|
|
11
|
+
readonly PATH_REPEATED_PARAM: "PATH_REPEATED_PARAM";
|
|
12
|
+
readonly VERSION_INCOMPATIBLE: "VERSION_INCOMPATIBLE";
|
|
13
|
+
readonly GUARD_VIOLATION: "GUARD_VIOLATION";
|
|
14
|
+
};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createError(module, message) {
|
|
2
|
+
const err = new Error(`[Zenith:${module}] ${message}`);
|
|
3
|
+
err.zenithModule = module;
|
|
4
|
+
return err;
|
|
5
|
+
}
|
|
6
|
+
export function formatError(module, message) {
|
|
7
|
+
return `[Zenith:${module}] ${message}`;
|
|
8
|
+
}
|
|
9
|
+
export function isZenithError(err) {
|
|
10
|
+
return err instanceof Error && typeof err.zenithModule === 'string';
|
|
11
|
+
}
|
|
12
|
+
export const ErrorCodes = {
|
|
13
|
+
CONFIG_UNKNOWN_KEY: 'CONFIG_UNKNOWN_KEY',
|
|
14
|
+
CONFIG_INVALID_TYPE: 'CONFIG_INVALID_TYPE',
|
|
15
|
+
CONFIG_EMPTY_VALUE: 'CONFIG_EMPTY_VALUE',
|
|
16
|
+
PATH_REPEATED_PARAM: 'PATH_REPEATED_PARAM',
|
|
17
|
+
VERSION_INCOMPATIBLE: 'VERSION_INCOMPATIBLE',
|
|
18
|
+
GUARD_VIOLATION: 'GUARD_VIOLATION'
|
|
19
|
+
};
|
package/dist/guards.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function containsForbiddenPattern(source: string, patterns: string[]): string[];
|
|
2
|
+
export { validateRouteParams } from './path.js';
|
|
3
|
+
export { validateConfig as validateConfigSchema } from './config.js';
|
|
4
|
+
export declare const FORBIDDEN_PATTERNS: string[];
|
|
5
|
+
export declare const BROWSER_GLOBALS: string[];
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function containsForbiddenPattern(source, patterns) {
|
|
2
|
+
const found = [];
|
|
3
|
+
for (const pattern of patterns) {
|
|
4
|
+
if (source.includes(pattern)) {
|
|
5
|
+
found.push(pattern);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return found;
|
|
9
|
+
}
|
|
10
|
+
export { validateRouteParams } from './path.js';
|
|
11
|
+
export { validateConfig as validateConfigSchema } from './config.js';
|
|
12
|
+
export const FORBIDDEN_PATTERNS = [
|
|
13
|
+
'eval(',
|
|
14
|
+
'new Function(',
|
|
15
|
+
'new Function (',
|
|
16
|
+
'document.write('
|
|
17
|
+
];
|
|
18
|
+
export const BROWSER_GLOBALS = [
|
|
19
|
+
'window',
|
|
20
|
+
'document',
|
|
21
|
+
'navigator',
|
|
22
|
+
'localStorage',
|
|
23
|
+
'sessionStorage'
|
|
24
|
+
];
|
package/dist/hash.d.ts
ADDED
package/dist/hash.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function hash(content) {
|
|
3
|
+
const normalized = normalizeInput(content);
|
|
4
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
5
|
+
}
|
|
6
|
+
export function hashShort(content, length = 8) {
|
|
7
|
+
return hash(content).slice(0, length);
|
|
8
|
+
}
|
|
9
|
+
function normalizeInput(content) {
|
|
10
|
+
let result = content.replace(/\\/g, '/');
|
|
11
|
+
result = result.replace(/\n+$/, '');
|
|
12
|
+
return result;
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { validateConfig, loadConfig, getDefaults } from './config.js';
|
|
2
|
+
export { normalizeSeparators, fileToRoute, extractParams, isDynamic, validateRouteParams, canonicalize } from './path.js';
|
|
3
|
+
export { sortRoutes, sortAlpha, isCorrectOrder } from './order.js';
|
|
4
|
+
export { hash, hashShort } from './hash.js';
|
|
5
|
+
export { createError, formatError, isZenithError, ErrorCodes } from './errors.js';
|
|
6
|
+
export { parseSemver, formatVersion, validateCompatibility } from './version.js';
|
|
7
|
+
export { IR_VERSION, IR_SCHEMA } from './schema.js';
|
|
8
|
+
export { coreModuleSource } from './core-template.js';
|
|
9
|
+
export { containsForbiddenPattern, FORBIDDEN_PATTERNS, BROWSER_GLOBALS } from './guards.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { validateConfig, loadConfig, getDefaults } from './config.js';
|
|
2
|
+
export { normalizeSeparators, fileToRoute, extractParams, isDynamic, validateRouteParams, canonicalize } from './path.js';
|
|
3
|
+
export { sortRoutes, sortAlpha, isCorrectOrder } from './order.js';
|
|
4
|
+
export { hash, hashShort } from './hash.js';
|
|
5
|
+
export { createError, formatError, isZenithError, ErrorCodes } from './errors.js';
|
|
6
|
+
export { parseSemver, formatVersion, validateCompatibility } from './version.js';
|
|
7
|
+
export { IR_VERSION, IR_SCHEMA } from './schema.js';
|
|
8
|
+
export { coreModuleSource } from './core-template.js';
|
|
9
|
+
export { containsForbiddenPattern, FORBIDDEN_PATTERNS, BROWSER_GLOBALS } from './guards.js';
|
package/dist/ir/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IR_VERSION, IR_SCHEMA } from '../schema.js';
|
package/dist/order.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface RouteEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function sortRoutes<T extends RouteEntry>(entries: T[]): T[];
|
|
5
|
+
export declare function sortAlpha(items: string[]): string[];
|
|
6
|
+
export declare function isCorrectOrder<T extends RouteEntry>(entries: T[]): boolean;
|
package/dist/order.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function isDynamicRoute(routePath) {
|
|
2
|
+
return routePath.includes(':');
|
|
3
|
+
}
|
|
4
|
+
export function sortRoutes(entries) {
|
|
5
|
+
return [...entries].sort((a, b) => {
|
|
6
|
+
const aDynamic = isDynamicRoute(a.path);
|
|
7
|
+
const bDynamic = isDynamicRoute(b.path);
|
|
8
|
+
if (!aDynamic && bDynamic)
|
|
9
|
+
return -1;
|
|
10
|
+
if (aDynamic && !bDynamic)
|
|
11
|
+
return 1;
|
|
12
|
+
return a.path.localeCompare(b.path);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function sortAlpha(items) {
|
|
16
|
+
return [...items].sort((a, b) => a.localeCompare(b));
|
|
17
|
+
}
|
|
18
|
+
export function isCorrectOrder(entries) {
|
|
19
|
+
const sorted = sortRoutes(entries);
|
|
20
|
+
return entries.every((entry, i) => entry.path === sorted[i]?.path);
|
|
21
|
+
}
|
package/dist/path.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function normalizeSeparators(filePath: string): string;
|
|
2
|
+
export declare function fileToRoute(filePath: string, extension?: string): string;
|
|
3
|
+
export declare function extractParams(routePath: string): string[];
|
|
4
|
+
export declare function isDynamic(routePath: string): boolean;
|
|
5
|
+
export declare function validateRouteParams(routePath: string): void;
|
|
6
|
+
export declare function canonicalize(routePath: string): string;
|
package/dist/path.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function normalizeSeparators(filePath) {
|
|
2
|
+
return filePath.replace(/\\/g, '/');
|
|
3
|
+
}
|
|
4
|
+
export function fileToRoute(filePath, extension = '.zen') {
|
|
5
|
+
let route = normalizeSeparators(filePath);
|
|
6
|
+
if (route.endsWith(extension)) {
|
|
7
|
+
route = route.slice(0, -extension.length);
|
|
8
|
+
}
|
|
9
|
+
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
10
|
+
if (route === 'index') {
|
|
11
|
+
return '/';
|
|
12
|
+
}
|
|
13
|
+
if (route.endsWith('/index')) {
|
|
14
|
+
route = route.slice(0, -'/index'.length);
|
|
15
|
+
}
|
|
16
|
+
if (!route.startsWith('/')) {
|
|
17
|
+
route = `/${route}`;
|
|
18
|
+
}
|
|
19
|
+
return route;
|
|
20
|
+
}
|
|
21
|
+
export function extractParams(routePath) {
|
|
22
|
+
const params = [];
|
|
23
|
+
const segments = routePath.split('/');
|
|
24
|
+
for (const segment of segments) {
|
|
25
|
+
if (segment.startsWith(':')) {
|
|
26
|
+
params.push(segment.slice(1));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return params;
|
|
30
|
+
}
|
|
31
|
+
export function isDynamic(routePath) {
|
|
32
|
+
return routePath.includes(':');
|
|
33
|
+
}
|
|
34
|
+
export function validateRouteParams(routePath) {
|
|
35
|
+
const params = extractParams(routePath);
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const param of params) {
|
|
38
|
+
if (seen.has(param)) {
|
|
39
|
+
throw new Error(`[Zenith:Path] Repeated parameter name ":${param}" in route "${routePath}"`);
|
|
40
|
+
}
|
|
41
|
+
seen.add(param);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function canonicalize(routePath) {
|
|
45
|
+
let path = normalizeSeparators(routePath);
|
|
46
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
47
|
+
path = path.slice(0, -1);
|
|
48
|
+
}
|
|
49
|
+
if (!path.startsWith('/')) {
|
|
50
|
+
path = `/${path}`;
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
package/dist/schema.d.ts
ADDED
package/{src → dist}/schema.js
RENAMED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// schema.js — Zenith Core IR authority
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
1
|
export const IR_VERSION = 1;
|
|
6
|
-
|
|
7
2
|
export const IR_SCHEMA = Object.freeze({
|
|
8
3
|
version: IR_VERSION,
|
|
9
4
|
requiredTopLevelKeys: Object.freeze([
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SemverVersion {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseSemver(version: string): SemverVersion;
|
|
7
|
+
export declare function formatVersion(ver: SemverVersion): string;
|
|
8
|
+
export declare function validateCompatibility(coreVersion: string, otherVersion: string): string | null;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function parseSemver(version) {
|
|
2
|
+
const cleaned = version.replace(/^v/, '');
|
|
3
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
4
|
+
if (!match) {
|
|
5
|
+
throw new Error(`[Zenith:Version] Invalid semver: "${version}"`);
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
major: Number.parseInt(match[1], 10),
|
|
9
|
+
minor: Number.parseInt(match[2], 10),
|
|
10
|
+
patch: Number.parseInt(match[3], 10)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function formatVersion(ver) {
|
|
14
|
+
return `${ver.major}.${ver.minor}.${ver.patch}`;
|
|
15
|
+
}
|
|
16
|
+
export function validateCompatibility(coreVersion, otherVersion) {
|
|
17
|
+
const core = parseSemver(coreVersion);
|
|
18
|
+
const other = parseSemver(otherVersion);
|
|
19
|
+
if (core.major !== other.major) {
|
|
20
|
+
throw new Error(`[Zenith:Version] Incompatible major versions: core=${formatVersion(core)}, other=${formatVersion(other)}`);
|
|
21
|
+
}
|
|
22
|
+
const minorDiff = Math.abs(core.minor - other.minor);
|
|
23
|
+
if (minorDiff > 1) {
|
|
24
|
+
return `[Zenith:Version] Minor version drift: core=${formatVersion(core)}, other=${formatVersion(other)} (diff=${minorDiff})`;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,55 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Deterministic utility substrate for the Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "./
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
8
9
|
"bin": {
|
|
9
10
|
"zenith": "bin/zenith.js"
|
|
10
11
|
},
|
|
11
12
|
"exports": {
|
|
12
|
-
".": "./
|
|
13
|
-
"./config": "./
|
|
14
|
-
"./path": "./
|
|
15
|
-
"./order": "./
|
|
16
|
-
"./hash": "./
|
|
17
|
-
"./errors": "./
|
|
18
|
-
"./version": "./
|
|
19
|
-
"./guards": "./
|
|
20
|
-
"./schema": "./
|
|
21
|
-
"./core-template": "./
|
|
22
|
-
"./ir": "./
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./config": "./dist/config.js",
|
|
15
|
+
"./path": "./dist/path.js",
|
|
16
|
+
"./order": "./dist/order.js",
|
|
17
|
+
"./hash": "./dist/hash.js",
|
|
18
|
+
"./errors": "./dist/errors.js",
|
|
19
|
+
"./version": "./dist/version.js",
|
|
20
|
+
"./guards": "./dist/guards.js",
|
|
21
|
+
"./schema": "./dist/schema.js",
|
|
22
|
+
"./core-template": "./dist/core-template.js",
|
|
23
|
+
"./ir": "./dist/ir/index.js"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
|
-
"
|
|
26
|
+
"dist",
|
|
26
27
|
"bin",
|
|
27
28
|
"CORE_CONTRACT.md",
|
|
28
29
|
"README.md",
|
|
29
|
-
"
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"package.json"
|
|
30
32
|
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/zenithbuild/framework",
|
|
36
|
+
"directory": "packages/core"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/zenithbuild/framework/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/zenithbuild/framework#readme",
|
|
31
42
|
"publishConfig": {
|
|
32
43
|
"access": "public"
|
|
33
44
|
},
|
|
34
45
|
"dependencies": {
|
|
35
|
-
"@zenithbuild/cli": "
|
|
36
|
-
"@zenithbuild/compiler": "
|
|
37
|
-
"@zenithbuild/runtime": "
|
|
38
|
-
"@zenithbuild/router": "
|
|
39
|
-
"@zenithbuild/bundler": "
|
|
46
|
+
"@zenithbuild/cli": "0.6.7",
|
|
47
|
+
"@zenithbuild/compiler": "0.6.7",
|
|
48
|
+
"@zenithbuild/runtime": "0.6.7",
|
|
49
|
+
"@zenithbuild/router": "0.6.7",
|
|
50
|
+
"@zenithbuild/bundler": "0.6.7"
|
|
40
51
|
},
|
|
41
52
|
"scripts": {
|
|
42
|
-
"
|
|
53
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
"test": "npm run build && npm run contract:deps && npm run contract:scan && npm run contract:api && npm run contract:template && npm run contract:golden && npm run test:legacy",
|
|
43
56
|
"test:legacy": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.js",
|
|
44
57
|
"contract:deps": "node dependency_contract.spec.js",
|
|
45
58
|
"contract:scan": "node contract-scan.mjs",
|
|
46
59
|
"contract:api": "node core_api_lock.spec.js",
|
|
47
60
|
"contract:template": "node template-contract.spec.js",
|
|
48
|
-
"contract:golden": "node golden-contract.spec.js"
|
|
61
|
+
"contract:golden": "node golden-contract.spec.js",
|
|
62
|
+
"prepublishOnly": "npm run build"
|
|
49
63
|
},
|
|
50
64
|
"devDependencies": {
|
|
65
|
+
"@types/node": "latest",
|
|
51
66
|
"@jest/globals": "^30.2.0",
|
|
52
|
-
"jest": "^30.2.0"
|
|
67
|
+
"jest": "^30.2.0",
|
|
68
|
+
"typescript": "^5.7.3"
|
|
53
69
|
},
|
|
54
70
|
"private": false
|
|
55
71
|
}
|
package/src/config.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// config.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Load and validate zenith.config.js. This is the ONLY module in core
|
|
5
|
-
// that touches the filesystem (via dynamic import).
|
|
6
|
-
//
|
|
7
|
-
// V0 Schema: { router, outDir, pagesDir }
|
|
8
|
-
// Unknown keys → throw.
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { pathToFileURL } from 'node:url';
|
|
13
|
-
|
|
14
|
-
/** V0 config defaults */
|
|
15
|
-
const DEFAULTS = {
|
|
16
|
-
router: false,
|
|
17
|
-
embeddedMarkupExpressions: false,
|
|
18
|
-
types: true,
|
|
19
|
-
typescriptDefault: true,
|
|
20
|
-
outDir: 'dist',
|
|
21
|
-
pagesDir: 'pages',
|
|
22
|
-
experimental: {},
|
|
23
|
-
strictDomLints: false
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/** Allowed keys and their expected types */
|
|
27
|
-
const SCHEMA = {
|
|
28
|
-
router: 'boolean',
|
|
29
|
-
embeddedMarkupExpressions: 'boolean',
|
|
30
|
-
types: 'boolean',
|
|
31
|
-
typescriptDefault: 'boolean',
|
|
32
|
-
outDir: 'string',
|
|
33
|
-
pagesDir: 'string',
|
|
34
|
-
experimental: 'object',
|
|
35
|
-
strictDomLints: 'boolean'
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Validate a config object against the V0 schema.
|
|
40
|
-
* Throws on unknown keys or wrong types.
|
|
41
|
-
*
|
|
42
|
-
* @param {object} config
|
|
43
|
-
* @returns {object} Normalized config with defaults applied
|
|
44
|
-
*/
|
|
45
|
-
export function validateConfig(config) {
|
|
46
|
-
if (config === null || config === undefined) {
|
|
47
|
-
return { ...DEFAULTS };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (typeof config !== 'object' || Array.isArray(config)) {
|
|
51
|
-
throw new Error('[Zenith:Config] Config must be a plain object');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check for unknown keys
|
|
55
|
-
for (const key of Object.keys(config)) {
|
|
56
|
-
if (!(key in SCHEMA)) {
|
|
57
|
-
throw new Error(`[Zenith:Config] Unknown key: "${key}"`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Validate types and apply defaults
|
|
62
|
-
const result = { ...DEFAULTS };
|
|
63
|
-
|
|
64
|
-
for (const [key, expectedType] of Object.entries(SCHEMA)) {
|
|
65
|
-
if (key in config) {
|
|
66
|
-
const value = config[key];
|
|
67
|
-
if (typeof value !== expectedType) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`[Zenith:Config] Key "${key}" must be ${expectedType}, got ${typeof value}`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
if (expectedType === 'string' && value.trim() === '') {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`[Zenith:Config] Key "${key}" must be a non-empty string`
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
if (key === 'experimental' && value) {
|
|
78
|
-
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
79
|
-
throw new Error(`[Zenith:Config] Key "experimental" must be a plain object`);
|
|
80
|
-
}
|
|
81
|
-
const expDefaults = { ...DEFAULTS.experimental };
|
|
82
|
-
for (const expKey of Object.keys(value)) {
|
|
83
|
-
if (!(expKey in expDefaults)) {
|
|
84
|
-
throw new Error(`[Zenith:Config] Unknown experimental key: "${expKey}"`);
|
|
85
|
-
}
|
|
86
|
-
if (typeof value[expKey] !== typeof expDefaults[expKey]) {
|
|
87
|
-
throw new Error(`[Zenith:Config] Experimental key "${expKey}" must be ${typeof expDefaults[expKey]}`);
|
|
88
|
-
}
|
|
89
|
-
expDefaults[expKey] = value[expKey];
|
|
90
|
-
}
|
|
91
|
-
result[key] = expDefaults;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
result[key] = value;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Load zenith.config.js from a project root.
|
|
103
|
-
* Returns validated config with defaults applied.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} projectRoot
|
|
106
|
-
* @returns {Promise<object>}
|
|
107
|
-
*/
|
|
108
|
-
export async function loadConfig(projectRoot) {
|
|
109
|
-
const configPath = join(projectRoot, 'zenith.config.js');
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const url = pathToFileURL(configPath).href;
|
|
113
|
-
const mod = await import(url);
|
|
114
|
-
const raw = mod.default || mod;
|
|
115
|
-
return validateConfig(raw);
|
|
116
|
-
} catch (err) {
|
|
117
|
-
// File not found — return defaults
|
|
118
|
-
if (
|
|
119
|
-
err.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
120
|
-
err.code === 'ENOENT' ||
|
|
121
|
-
err.message?.includes('Cannot find module') ||
|
|
122
|
-
err.message?.includes('ENOENT')
|
|
123
|
-
) {
|
|
124
|
-
return { ...DEFAULTS };
|
|
125
|
-
}
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Return the default config.
|
|
132
|
-
* @returns {object}
|
|
133
|
-
*/
|
|
134
|
-
export function getDefaults() {
|
|
135
|
-
return { ...DEFAULTS };
|
|
136
|
-
}
|
package/src/errors.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// errors.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Shared error formatting. All Zenith errors use the format:
|
|
5
|
-
// [Zenith:MODULE] message
|
|
6
|
-
//
|
|
7
|
-
// Pure factory functions. No side effects.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create a Zenith error with standard formatting.
|
|
12
|
-
*
|
|
13
|
-
* @param {string} module Module name (e.g. 'Config', 'Path', 'Build')
|
|
14
|
-
* @param {string} message Error message
|
|
15
|
-
* @returns {Error}
|
|
16
|
-
*/
|
|
17
|
-
export function createError(module, message) {
|
|
18
|
-
const err = new Error(`[Zenith:${module}] ${message}`);
|
|
19
|
-
err.zenithModule = module;
|
|
20
|
-
return err;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Format an error message with the Zenith prefix.
|
|
25
|
-
*
|
|
26
|
-
* @param {string} module
|
|
27
|
-
* @param {string} message
|
|
28
|
-
* @returns {string}
|
|
29
|
-
*/
|
|
30
|
-
export function formatError(module, message) {
|
|
31
|
-
return `[Zenith:${module}] ${message}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Check if an error is a Zenith error.
|
|
36
|
-
*
|
|
37
|
-
* @param {Error} err
|
|
38
|
-
* @returns {boolean}
|
|
39
|
-
*/
|
|
40
|
-
export function isZenithError(err) {
|
|
41
|
-
return err instanceof Error && typeof err.zenithModule === 'string';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Predefined error codes for common situations.
|
|
46
|
-
*/
|
|
47
|
-
export const ErrorCodes = {
|
|
48
|
-
CONFIG_UNKNOWN_KEY: 'CONFIG_UNKNOWN_KEY',
|
|
49
|
-
CONFIG_INVALID_TYPE: 'CONFIG_INVALID_TYPE',
|
|
50
|
-
CONFIG_EMPTY_VALUE: 'CONFIG_EMPTY_VALUE',
|
|
51
|
-
PATH_REPEATED_PARAM: 'PATH_REPEATED_PARAM',
|
|
52
|
-
VERSION_INCOMPATIBLE: 'VERSION_INCOMPATIBLE',
|
|
53
|
-
GUARD_VIOLATION: 'GUARD_VIOLATION'
|
|
54
|
-
};
|
package/src/guards.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// guards.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Small pure validation helpers. These provide primitives —
|
|
5
|
-
// they do NOT scan repos, enforce architecture, or police other layers.
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Check if source contains any of the given forbidden patterns.
|
|
10
|
-
*
|
|
11
|
-
* @param {string} source Source code text
|
|
12
|
-
* @param {string[]} patterns Patterns to check for
|
|
13
|
-
* @returns {string[]} Array of matched patterns (empty if none)
|
|
14
|
-
*/
|
|
15
|
-
export function containsForbiddenPattern(source, patterns) {
|
|
16
|
-
const found = [];
|
|
17
|
-
for (const pattern of patterns) {
|
|
18
|
-
if (source.includes(pattern)) {
|
|
19
|
-
found.push(pattern);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return found;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Validate that a route path has no repeated parameter names.
|
|
27
|
-
* Re-exported from path.js for convenience.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} routePath
|
|
30
|
-
*/
|
|
31
|
-
export { validateRouteParams } from './path.js';
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Validate a config object against the V0 schema.
|
|
35
|
-
* Re-exported from config.js for convenience.
|
|
36
|
-
*
|
|
37
|
-
* @param {object} config
|
|
38
|
-
* @returns {object}
|
|
39
|
-
*/
|
|
40
|
-
export { validateConfig as validateConfigSchema } from './config.js';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Default forbidden patterns for Zenith source code.
|
|
44
|
-
*/
|
|
45
|
-
export const FORBIDDEN_PATTERNS = [
|
|
46
|
-
'eval(',
|
|
47
|
-
'new Function(',
|
|
48
|
-
'new Function (',
|
|
49
|
-
'document.write('
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Default browser globals that should not appear in Node-only code.
|
|
54
|
-
*/
|
|
55
|
-
export const BROWSER_GLOBALS = [
|
|
56
|
-
'window',
|
|
57
|
-
'document',
|
|
58
|
-
'navigator',
|
|
59
|
-
'localStorage',
|
|
60
|
-
'sessionStorage'
|
|
61
|
-
];
|
package/src/hash.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// hash.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Deterministic content hashing. SHA-256, hex output.
|
|
5
|
-
//
|
|
6
|
-
// CRITICAL: This algorithm must match the bundler's hashing exactly.
|
|
7
|
-
// If bundler changes hash algorithm, core must change in lockstep.
|
|
8
|
-
//
|
|
9
|
-
// Input normalization:
|
|
10
|
-
// - Path separators → /
|
|
11
|
-
// - Trailing newlines stripped
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Hash a string using SHA-256. Returns hex digest.
|
|
18
|
-
*
|
|
19
|
-
* @param {string} content
|
|
20
|
-
* @returns {string}
|
|
21
|
-
*/
|
|
22
|
-
export function hash(content) {
|
|
23
|
-
const normalized = _normalizeInput(content);
|
|
24
|
-
return createHash('sha256').update(normalized).digest('hex');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Hash a string and return a truncated hash (first N characters).
|
|
29
|
-
* Useful for content-hashed filenames.
|
|
30
|
-
*
|
|
31
|
-
* @param {string} content
|
|
32
|
-
* @param {number} [length=8]
|
|
33
|
-
* @returns {string}
|
|
34
|
-
*/
|
|
35
|
-
export function hashShort(content, length = 8) {
|
|
36
|
-
return hash(content).slice(0, length);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Normalize input for deterministic hashing.
|
|
41
|
-
* - Replaces backslashes with forward slashes
|
|
42
|
-
* - Strips trailing newlines
|
|
43
|
-
*
|
|
44
|
-
* @param {string} content
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
function _normalizeInput(content) {
|
|
48
|
-
let result = content.replace(/\\/g, '/');
|
|
49
|
-
// Strip trailing newlines
|
|
50
|
-
result = result.replace(/\n+$/, '');
|
|
51
|
-
return result;
|
|
52
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// index.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Public API. Re-exports frozen module namespaces only.
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
export { validateConfig, loadConfig, getDefaults } from './config.js';
|
|
8
|
-
export {
|
|
9
|
-
normalizeSeparators,
|
|
10
|
-
fileToRoute,
|
|
11
|
-
extractParams,
|
|
12
|
-
isDynamic,
|
|
13
|
-
validateRouteParams,
|
|
14
|
-
canonicalize
|
|
15
|
-
} from './path.js';
|
|
16
|
-
export { sortRoutes, sortAlpha, isCorrectOrder } from './order.js';
|
|
17
|
-
export { hash, hashShort } from './hash.js';
|
|
18
|
-
export { createError, formatError, isZenithError, ErrorCodes } from './errors.js';
|
|
19
|
-
export { parseSemver, formatVersion, validateCompatibility } from './version.js';
|
|
20
|
-
export { IR_VERSION, IR_SCHEMA } from './schema.js';
|
|
21
|
-
export { coreModuleSource } from './core-template.js';
|
|
22
|
-
export {
|
|
23
|
-
containsForbiddenPattern,
|
|
24
|
-
FORBIDDEN_PATTERNS,
|
|
25
|
-
BROWSER_GLOBALS
|
|
26
|
-
} from './guards.js';
|
package/src/order.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// order.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Deterministic ordering utilities. Pure transforms.
|
|
5
|
-
//
|
|
6
|
-
// Sorting contract:
|
|
7
|
-
// 1. Static routes first, dynamic routes second
|
|
8
|
-
// 2. Alphabetical tiebreak within each group
|
|
9
|
-
// 3. Stable across runs
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Check if a route path contains dynamic segments (starts with :).
|
|
14
|
-
*
|
|
15
|
-
* @param {string} routePath
|
|
16
|
-
* @returns {boolean}
|
|
17
|
-
*/
|
|
18
|
-
function _isDynamic(routePath) {
|
|
19
|
-
return routePath.includes(':');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Sort route entries deterministically.
|
|
24
|
-
*
|
|
25
|
-
* Rules:
|
|
26
|
-
* 1. Static routes before dynamic routes
|
|
27
|
-
* 2. Alphabetical (lexicographic) within each group
|
|
28
|
-
*
|
|
29
|
-
* Entries must have a `path` property.
|
|
30
|
-
*
|
|
31
|
-
* @param {{ path: string }[]} entries
|
|
32
|
-
* @returns {{ path: string }[]}
|
|
33
|
-
*/
|
|
34
|
-
export function sortRoutes(entries) {
|
|
35
|
-
return [...entries].sort((a, b) => {
|
|
36
|
-
const aDynamic = _isDynamic(a.path);
|
|
37
|
-
const bDynamic = _isDynamic(b.path);
|
|
38
|
-
|
|
39
|
-
// Static before dynamic
|
|
40
|
-
if (!aDynamic && bDynamic) return -1;
|
|
41
|
-
if (aDynamic && !bDynamic) return 1;
|
|
42
|
-
|
|
43
|
-
// Alphabetical tiebreak
|
|
44
|
-
return a.path.localeCompare(b.path);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Sort an array of strings deterministically.
|
|
50
|
-
* Simple alphabetical sort with stable ordering.
|
|
51
|
-
*
|
|
52
|
-
* @param {string[]} items
|
|
53
|
-
* @returns {string[]}
|
|
54
|
-
*/
|
|
55
|
-
export function sortAlpha(items) {
|
|
56
|
-
return [...items].sort((a, b) => a.localeCompare(b));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Check if an array of route entries is in correct deterministic order.
|
|
61
|
-
* Does not throw — returns boolean.
|
|
62
|
-
*
|
|
63
|
-
* @param {{ path: string }[]} entries
|
|
64
|
-
* @returns {boolean}
|
|
65
|
-
*/
|
|
66
|
-
export function isCorrectOrder(entries) {
|
|
67
|
-
const sorted = sortRoutes(entries);
|
|
68
|
-
return entries.every((entry, i) => entry.path === sorted[i].path);
|
|
69
|
-
}
|
package/src/path.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// path.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Path normalization utilities. Pure transforms — no filesystem access.
|
|
5
|
-
//
|
|
6
|
-
// - Normalizes separators (\ → /)
|
|
7
|
-
// - Converts [param] → :param
|
|
8
|
-
// - Extracts params from route strings
|
|
9
|
-
// - Validates no repeated param names
|
|
10
|
-
// - Strips file extensions
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Normalize path separators to forward slashes.
|
|
15
|
-
*
|
|
16
|
-
* @param {string} filePath
|
|
17
|
-
* @returns {string}
|
|
18
|
-
*/
|
|
19
|
-
export function normalizeSeparators(filePath) {
|
|
20
|
-
return filePath.replace(/\\/g, '/');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Convert a file-system path to a route path.
|
|
25
|
-
*
|
|
26
|
-
* Rules:
|
|
27
|
-
* - Normalize separators
|
|
28
|
-
* - Strip the extension
|
|
29
|
-
* - Convert [param] → :param
|
|
30
|
-
* - index files → parent path
|
|
31
|
-
* - Ensure leading /
|
|
32
|
-
*
|
|
33
|
-
* @param {string} filePath Relative path from pages dir (e.g. "users/[id].zen")
|
|
34
|
-
* @param {string} [extension='.zen']
|
|
35
|
-
* @returns {string}
|
|
36
|
-
*/
|
|
37
|
-
export function fileToRoute(filePath, extension = '.zen') {
|
|
38
|
-
let route = normalizeSeparators(filePath);
|
|
39
|
-
|
|
40
|
-
// Strip extension
|
|
41
|
-
if (route.endsWith(extension)) {
|
|
42
|
-
route = route.slice(0, -extension.length);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Convert [param] → :param
|
|
46
|
-
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
47
|
-
|
|
48
|
-
// Handle index → parent
|
|
49
|
-
if (route === 'index') {
|
|
50
|
-
return '/';
|
|
51
|
-
}
|
|
52
|
-
if (route.endsWith('/index')) {
|
|
53
|
-
route = route.slice(0, -'/index'.length);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Ensure leading /
|
|
57
|
-
if (!route.startsWith('/')) {
|
|
58
|
-
route = '/' + route;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return route;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Extract parameter names from a route path.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} routePath e.g. "/users/:id/posts/:postId"
|
|
68
|
-
* @returns {string[]} e.g. ["id", "postId"]
|
|
69
|
-
*/
|
|
70
|
-
export function extractParams(routePath) {
|
|
71
|
-
const params = [];
|
|
72
|
-
const segments = routePath.split('/');
|
|
73
|
-
for (const segment of segments) {
|
|
74
|
-
if (segment.startsWith(':')) {
|
|
75
|
-
params.push(segment.slice(1));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return params;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Check if a route path contains dynamic segments.
|
|
83
|
-
*
|
|
84
|
-
* @param {string} routePath
|
|
85
|
-
* @returns {boolean}
|
|
86
|
-
*/
|
|
87
|
-
export function isDynamic(routePath) {
|
|
88
|
-
return routePath.includes(':');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Validate that a route path has no repeated parameter names.
|
|
93
|
-
* Throws on violation.
|
|
94
|
-
*
|
|
95
|
-
* @param {string} routePath
|
|
96
|
-
*/
|
|
97
|
-
export function validateRouteParams(routePath) {
|
|
98
|
-
const params = extractParams(routePath);
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
for (const param of params) {
|
|
101
|
-
if (seen.has(param)) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`[Zenith:Path] Repeated parameter name ":${param}" in route "${routePath}"`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
seen.add(param);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Canonicalize a route path.
|
|
112
|
-
* Strips trailing slashes (except root), normalizes separators.
|
|
113
|
-
*
|
|
114
|
-
* @param {string} routePath
|
|
115
|
-
* @returns {string}
|
|
116
|
-
*/
|
|
117
|
-
export function canonicalize(routePath) {
|
|
118
|
-
let path = normalizeSeparators(routePath);
|
|
119
|
-
|
|
120
|
-
// Remove trailing slash (unless root)
|
|
121
|
-
if (path.length > 1 && path.endsWith('/')) {
|
|
122
|
-
path = path.slice(0, -1);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Ensure leading slash
|
|
126
|
-
if (!path.startsWith('/')) {
|
|
127
|
-
path = '/' + path;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return path;
|
|
131
|
-
}
|
package/src/version.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// version.js — Zenith Core V0
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// SemVer parsing and compatibility validation.
|
|
5
|
-
//
|
|
6
|
-
// Direction of control: Other layers call core's version utility.
|
|
7
|
-
// Core never imports other packages to auto-check.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Parse a SemVer string into components.
|
|
12
|
-
*
|
|
13
|
-
* @param {string} version e.g. "1.2.3"
|
|
14
|
-
* @returns {{ major: number, minor: number, patch: number }}
|
|
15
|
-
*/
|
|
16
|
-
export function parseSemver(version) {
|
|
17
|
-
const cleaned = version.replace(/^v/, '');
|
|
18
|
-
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
19
|
-
if (!match) {
|
|
20
|
-
throw new Error(`[Zenith:Version] Invalid semver: "${version}"`);
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
major: parseInt(match[1], 10),
|
|
24
|
-
minor: parseInt(match[2], 10),
|
|
25
|
-
patch: parseInt(match[3], 10)
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Format a version object back to a string.
|
|
31
|
-
*
|
|
32
|
-
* @param {{ major: number, minor: number, patch: number }} ver
|
|
33
|
-
* @returns {string}
|
|
34
|
-
*/
|
|
35
|
-
export function formatVersion(ver) {
|
|
36
|
-
return `${ver.major}.${ver.minor}.${ver.patch}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Validate compatibility between two versions.
|
|
41
|
-
*
|
|
42
|
-
* Rules:
|
|
43
|
-
* - Major versions must match (throws if different)
|
|
44
|
-
* - Minor version difference > 1 produces a warning (returns warning string)
|
|
45
|
-
* - Otherwise returns null (compatible)
|
|
46
|
-
*
|
|
47
|
-
* @param {string} coreVersion
|
|
48
|
-
* @param {string} otherVersion
|
|
49
|
-
* @returns {string|null} Warning message or null if fully compatible
|
|
50
|
-
*/
|
|
51
|
-
export function validateCompatibility(coreVersion, otherVersion) {
|
|
52
|
-
const core = parseSemver(coreVersion);
|
|
53
|
-
const other = parseSemver(otherVersion);
|
|
54
|
-
|
|
55
|
-
if (core.major !== other.major) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
`[Zenith:Version] Incompatible major versions: core=${formatVersion(core)}, other=${formatVersion(other)}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const minorDiff = Math.abs(core.minor - other.minor);
|
|
62
|
-
if (minorDiff > 1) {
|
|
63
|
-
return `[Zenith:Version] Minor version drift: core=${formatVersion(core)}, other=${formatVersion(other)} (diff=${minorDiff})`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
File without changes
|