@vojtaholik/static-kit-core 1.0.7 → 2.0.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 +14 -49
- package/src/block-registry.ts +196 -0
- package/src/config.ts +32 -0
- package/src/html-renderer.ts +288 -0
- package/src/index.ts +70 -0
- package/src/layout.ts +27 -0
- package/src/schema-address.ts +63 -0
- package/src/schema.ts +164 -0
- package/src/template-compiler.ts +584 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/plugins/build-plugins.d.ts +0 -9
- package/dist/plugins/build-plugins.d.ts.map +0 -1
- package/dist/plugins/build-plugins.js +0 -271
- package/dist/plugins/index.d.ts +0 -7
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/pages-preview.d.ts +0 -4
- package/dist/plugins/pages-preview.d.ts.map +0 -1
- package/dist/plugins/pages-preview.js +0 -292
- package/dist/plugins/svg-sprite.d.ts +0 -8
- package/dist/plugins/svg-sprite.d.ts.map +0 -1
- package/dist/plugins/svg-sprite.js +0 -98
- package/dist/types.d.ts +0 -27
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
- package/dist/utils/config.d.ts +0 -5
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -34
- package/dist/utils/file-scanner.d.ts +0 -9
- package/dist/utils/file-scanner.d.ts.map +0 -1
- package/dist/utils/file-scanner.js +0 -43
- package/dist/utils/html-imports.d.ts +0 -7
- package/dist/utils/html-imports.d.ts.map +0 -1
- package/dist/utils/html-imports.js +0 -100
- package/dist/utils/typescript-compiler.d.ts +0 -13
- package/dist/utils/typescript-compiler.d.ts.map +0 -1
- package/dist/utils/typescript-compiler.js +0 -61
- package/dist/vite-config.d.ts +0 -5
- package/dist/vite-config.d.ts.map +0 -1
- package/dist/vite-config.js +0 -101
package/package.json
CHANGED
|
@@ -1,62 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vojtaholik/static-kit-core",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Core library for Static Kit - simple static site framework",
|
|
3
|
+
"version": "2.0.0",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
5
|
"exports": {
|
|
9
6
|
".": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
},
|
|
13
|
-
"./vite": {
|
|
14
|
-
"types": "./dist/vite-config.d.ts",
|
|
15
|
-
"import": "./dist/vite-config.js"
|
|
16
|
-
},
|
|
17
|
-
"./plugins": {
|
|
18
|
-
"types": "./dist/plugins/index.d.ts",
|
|
19
|
-
"import": "./dist/plugins/index.js"
|
|
7
|
+
"import": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts"
|
|
20
9
|
}
|
|
21
10
|
},
|
|
22
|
-
"files": [
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"build": "tsc",
|
|
28
|
-
"dev": "tsc --watch",
|
|
29
|
-
"clean": "rm -rf dist"
|
|
11
|
+
"files": ["src"],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/vojtaholik/module-kit",
|
|
15
|
+
"directory": "packages/core"
|
|
30
16
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
33
19
|
},
|
|
34
20
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
},
|
|
38
|
-
"devDependencies": {
|
|
39
|
-
"@types/node": "^24.2.1",
|
|
40
|
-
"typescript": "~5.9.2",
|
|
41
|
-
"vite": "^7.1.1"
|
|
21
|
+
"parse5": "^8.0.0",
|
|
22
|
+
"zod": "^4.1.13"
|
|
42
23
|
},
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"vite",
|
|
46
|
-
"html",
|
|
47
|
-
"scss",
|
|
48
|
-
"typescript",
|
|
49
|
-
"components"
|
|
50
|
-
],
|
|
51
|
-
"author": "Vojta Holik <vojta@holik.dev>",
|
|
52
|
-
"license": "MIT",
|
|
53
|
-
"repository": {
|
|
54
|
-
"type": "git",
|
|
55
|
-
"url": "https://github.com/vojtaholik/static-kit.git",
|
|
56
|
-
"directory": "packages/static-kit-core"
|
|
57
|
-
},
|
|
58
|
-
"homepage": "https://github.com/vojtaholik/static-kit#readme",
|
|
59
|
-
"bugs": {
|
|
60
|
-
"url": "https://github.com/vojtaholik/static-kit/issues"
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"typescript": "^5"
|
|
61
26
|
}
|
|
62
27
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import type { LayoutProps } from "./layout.ts";
|
|
3
|
+
import type { SchemaAddress } from "./schema-address.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context passed to block render functions
|
|
7
|
+
*/
|
|
8
|
+
export interface RenderContext {
|
|
9
|
+
/** Current page ID */
|
|
10
|
+
pageId: string;
|
|
11
|
+
/** Base URL for assets */
|
|
12
|
+
assetBase: string;
|
|
13
|
+
/** Whether we're in dev mode */
|
|
14
|
+
isDev: boolean;
|
|
15
|
+
/** Layout props for styling context */
|
|
16
|
+
layout: LayoutProps;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Input to block render function
|
|
21
|
+
*/
|
|
22
|
+
export interface RenderBlockInput<T extends z.ZodType = z.ZodType> {
|
|
23
|
+
/** Validated block props */
|
|
24
|
+
props: z.infer<T>;
|
|
25
|
+
/** Render context */
|
|
26
|
+
ctx: RenderContext;
|
|
27
|
+
/** Schema address for CMS editing */
|
|
28
|
+
addr: SchemaAddress;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Block definition - ties together type, schema, and render function
|
|
33
|
+
*/
|
|
34
|
+
export interface BlockDefinition<T extends z.ZodType = z.ZodType> {
|
|
35
|
+
type: string;
|
|
36
|
+
propsSchema: T;
|
|
37
|
+
renderHtml: (input: RenderBlockInput<T>) => string;
|
|
38
|
+
/** Source file path (for dev inspector) - pass import.meta.url */
|
|
39
|
+
sourceFile?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Define a block with type-safe props
|
|
44
|
+
* Pass `sourceFile: import.meta.url` to enable click-to-open in dev inspector
|
|
45
|
+
*/
|
|
46
|
+
export function defineBlock<T extends z.ZodType>(config: {
|
|
47
|
+
type: string;
|
|
48
|
+
propsSchema: T;
|
|
49
|
+
renderHtml: (input: RenderBlockInput<T>) => string;
|
|
50
|
+
/** Source file path - pass import.meta.url */
|
|
51
|
+
sourceFile?: string;
|
|
52
|
+
}): BlockDefinition<T> {
|
|
53
|
+
return {
|
|
54
|
+
type: config.type,
|
|
55
|
+
propsSchema: config.propsSchema,
|
|
56
|
+
renderHtml: config.renderHtml,
|
|
57
|
+
sourceFile: config.sourceFile,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Registry of all block definitions
|
|
63
|
+
*/
|
|
64
|
+
export class BlockRegistry {
|
|
65
|
+
private blocks = new Map<string, BlockDefinition>();
|
|
66
|
+
|
|
67
|
+
register<T extends z.ZodType>(definition: BlockDefinition<T>): void {
|
|
68
|
+
if (this.blocks.has(definition.type)) {
|
|
69
|
+
throw new Error(`Block type "${definition.type}" is already registered`);
|
|
70
|
+
}
|
|
71
|
+
this.blocks.set(definition.type, definition as BlockDefinition);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get(type: string): BlockDefinition | undefined {
|
|
75
|
+
return this.blocks.get(type);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getOrThrow(type: string): BlockDefinition {
|
|
79
|
+
const block = this.blocks.get(type);
|
|
80
|
+
if (!block) {
|
|
81
|
+
throw new Error(`Unknown block type: "${type}"`);
|
|
82
|
+
}
|
|
83
|
+
return block;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
has(type: string): boolean {
|
|
87
|
+
return this.blocks.has(type);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
all(): BlockDefinition[] {
|
|
91
|
+
return Array.from(this.blocks.values());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
types(): string[] {
|
|
95
|
+
return Array.from(this.blocks.keys());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
clear(): void {
|
|
99
|
+
this.blocks.clear();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Global block registry instance
|
|
105
|
+
*/
|
|
106
|
+
export const blockRegistry = new BlockRegistry();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* HTML escape helper for templates
|
|
110
|
+
*/
|
|
111
|
+
export function escapeHtml(str: unknown): string {
|
|
112
|
+
if (str === null || str === undefined) return "";
|
|
113
|
+
return String(str)
|
|
114
|
+
.replace(/&/g, "&")
|
|
115
|
+
.replace(/</g, "<")
|
|
116
|
+
.replace(/>/g, ">")
|
|
117
|
+
.replace(/"/g, """)
|
|
118
|
+
.replace(/'/g, "'");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Escape for use in HTML attributes
|
|
123
|
+
*/
|
|
124
|
+
export function escapeAttr(str: unknown): string {
|
|
125
|
+
return escapeHtml(str);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Format a slot error for dev overlay display
|
|
130
|
+
*/
|
|
131
|
+
function formatSlotError(
|
|
132
|
+
type: "not-found" | "validation",
|
|
133
|
+
blockType: string,
|
|
134
|
+
addr: SchemaAddress,
|
|
135
|
+
details?: string
|
|
136
|
+
): string {
|
|
137
|
+
const errorData = JSON.stringify({
|
|
138
|
+
type,
|
|
139
|
+
blockType,
|
|
140
|
+
addr,
|
|
141
|
+
details,
|
|
142
|
+
});
|
|
143
|
+
// Hidden element that dev overlay will pick up
|
|
144
|
+
return `<script type="application/json" data-slot-error>${escapeHtml(
|
|
145
|
+
errorData
|
|
146
|
+
)}</script>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render a slot with optional block delegation
|
|
151
|
+
*
|
|
152
|
+
* If blockType is provided and valid, renders that block with slotProps.
|
|
153
|
+
* Otherwise, renders the fallback content.
|
|
154
|
+
*
|
|
155
|
+
* @param blockType - Block type string to delegate rendering to (optional)
|
|
156
|
+
* @param slotProps - Props to pass to the delegated block
|
|
157
|
+
* @param ctx - Render context
|
|
158
|
+
* @param addr - Schema address for CMS editing
|
|
159
|
+
* @param fallback - Function that returns fallback HTML if no block specified
|
|
160
|
+
*/
|
|
161
|
+
export function renderSlot(
|
|
162
|
+
blockType: string | undefined,
|
|
163
|
+
slotProps: Record<string, unknown>,
|
|
164
|
+
ctx: RenderContext,
|
|
165
|
+
addr: SchemaAddress,
|
|
166
|
+
fallback: () => string
|
|
167
|
+
): string {
|
|
168
|
+
if (!blockType) return fallback();
|
|
169
|
+
|
|
170
|
+
const block = blockRegistry.get(blockType);
|
|
171
|
+
if (!block) {
|
|
172
|
+
const msg = `renderSlot: Unknown block type "${blockType}", using fallback`;
|
|
173
|
+
console.warn(msg);
|
|
174
|
+
const errorHtml = ctx.isDev
|
|
175
|
+
? formatSlotError("not-found", blockType, addr)
|
|
176
|
+
: "";
|
|
177
|
+
return errorHtml + fallback();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = block.propsSchema.safeParse(slotProps);
|
|
181
|
+
if (!result.success) {
|
|
182
|
+
const issues = result.error.issues
|
|
183
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
184
|
+
.join("; ");
|
|
185
|
+
console.warn(
|
|
186
|
+
`renderSlot: Props validation failed for "${blockType}", using fallback:`,
|
|
187
|
+
issues
|
|
188
|
+
);
|
|
189
|
+
const errorHtml = ctx.isDev
|
|
190
|
+
? formatSlotError("validation", blockType, addr, issues)
|
|
191
|
+
: "";
|
|
192
|
+
return errorHtml + fallback();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return block.renderHtml({ props: result.data, ctx, addr });
|
|
196
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static Kit configuration schema
|
|
5
|
+
*/
|
|
6
|
+
export const configSchema = z.object({
|
|
7
|
+
/** Directory containing block definitions and templates */
|
|
8
|
+
blocksDir: z.string().default("blocks"),
|
|
9
|
+
/** Directory containing page configs and templates */
|
|
10
|
+
pagesDir: z.string().default("site/pages"),
|
|
11
|
+
/** Source directory for public assets (css, js, images) - structure mirrors output */
|
|
12
|
+
publicDir: z.string().default("public"),
|
|
13
|
+
/** Output directory for built site */
|
|
14
|
+
outDir: z.string().default("dist"),
|
|
15
|
+
/** URL path prefix where public assets are served */
|
|
16
|
+
publicPath: z.string().default("/public"),
|
|
17
|
+
/** Dev server port */
|
|
18
|
+
devPort: z.number().default(3000),
|
|
19
|
+
/** Path to cms-blocks file (relative to project root) */
|
|
20
|
+
cmsBlocksFile: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type StaticKitConfig = z.infer<typeof configSchema>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Define a Static Kit configuration with type-safe defaults
|
|
27
|
+
*/
|
|
28
|
+
export function defineConfig(
|
|
29
|
+
config: z.input<typeof configSchema> = {}
|
|
30
|
+
): StaticKitConfig {
|
|
31
|
+
return configSchema.parse(config);
|
|
32
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import * as parse5 from "parse5";
|
|
2
|
+
import type { LayoutProps } from "./layout.ts";
|
|
3
|
+
import { layoutPropsSchema } from "./layout.ts";
|
|
4
|
+
import { blockRegistry, type RenderContext } from "./block-registry.ts";
|
|
5
|
+
import type { SchemaAddress } from "./schema-address.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Block instance in a page config
|
|
9
|
+
*/
|
|
10
|
+
export interface BlockInstance {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
props: Record<string, unknown>;
|
|
14
|
+
layout?: Partial<LayoutProps>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Region config - blocks in a named region
|
|
19
|
+
*/
|
|
20
|
+
export interface RegionConfig {
|
|
21
|
+
blocks: BlockInstance[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Page configuration
|
|
26
|
+
*/
|
|
27
|
+
export interface PageConfig {
|
|
28
|
+
id: string;
|
|
29
|
+
path: string;
|
|
30
|
+
title: string;
|
|
31
|
+
template: string;
|
|
32
|
+
density?: "compact" | "comfortable" | "relaxed";
|
|
33
|
+
regions: Record<string, RegionConfig>;
|
|
34
|
+
meta?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Element {
|
|
38
|
+
nodeName: string;
|
|
39
|
+
tagName?: string;
|
|
40
|
+
attrs?: Array<{ name: string; value: string }>;
|
|
41
|
+
childNodes?: Node[];
|
|
42
|
+
value?: string;
|
|
43
|
+
data?: string;
|
|
44
|
+
parentNode?: Node;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Node = Element;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for rendering a page
|
|
51
|
+
*/
|
|
52
|
+
export interface RenderPageOptions {
|
|
53
|
+
/** Directory containing page templates */
|
|
54
|
+
templateDir: string;
|
|
55
|
+
/** Whether we're in dev mode (injects dev overlay) */
|
|
56
|
+
isDev?: boolean;
|
|
57
|
+
/** Base URL for assets */
|
|
58
|
+
assetBase?: string;
|
|
59
|
+
/** Function to read template file content */
|
|
60
|
+
readFile?: (path: string) => Promise<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render a page from its config
|
|
65
|
+
*/
|
|
66
|
+
export async function renderPage(
|
|
67
|
+
page: PageConfig,
|
|
68
|
+
options: RenderPageOptions
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const templatePath = `${options.templateDir}/${page.template}`;
|
|
71
|
+
|
|
72
|
+
// Use provided readFile or default to Bun.file
|
|
73
|
+
const readFile =
|
|
74
|
+
options.readFile ?? (async (path: string) => await Bun.file(path).text());
|
|
75
|
+
const templateHtml = await readFile(templatePath);
|
|
76
|
+
|
|
77
|
+
// Parse the template
|
|
78
|
+
const document = parse5.parse(templateHtml) as Element;
|
|
79
|
+
|
|
80
|
+
// Track regions and their rendered content
|
|
81
|
+
const regionContent: Record<string, string> = {};
|
|
82
|
+
|
|
83
|
+
// Find and update elements
|
|
84
|
+
walkTree(document, (node) => {
|
|
85
|
+
// Update <title>
|
|
86
|
+
if (node.nodeName === "title") {
|
|
87
|
+
const textNode = node.childNodes?.[0];
|
|
88
|
+
if (textNode && textNode.nodeName === "#text") {
|
|
89
|
+
textNode.value = page.title;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Update <html> attributes
|
|
94
|
+
if (node.nodeName === "html") {
|
|
95
|
+
setAttr(node, "data-page-id", page.id);
|
|
96
|
+
// if (page.density) {
|
|
97
|
+
// setAttr(node, "data-density", page.density);
|
|
98
|
+
// }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Process regions - inject a marker that we'll replace after serialization
|
|
102
|
+
const regionName = getAttr(node, "data-region");
|
|
103
|
+
if (regionName) {
|
|
104
|
+
const regionConfig = page.regions[regionName];
|
|
105
|
+
if (regionConfig) {
|
|
106
|
+
// Render blocks for this region
|
|
107
|
+
regionContent[regionName] = renderRegionBlocks(regionConfig, {
|
|
108
|
+
pageId: page.id,
|
|
109
|
+
region: regionName,
|
|
110
|
+
isDev: options.isDev ?? false,
|
|
111
|
+
assetBase: options.assetBase ?? "/",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Insert a marker that won't get escaped
|
|
115
|
+
node.childNodes = [
|
|
116
|
+
{
|
|
117
|
+
nodeName: "#comment",
|
|
118
|
+
data: `__REGION_CONTENT_${regionName}__`,
|
|
119
|
+
} as Node,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Serialize to HTML
|
|
126
|
+
let html = parse5.serialize(
|
|
127
|
+
document as unknown as parse5.DefaultTreeAdapterMap["parentNode"]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Replace markers with actual region content
|
|
131
|
+
for (const [regionName, content] of Object.entries(regionContent)) {
|
|
132
|
+
const marker = `<!--__REGION_CONTENT_${regionName}__-->`;
|
|
133
|
+
html = html.replace(marker, content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Inject dev overlay if in dev mode
|
|
137
|
+
if (options.isDev) {
|
|
138
|
+
html = injectDevOverlay(html);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return html;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Render all blocks in a region
|
|
146
|
+
*/
|
|
147
|
+
function renderRegionBlocks(
|
|
148
|
+
region: RegionConfig,
|
|
149
|
+
context: {
|
|
150
|
+
pageId: string;
|
|
151
|
+
region: string;
|
|
152
|
+
isDev: boolean;
|
|
153
|
+
assetBase: string;
|
|
154
|
+
}
|
|
155
|
+
): string {
|
|
156
|
+
let html = "";
|
|
157
|
+
|
|
158
|
+
for (const block of region.blocks) {
|
|
159
|
+
const definition = blockRegistry.get(block.type);
|
|
160
|
+
if (!definition) {
|
|
161
|
+
console.warn(`Unknown block type: ${block.type}`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Validate and parse props
|
|
166
|
+
const propsResult = definition.propsSchema.safeParse(block.props);
|
|
167
|
+
if (!propsResult.success) {
|
|
168
|
+
const issues = propsResult.error.issues
|
|
169
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
170
|
+
.join("; ");
|
|
171
|
+
console.warn(`Invalid props for block ${block.id}: ${issues}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build layout props
|
|
176
|
+
const layout = layoutPropsSchema.parse(block.layout ?? {});
|
|
177
|
+
|
|
178
|
+
// Build render context
|
|
179
|
+
const ctx: RenderContext = {
|
|
180
|
+
pageId: context.pageId,
|
|
181
|
+
assetBase: context.assetBase,
|
|
182
|
+
isDev: context.isDev,
|
|
183
|
+
layout,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Build schema address
|
|
187
|
+
const addr: SchemaAddress = {
|
|
188
|
+
pageId: context.pageId,
|
|
189
|
+
region: context.region,
|
|
190
|
+
blockId: block.id,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Render the block
|
|
194
|
+
html += definition.renderHtml({
|
|
195
|
+
props: propsResult.data,
|
|
196
|
+
ctx,
|
|
197
|
+
addr,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return html;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Walk the parse5 tree
|
|
206
|
+
*/
|
|
207
|
+
function walkTree(node: Node, callback: (node: Node) => void): void {
|
|
208
|
+
callback(node);
|
|
209
|
+
if (node.childNodes) {
|
|
210
|
+
for (const child of node.childNodes) {
|
|
211
|
+
walkTree(child, callback);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get attribute value
|
|
218
|
+
*/
|
|
219
|
+
function getAttr(node: Node, name: string): string | undefined {
|
|
220
|
+
const element = node as Element;
|
|
221
|
+
const attr = element.attrs?.find((a) => a.name === name);
|
|
222
|
+
return attr?.value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Set attribute value
|
|
227
|
+
*/
|
|
228
|
+
function setAttr(node: Node, name: string, value: string): void {
|
|
229
|
+
const element = node as Element;
|
|
230
|
+
if (!element.attrs) {
|
|
231
|
+
element.attrs = [];
|
|
232
|
+
}
|
|
233
|
+
const existing = element.attrs.find((a) => a.name === name);
|
|
234
|
+
if (existing) {
|
|
235
|
+
existing.value = value;
|
|
236
|
+
} else {
|
|
237
|
+
element.attrs.push({ name, value });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Inject dev overlay script
|
|
243
|
+
*/
|
|
244
|
+
function injectDevOverlay(html: string): string {
|
|
245
|
+
const script = `<script src="/__dev-overlay.js"></script>`;
|
|
246
|
+
return html.replace("</body>", `${script}</body>`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Render a standalone block (for API/preview)
|
|
251
|
+
*/
|
|
252
|
+
export function renderBlock(
|
|
253
|
+
block: BlockInstance,
|
|
254
|
+
context: {
|
|
255
|
+
pageId: string;
|
|
256
|
+
region: string;
|
|
257
|
+
isDev: boolean;
|
|
258
|
+
assetBase: string;
|
|
259
|
+
}
|
|
260
|
+
): string {
|
|
261
|
+
const definition = blockRegistry.getOrThrow(block.type);
|
|
262
|
+
|
|
263
|
+
const propsResult = definition.propsSchema.safeParse(block.props);
|
|
264
|
+
if (!propsResult.success) {
|
|
265
|
+
throw new Error(`Invalid props: ${propsResult.error.message}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const layout = layoutPropsSchema.parse(block.layout ?? {});
|
|
269
|
+
|
|
270
|
+
const ctx: RenderContext = {
|
|
271
|
+
pageId: context.pageId,
|
|
272
|
+
assetBase: context.assetBase,
|
|
273
|
+
isDev: context.isDev,
|
|
274
|
+
layout,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const addr: SchemaAddress = {
|
|
278
|
+
pageId: context.pageId,
|
|
279
|
+
region: context.region,
|
|
280
|
+
blockId: block.id,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return definition.renderHtml({
|
|
284
|
+
props: propsResult.data,
|
|
285
|
+
ctx,
|
|
286
|
+
addr,
|
|
287
|
+
});
|
|
288
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Layout schemas and types
|
|
2
|
+
export {
|
|
3
|
+
toneEnum,
|
|
4
|
+
contentAlignEnum,
|
|
5
|
+
densityEnum,
|
|
6
|
+
contentWidthEnum,
|
|
7
|
+
layoutPropsSchema,
|
|
8
|
+
type Tone,
|
|
9
|
+
type ContentAlign,
|
|
10
|
+
type Density,
|
|
11
|
+
type ContentWidth,
|
|
12
|
+
type LayoutProps,
|
|
13
|
+
} from "./layout.ts";
|
|
14
|
+
|
|
15
|
+
// Schema address utilities
|
|
16
|
+
export {
|
|
17
|
+
schemaAddressSchema,
|
|
18
|
+
encodeSchemaAddress,
|
|
19
|
+
decodeSchemaAddress,
|
|
20
|
+
withPropPath,
|
|
21
|
+
isSameBlock,
|
|
22
|
+
type SchemaAddress,
|
|
23
|
+
} from "./schema-address.ts";
|
|
24
|
+
|
|
25
|
+
// CMS schema utilities
|
|
26
|
+
export {
|
|
27
|
+
cmsFieldTypeEnum,
|
|
28
|
+
cmsFieldSchema,
|
|
29
|
+
cmsBlockSchemaMapSchema,
|
|
30
|
+
createSchemaFromCmsFields,
|
|
31
|
+
createSchemaFromCmsBlocks,
|
|
32
|
+
type CmsFieldType,
|
|
33
|
+
type CmsField,
|
|
34
|
+
type CmsBlockSchema,
|
|
35
|
+
type CmsBlockSchemaMap,
|
|
36
|
+
} from "./schema.ts";
|
|
37
|
+
|
|
38
|
+
// Block registry
|
|
39
|
+
export {
|
|
40
|
+
defineBlock,
|
|
41
|
+
BlockRegistry,
|
|
42
|
+
blockRegistry,
|
|
43
|
+
escapeHtml,
|
|
44
|
+
escapeAttr,
|
|
45
|
+
renderSlot,
|
|
46
|
+
type RenderContext,
|
|
47
|
+
type RenderBlockInput,
|
|
48
|
+
type BlockDefinition,
|
|
49
|
+
} from "./block-registry.ts";
|
|
50
|
+
|
|
51
|
+
// HTML renderer
|
|
52
|
+
export {
|
|
53
|
+
renderPage,
|
|
54
|
+
renderBlock,
|
|
55
|
+
type BlockInstance,
|
|
56
|
+
type RegionConfig,
|
|
57
|
+
type PageConfig,
|
|
58
|
+
type RenderPageOptions,
|
|
59
|
+
} from "./html-renderer.ts";
|
|
60
|
+
|
|
61
|
+
// Template compiler
|
|
62
|
+
export {
|
|
63
|
+
compileBlockTemplates,
|
|
64
|
+
compileTemplateFile,
|
|
65
|
+
compileTemplate,
|
|
66
|
+
type CompileOptions,
|
|
67
|
+
} from "./template-compiler.ts";
|
|
68
|
+
|
|
69
|
+
// Configuration
|
|
70
|
+
export { configSchema, defineConfig, type StaticKitConfig } from "./config.ts";
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
// Layout enums - design system tokens for blocks
|
|
4
|
+
export const toneEnum = z.enum(["surface", "raised", "accent", "inverted"]);
|
|
5
|
+
export const contentAlignEnum = z.enum([
|
|
6
|
+
"left",
|
|
7
|
+
"center",
|
|
8
|
+
"right",
|
|
9
|
+
"split-start",
|
|
10
|
+
"split-end",
|
|
11
|
+
]);
|
|
12
|
+
export const densityEnum = z.enum(["compact", "comfortable", "relaxed"]);
|
|
13
|
+
export const contentWidthEnum = z.enum(["narrow", "default", "wide"]);
|
|
14
|
+
|
|
15
|
+
// Combined layout props schema for block rendering context
|
|
16
|
+
export const layoutPropsSchema = z.object({
|
|
17
|
+
tone: toneEnum.default("surface"),
|
|
18
|
+
contentAlign: contentAlignEnum.default("left"),
|
|
19
|
+
density: densityEnum.default("comfortable"),
|
|
20
|
+
contentWidth: contentWidthEnum.default("default"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type Tone = z.infer<typeof toneEnum>;
|
|
24
|
+
export type ContentAlign = z.infer<typeof contentAlignEnum>;
|
|
25
|
+
export type Density = z.infer<typeof densityEnum>;
|
|
26
|
+
export type ContentWidth = z.infer<typeof contentWidthEnum>;
|
|
27
|
+
export type LayoutProps = z.infer<typeof layoutPropsSchema>;
|