@zenithbuild/core 0.6.3 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/core",
3
- "version": "0.6.3",
3
+ "version": "1.1.0",
4
4
  "description": "Core library for the Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,7 +47,15 @@
47
47
  "start": "bun run build && bun run dev",
48
48
  "build:cli": "bun build bin/zenith.ts bin/zen-dev.ts bin/zen-build.ts bin/zen-preview.ts --outdir dist --target bun --bundle && for f in dist/*.js; do echo '#!/usr/bin/env bun' | cat - \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\" && chmod +x \"$f\"; done",
49
49
  "format": "prettier --write \"**/*.ts\"",
50
- "format:check": "prettier --check \"**/*.ts\""
50
+ "format:check": "prettier --check \"**/*.ts\"",
51
+ "release": "bun run scripts/release.ts",
52
+ "release:dry": "bun run scripts/release.ts --dry-run",
53
+ "release:patch": "bun run scripts/release.ts --bump=patch",
54
+ "release:minor": "bun run scripts/release.ts --bump=minor",
55
+ "release:major": "bun run scripts/release.ts --bump=major"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
51
59
  },
52
60
  "private": false,
53
61
  "devDependencies": {
@@ -640,7 +640,16 @@ export function generateBundleJS(): string {
640
640
  function defineSchema(name, schema) { schemaRegistry.set(name, schema); }
641
641
 
642
642
  function zenCollection(collectionName) {
643
- const data = (global.__ZENITH_CONTENT__ && global.__ZENITH_CONTENT__[collectionName]) || [];
643
+ // Access plugin data from the neutral envelope
644
+ // Content plugin stores all items under 'content' namespace
645
+ const pluginData = global.__ZENITH_PLUGIN_DATA__ || {};
646
+ const contentItems = pluginData.content || [];
647
+
648
+ // Filter by collection name (plugin owns data structure, runtime just filters)
649
+ const data = Array.isArray(contentItems)
650
+ ? contentItems.filter(item => item && item.collection === collectionName)
651
+ : [];
652
+
644
653
  return new ZenCollection(data);
645
654
  }
646
655
 
@@ -485,6 +485,25 @@ export function cleanup(container?: Element | Document): void {
485
485
  triggerUnmount();
486
486
  }
487
487
 
488
+ // ============================================
489
+ // Plugin Runtime Data Access
490
+ // ============================================
491
+
492
+ /**
493
+ * Access plugin data from the neutral envelope
494
+ *
495
+ * Plugins use this to retrieve their data at runtime.
496
+ * The CLI injected this data via window.__ZENITH_PLUGIN_DATA__
497
+ *
498
+ * @param namespace - Plugin namespace (e.g., 'content', 'router')
499
+ * @returns The plugin's data, or undefined if not present
500
+ */
501
+ export function getPluginRuntimeData(namespace: string): unknown {
502
+ if (typeof window === 'undefined') return undefined;
503
+ const envelope = (window as any).__ZENITH_PLUGIN_DATA__;
504
+ return envelope?.[namespace];
505
+ }
506
+
488
507
  // ============================================
489
508
  // Browser Globals Setup
490
509
  // ============================================
@@ -506,7 +525,8 @@ export function setupGlobals(): void {
506
525
  onMount: zenOnMount,
507
526
  onUnmount: zenOnUnmount,
508
527
  triggerMount,
509
- triggerUnmount
528
+ triggerUnmount,
529
+ getPluginData: getPluginRuntimeData // Access plugin data from envelope
510
530
  };
511
531
 
512
532
  // Expression registry
@@ -1,112 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { marked } from 'marked';
4
-
5
- export interface ContentItem {
6
- id?: string | number;
7
- slug?: string | null;
8
- collection?: string | null;
9
- content?: string | null;
10
- [key: string]: any | null;
11
- }
12
-
13
- /**
14
- * Load all content from the content directory
15
- */
16
- export function loadContent(contentDir: string): Record<string, ContentItem[]> {
17
- if (!fs.existsSync(contentDir)) {
18
- return {};
19
- }
20
-
21
- const collections: Record<string, ContentItem[]> = {};
22
- const files = getAllFiles(contentDir);
23
-
24
- for (const filePath of files) {
25
- const ext = path.extname(filePath).toLowerCase();
26
- const relativePath = path.relative(contentDir, filePath);
27
- const collection = relativePath.split(path.sep)[0];
28
- if (!collection) continue;
29
-
30
- const slug = relativePath.replace(/\.(md|mdx|json)$/, '').replace(/\\/g, '/');
31
- const id = slug;
32
-
33
- const rawContent = fs.readFileSync(filePath, 'utf-8');
34
-
35
- if (!collections[collection]) {
36
- collections[collection] = [];
37
- }
38
-
39
- if (ext === '.json') {
40
- try {
41
- const data = JSON.parse(rawContent);
42
- collections[collection].push({
43
- id,
44
- slug,
45
- collection,
46
- content: '',
47
- ...data
48
- });
49
- } catch (e) {
50
- console.error(`Error parsing JSON file ${filePath}:`, e);
51
- }
52
- } else if (ext === '.md' || ext === '.mdx') {
53
- const { metadata, content } = parseMarkdown(rawContent);
54
- collections[collection].push({
55
- id,
56
- slug,
57
- collection,
58
- content,
59
- ...metadata
60
- });
61
- }
62
- }
63
-
64
- return collections;
65
- }
66
-
67
- function getAllFiles(dir: string, fileList: string[] = []): string[] {
68
- const files = fs.readdirSync(dir);
69
- files.forEach((file: string) => {
70
- const name = path.join(dir, file);
71
- if (fs.statSync(name).isDirectory()) {
72
- getAllFiles(name, fileList);
73
- } else {
74
- fileList.push(name);
75
- }
76
- });
77
- return fileList;
78
- }
79
-
80
- function parseMarkdown(content: string): { metadata: Record<string, any>, content: string } {
81
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
82
- const match = content.match(frontmatterRegex);
83
-
84
- if (!match) {
85
- return { metadata: {}, content: content.trim() };
86
- }
87
-
88
- const [, yamlStr, body] = match;
89
- const metadata: Record<string, any> = {};
90
-
91
- if (yamlStr) {
92
- yamlStr.split('\n').forEach(line => {
93
- const [key, ...values] = line.split(':');
94
- if (key && values.length > 0) {
95
- const value = values.join(':').trim();
96
- // Basic type conversion
97
- if (value === 'true') metadata[key.trim()] = true;
98
- else if (value === 'false') metadata[key.trim()] = false;
99
- else if (!isNaN(Number(value))) metadata[key.trim()] = Number(value);
100
- else if (value.startsWith('[') && value.endsWith(']')) {
101
- metadata[key.trim()] = value.slice(1, -1).split(',').map(v => v.trim().replace(/^['"]|['"]$/g, ''));
102
- }
103
- else metadata[key.trim()] = value.replace(/^['"]|['"]$/g, '');
104
- }
105
- });
106
- }
107
-
108
- return {
109
- metadata,
110
- content: marked.parse((body || '').trim()) as string
111
- };
112
- }
@@ -1,314 +0,0 @@
1
- /**
2
- * Zenith Route Manifest Generator
3
- *
4
- * Scans pages/ directory at build time and generates a route manifest
5
- * with proper scoring for deterministic route matching.
6
- */
7
-
8
- import fs from "fs"
9
- import path from "path"
10
- import {
11
- type RouteDefinition,
12
- type ParsedSegment,
13
- SegmentType
14
- } from "./types"
15
-
16
- /**
17
- * Scoring constants for route ranking
18
- * Higher scores = higher priority
19
- */
20
- const SEGMENT_SCORES = {
21
- [SegmentType.STATIC]: 10,
22
- [SegmentType.DYNAMIC]: 5,
23
- [SegmentType.CATCH_ALL]: 1,
24
- [SegmentType.OPTIONAL_CATCH_ALL]: 0
25
- } as const
26
-
27
- /**
28
- * Discover all .zen files in the pages directory
29
- */
30
- export function discoverPages(pagesDir: string): string[] {
31
- const pages: string[] = []
32
-
33
- function walk(dir: string): void {
34
- if (!fs.existsSync(dir)) return
35
-
36
- const entries = fs.readdirSync(dir, { withFileTypes: true })
37
-
38
- for (const entry of entries) {
39
- const fullPath = path.join(dir, entry.name)
40
-
41
- if (entry.isDirectory()) {
42
- walk(fullPath)
43
- } else if (entry.isFile() && entry.name.endsWith(".zen")) {
44
- pages.push(fullPath)
45
- }
46
- }
47
- }
48
-
49
- walk(pagesDir)
50
- return pages
51
- }
52
-
53
- /**
54
- * Convert a file path to a route path
55
- *
56
- * Examples:
57
- * pages/index.zen → /
58
- * pages/about.zen → /about
59
- * pages/blog/index.zen → /blog
60
- * pages/blog/[id].zen → /blog/:id
61
- * pages/posts/[...slug].zen → /posts/*slug
62
- * pages/[[...all]].zen → /*all (optional)
63
- */
64
- export function filePathToRoutePath(filePath: string, pagesDir: string): string {
65
- // Get relative path from pages directory
66
- const relativePath = path.relative(pagesDir, filePath)
67
-
68
- // Remove .zen extension
69
- const withoutExt = relativePath.replace(/\.zen$/, "")
70
-
71
- // Split into segments
72
- const segments = withoutExt.split(path.sep)
73
-
74
- // Transform segments
75
- const routeSegments: string[] = []
76
-
77
- for (const segment of segments) {
78
- // Handle index files (they represent the directory root)
79
- if (segment === "index") {
80
- continue
81
- }
82
-
83
- // Handle optional catch-all: [[...param]]
84
- const optionalCatchAllMatch = segment.match(/^\[\[\.\.\.(\w+)\]\]$/)
85
- if (optionalCatchAllMatch) {
86
- routeSegments.push(`*${optionalCatchAllMatch[1]}?`)
87
- continue
88
- }
89
-
90
- // Handle required catch-all: [...param]
91
- const catchAllMatch = segment.match(/^\[\.\.\.(\w+)\]$/)
92
- if (catchAllMatch) {
93
- routeSegments.push(`*${catchAllMatch[1]}`)
94
- continue
95
- }
96
-
97
- // Handle dynamic segment: [param]
98
- const dynamicMatch = segment.match(/^\[(\w+)\]$/)
99
- if (dynamicMatch) {
100
- routeSegments.push(`:${dynamicMatch[1]}`)
101
- continue
102
- }
103
-
104
- // Static segment
105
- routeSegments.push(segment)
106
- }
107
-
108
- // Build route path
109
- const routePath = "/" + routeSegments.join("/")
110
-
111
- // Normalize trailing slashes
112
- return routePath === "/" ? "/" : routePath.replace(/\/$/, "")
113
- }
114
-
115
- /**
116
- * Parse a route path into segments with type information
117
- */
118
- export function parseRouteSegments(routePath: string): ParsedSegment[] {
119
- if (routePath === "/") {
120
- return []
121
- }
122
-
123
- const segments = routePath.slice(1).split("/")
124
- const parsed: ParsedSegment[] = []
125
-
126
- for (const segment of segments) {
127
- // Optional catch-all: *param?
128
- if (segment.startsWith("*") && segment.endsWith("?")) {
129
- parsed.push({
130
- type: SegmentType.OPTIONAL_CATCH_ALL,
131
- paramName: segment.slice(1, -1),
132
- raw: segment
133
- })
134
- continue
135
- }
136
-
137
- // Required catch-all: *param
138
- if (segment.startsWith("*")) {
139
- parsed.push({
140
- type: SegmentType.CATCH_ALL,
141
- paramName: segment.slice(1),
142
- raw: segment
143
- })
144
- continue
145
- }
146
-
147
- // Dynamic: :param
148
- if (segment.startsWith(":")) {
149
- parsed.push({
150
- type: SegmentType.DYNAMIC,
151
- paramName: segment.slice(1),
152
- raw: segment
153
- })
154
- continue
155
- }
156
-
157
- // Static
158
- parsed.push({
159
- type: SegmentType.STATIC,
160
- raw: segment
161
- })
162
- }
163
-
164
- return parsed
165
- }
166
-
167
- /**
168
- * Calculate route score based on segments
169
- * Higher scores = higher priority for matching
170
- */
171
- export function calculateRouteScore(segments: ParsedSegment[]): number {
172
- if (segments.length === 0) {
173
- // Root route gets a high score
174
- return 100
175
- }
176
-
177
- let score = 0
178
-
179
- for (const segment of segments) {
180
- score += SEGMENT_SCORES[segment.type]
181
- }
182
-
183
- // Bonus for having more static segments (specificity)
184
- const staticCount = segments.filter(s => s.type === SegmentType.STATIC).length
185
- score += staticCount * 2
186
-
187
- return score
188
- }
189
-
190
- /**
191
- * Extract parameter names from parsed segments
192
- */
193
- export function extractParamNames(segments: ParsedSegment[]): string[] {
194
- return segments
195
- .filter(s => s.paramName !== undefined)
196
- .map(s => s.paramName!)
197
- }
198
-
199
- /**
200
- * Convert route path to regex pattern
201
- *
202
- * Examples:
203
- * /about → /^\/about\/?$/
204
- * /blog/:id → /^\/blog\/([^/]+)\/?$/
205
- * /posts/*slug → /^\/posts\/(.+)\/?$/
206
- * / → /^\/$/
207
- * /*all? → /^(?:\/(.*))?$/ (optional catch-all)
208
- */
209
- export function routePathToRegex(routePath: string): RegExp {
210
- if (routePath === "/") {
211
- return /^\/$/
212
- }
213
-
214
- const segments = routePath.slice(1).split("/")
215
- const regexParts: string[] = []
216
-
217
- for (let i = 0; i < segments.length; i++) {
218
- const segment = segments[i]
219
- if (!segment) continue
220
-
221
- // Optional catch-all: *param?
222
- if (segment.startsWith("*") && segment.endsWith("?")) {
223
- // Optional catch-all - matches zero or more path segments
224
- // Should only be at the end
225
- regexParts.push("(?:\\/(.*))?")
226
- continue
227
- }
228
-
229
- // Required catch-all: *param
230
- if (segment.startsWith("*")) {
231
- // Required catch-all - matches one or more path segments
232
- regexParts.push("\\/(.+)")
233
- continue
234
- }
235
-
236
- // Dynamic: :param
237
- if (segment.startsWith(":")) {
238
- regexParts.push("\\/([^/]+)")
239
- continue
240
- }
241
-
242
- // Static segment - escape special regex characters
243
- const escaped = segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
244
- regexParts.push(`\\/${escaped}`)
245
- }
246
-
247
- // Build final regex with optional trailing slash
248
- const pattern = `^${regexParts.join("")}\\/?$`
249
- return new RegExp(pattern)
250
- }
251
-
252
- /**
253
- * Generate a route definition from a file path
254
- */
255
- export function generateRouteDefinition(
256
- filePath: string,
257
- pagesDir: string
258
- ): RouteDefinition {
259
- const routePath = filePathToRoutePath(filePath, pagesDir)
260
- const segments = parseRouteSegments(routePath)
261
- const paramNames = extractParamNames(segments)
262
- const score = calculateRouteScore(segments)
263
-
264
- return {
265
- path: routePath,
266
- segments,
267
- paramNames,
268
- score,
269
- filePath
270
- }
271
- }
272
-
273
- /**
274
- * Generate route manifest from pages directory
275
- * Returns route definitions sorted by score (highest first)
276
- */
277
- export function generateRouteManifest(pagesDir: string): RouteDefinition[] {
278
- const pages = discoverPages(pagesDir)
279
-
280
- const definitions = pages.map(filePath =>
281
- generateRouteDefinition(filePath, pagesDir)
282
- )
283
-
284
- // Sort by score descending (highest priority first)
285
- definitions.sort((a, b) => b.score - a.score)
286
-
287
- return definitions
288
- }
289
-
290
- /**
291
- * Generate the route manifest as JavaScript code for runtime
292
- */
293
- export function generateRouteManifestCode(definitions: RouteDefinition[]): string {
294
- const routeEntries = definitions.map(def => {
295
- const regex = routePathToRegex(def.path)
296
-
297
- return ` {
298
- path: ${JSON.stringify(def.path)},
299
- regex: ${regex.toString()},
300
- paramNames: ${JSON.stringify(def.paramNames)},
301
- score: ${def.score},
302
- filePath: ${JSON.stringify(def.filePath)}
303
- }`
304
- })
305
-
306
- return `// Auto-generated route manifest
307
- // Do not edit directly
308
-
309
- export const routeManifest = [
310
- ${routeEntries.join(",\n")}
311
- ];
312
- `
313
- }
314
-