@vojtaholik/static-kit-cli 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 ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@vojtaholik/static-kit-cli",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "static-kit": "./src/cli.ts"
7
+ },
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/vojtaholik/module-kit",
14
+ "directory": "packages/cli"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@vojtaholik/static-kit-core": "^2.0.0",
21
+ "lightningcss": "^1.28.2",
22
+ "browserslist": "^4.24.4"
23
+ },
24
+ "peerDependencies": {
25
+ "typescript": "^5"
26
+ }
27
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Static Block Kit CLI
4
+ *
5
+ * Commands:
6
+ * dev - Start development server
7
+ * build - Build static site for production
8
+ * gen - Compile block templates
9
+ * sprite - Compile SVGs into spritesheet
10
+ */
11
+
12
+ const command = process.argv[2];
13
+
14
+ switch (command) {
15
+ case "dev":
16
+ await import("./commands/dev.ts");
17
+ break;
18
+ case "build":
19
+ await import("./commands/build.ts");
20
+ break;
21
+ case "gen":
22
+ await import("./commands/gen.ts");
23
+ break;
24
+ case "sprite":
25
+ await import("./commands/sprite.ts");
26
+ break;
27
+ case "--help":
28
+ case "-h":
29
+ case undefined:
30
+ console.log(`
31
+ Static Block Kit CLI
32
+
33
+ Usage:
34
+ static-block-kit <command>
35
+
36
+ Commands:
37
+ dev Start development server with hot reload
38
+ build Build static site for production
39
+ gen Compile block templates to render functions
40
+ sprite Compile SVGs from svg/ into spritesheet
41
+
42
+ Options:
43
+ --help, -h Show this help message
44
+ `);
45
+ break;
46
+ default:
47
+ console.error(`Unknown command: ${command}`);
48
+ console.error("Run 'static-block-kit --help' for available commands");
49
+ process.exit(1);
50
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Static Kit - Build Pipeline
4
+ *
5
+ * 1. Run template compiler (gen-blocks)
6
+ * 2. Load and validate all page configs
7
+ * 3. Render each page to HTML (no dev overlay)
8
+ * 4. Copy publicDir to dist/{publicPath}/ (1:1 structure)
9
+ * 5. Write HTML files to dist/ (flat structure)
10
+ */
11
+
12
+ import { mkdir, rm, cp } from "node:fs/promises";
13
+ import { join, dirname } from "node:path";
14
+ import { Glob } from "bun";
15
+ import {
16
+ renderPage,
17
+ compileBlockTemplates,
18
+ type PageConfig,
19
+ } from "@vojtaholik/static-kit-core";
20
+ import { loadConfig, resolvePath } from "../config-loader.ts";
21
+ import { processCSS } from "../css-processor.ts";
22
+ import { compileSpritesheet } from "../sprite-compiler.ts";
23
+
24
+ const cwd = process.cwd();
25
+ const config = await loadConfig(cwd);
26
+
27
+ const blocksDir = resolvePath(config, "blocksDir", cwd);
28
+ const pagesDir = resolvePath(config, "pagesDir", cwd);
29
+ const publicDir = resolvePath(config, "publicDir", cwd);
30
+ const outDir = resolvePath(config, "outDir", cwd);
31
+
32
+ // Strip leading slash from publicPath for filesystem operations
33
+ const publicPathDir = config.publicPath.replace(/^\//, "");
34
+
35
+ async function build() {
36
+ console.log("🔨 Building static site...\n");
37
+
38
+ // Clean dist directory
39
+ console.log("Cleaning output directory...");
40
+ await rm(outDir, { recursive: true, force: true });
41
+ await mkdir(outDir, { recursive: true });
42
+
43
+ // Step 1: Compile templates
44
+ console.log("\n📝 Compiling block templates...");
45
+ await compileBlockTemplates({
46
+ blocksDir,
47
+ genDir: join(blocksDir, "gen"),
48
+ });
49
+
50
+ // Step 1.5: Compile SVG spritesheet
51
+ console.log("\n🎨 Compiling SVG spritesheet...");
52
+ try {
53
+ const { count } = await compileSpritesheet({ publicDir });
54
+ console.log(` ✓ ${count} SVGs compiled`);
55
+ } catch (err) {
56
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
57
+ throw err;
58
+ }
59
+ console.log(" ℹ No svg/ directory found, skipping");
60
+ }
61
+
62
+ // Step 2: Import fresh modules (after template compilation)
63
+ const blocksModule = await import(join(blocksDir, "index.ts"));
64
+ const pagesModule = await import(join(pagesDir, "index.ts"));
65
+
66
+ // Register blocks
67
+ if (typeof blocksModule.registerAllBlocks === "function") {
68
+ blocksModule.registerAllBlocks();
69
+ }
70
+
71
+ const pages: PageConfig[] = pagesModule.pages;
72
+
73
+ // Step 3: Render all pages (flat structure in dist/)
74
+ console.log("\n📄 Rendering pages...");
75
+ for (const page of pages) {
76
+ const html = await renderPage(page, {
77
+ templateDir: pagesDir,
78
+ isDev: false,
79
+ assetBase: "/",
80
+ });
81
+
82
+ // Flat output: /about -> dist/about.html, / -> dist/index.html
83
+ const fileName =
84
+ page.path === "/" ? "index.html" : `${page.path.replace(/^\//, "")}.html`;
85
+ const outPath = join(outDir, fileName);
86
+
87
+ // Write HTML
88
+ await Bun.write(outPath, html);
89
+ console.log(` ✓ ${page.path} → ${outPath}`);
90
+ }
91
+
92
+ // Step 4: Copy publicDir to dist/public/ (1:1 mirrored structure)
93
+ console.log("\n📦 Copying public assets...");
94
+ const outPublicDir = join(outDir, publicPathDir);
95
+
96
+ const publicGlob = new Glob("**/*");
97
+ for await (const file of publicGlob.scan(publicDir)) {
98
+ const srcFile = join(publicDir, file);
99
+ const destFile = join(outPublicDir, file);
100
+
101
+ const bunFile = Bun.file(srcFile);
102
+ if (await bunFile.exists()) {
103
+ await mkdir(dirname(destFile), { recursive: true });
104
+
105
+ // Process CSS files through lightningcss
106
+ if (file.endsWith(".css")) {
107
+ const cssBytes = new Uint8Array(await bunFile.arrayBuffer());
108
+ const result = processCSS({
109
+ filename: srcFile,
110
+ code: cssBytes,
111
+ minify: true,
112
+ });
113
+ await Bun.write(destFile, result.code);
114
+ console.log(` ✓ ${config.publicPath}/${file} (minified)`);
115
+ } else {
116
+ await Bun.write(destFile, await bunFile.arrayBuffer());
117
+ console.log(` ✓ ${config.publicPath}/${file}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ // Done
123
+ console.log("\n✅ Build complete!");
124
+ console.log(` Output: ${outDir}/`);
125
+
126
+ // List output files
127
+ console.log("\n Files:");
128
+ const glob = new Glob("**/*");
129
+ for await (const file of glob.scan(outDir)) {
130
+ const stat = await Bun.file(join(outDir, file)).size;
131
+ console.log(` - ${file} (${formatBytes(stat)})`);
132
+ }
133
+ }
134
+
135
+ function formatBytes(bytes: number): string {
136
+ if (bytes < 1024) return `${bytes} B`;
137
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
138
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
139
+ }
140
+
141
+ build().catch((err) => {
142
+ console.error("Build failed:", err);
143
+ process.exit(1);
144
+ });
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Static Kit - Development Server
4
+ *
5
+ * Bun.serve() based dev server with:
6
+ * - Page routes (rendered HTML with dev overlay)
7
+ * - Static asset serving
8
+ * - Dev API endpoints
9
+ * - Hot reload via SSE
10
+ */
11
+
12
+ import { watch } from "node:fs";
13
+ import { join } from "node:path";
14
+ import {
15
+ decodeSchemaAddress,
16
+ blockRegistry,
17
+ renderPage,
18
+ compileBlockTemplates,
19
+ type PageConfig,
20
+ } from "@vojtaholik/static-kit-core";
21
+ import { loadConfig, resolvePath } from "../config-loader.ts";
22
+ import { processCSS } from "../css-processor.ts";
23
+ import { compileSpritesheet } from "../sprite-compiler.ts";
24
+
25
+ const cwd = process.cwd();
26
+ const config = await loadConfig(cwd);
27
+
28
+ const blocksDir = resolvePath(config, "blocksDir", cwd);
29
+ const pagesDir = resolvePath(config, "pagesDir", cwd);
30
+ const publicDir = resolvePath(config, "publicDir", cwd);
31
+ const svgDir = join(publicDir, "svg");
32
+
33
+ // Compile block templates before importing blocks (gen/ may not exist yet)
34
+ console.log("🔨 Compiling block templates...");
35
+ await compileBlockTemplates({
36
+ blocksDir,
37
+ genDir: join(blocksDir, "gen"),
38
+ });
39
+
40
+ // Compile SVG spritesheet if svg/ directory exists
41
+ try {
42
+ console.log("🎨 Compiling SVG spritesheet...");
43
+ const { count } = await compileSpritesheet({ publicDir });
44
+ if (count === 0) {
45
+ console.log(" (no SVGs found in svg/)");
46
+ }
47
+ } catch (err) {
48
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
49
+ console.warn(" ⚠ Sprite compilation failed:", err);
50
+ }
51
+ // svg/ directory doesn't exist, skip silently
52
+ }
53
+
54
+ // Dynamically import site's blocks and pages
55
+ const blocksModule = await import(join(blocksDir, "index.ts"));
56
+ const pagesModule = await import(join(pagesDir, "index.ts"));
57
+
58
+ // Register all blocks at startup
59
+ if (typeof blocksModule.registerAllBlocks === "function") {
60
+ blocksModule.registerAllBlocks();
61
+ }
62
+
63
+ const pages: PageConfig[] = pagesModule.pages;
64
+ const getPageByPath = pagesModule.getPageByPath as (
65
+ path: string
66
+ ) => PageConfig | undefined;
67
+
68
+ // Try to load cms-blocks if it exists
69
+ let cmsBlocks: Record<string, unknown> = {};
70
+ try {
71
+ const cmsPath = config.cmsBlocksFile
72
+ ? join(cwd, config.cmsBlocksFile)
73
+ : join(cwd, "cms-blocks.ts");
74
+ const cmsBlocksModule = await import(`file://${cmsPath}`);
75
+ cmsBlocks = cmsBlocksModule.cmsBlocks ?? cmsBlocksModule.default ?? {};
76
+ } catch {
77
+ // No cms-blocks file, that's fine
78
+ }
79
+
80
+ const PORT = config.devPort;
81
+ const publicPath = config.publicPath; // e.g. "/public"
82
+
83
+ // Server start time for HMR restart detection
84
+ const SERVER_START_TIME = Date.now();
85
+
86
+ // SSE clients for hot reload
87
+ const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();
88
+ const sseHeartbeats = new Map<
89
+ ReadableStreamDefaultController<Uint8Array>,
90
+ ReturnType<typeof setInterval>
91
+ >();
92
+
93
+ // Debounce file change notifications
94
+ let reloadTimeout: ReturnType<typeof setTimeout> | null = null;
95
+ let lastChangeType: "full" | "css" = "full";
96
+
97
+ function broadcastReload(type: "full" | "css" = "full") {
98
+ // Debounce rapid changes
99
+ if (reloadTimeout) {
100
+ clearTimeout(reloadTimeout);
101
+ // Escalate to full reload if mixed changes
102
+ if (type === "full") lastChangeType = "full";
103
+ } else {
104
+ lastChangeType = type;
105
+ }
106
+
107
+ reloadTimeout = setTimeout(() => {
108
+ reloadTimeout = null;
109
+ const message = `data: ${JSON.stringify({
110
+ type: lastChangeType,
111
+ time: Date.now(),
112
+ })}\n\n`;
113
+ const encoder = new TextEncoder();
114
+ const data = encoder.encode(message);
115
+
116
+ for (const controller of sseClients) {
117
+ try {
118
+ controller.enqueue(data);
119
+ } catch {
120
+ // Clean up both client and heartbeat when controller fails
121
+ const heartbeat = sseHeartbeats.get(controller);
122
+ if (heartbeat) {
123
+ clearInterval(heartbeat);
124
+ sseHeartbeats.delete(controller);
125
+ }
126
+ sseClients.delete(controller);
127
+ }
128
+ }
129
+ console.log(
130
+ `🔄 ${lastChangeType === "css" ? "CSS" : "Full"} reload triggered`
131
+ );
132
+ }, 50);
133
+ }
134
+
135
+ // Watch for file changes
136
+ const watchDirs = [blocksDir, pagesDir, publicDir, svgDir];
137
+
138
+ async function handleFileChange(filename: string, dir: string) {
139
+ // Ignore generated files
140
+ if (filename.includes("/gen/") || filename.endsWith(".render.ts")) return;
141
+
142
+ // SVG in svg/ directory changed - recompile spritesheet
143
+ if (filename.endsWith(".svg") && dir === svgDir) {
144
+ console.log(`🎨 SVG changed: ${filename}`);
145
+ try {
146
+ await compileSpritesheet({ publicDir });
147
+ broadcastReload("css"); // No full reload needed for SVG sprites
148
+ } catch (err) {
149
+ console.warn(" ⚠ Sprite compilation failed:", err);
150
+ }
151
+ return;
152
+ }
153
+
154
+ if (filename.endsWith(".css")) {
155
+ broadcastReload("css");
156
+ return;
157
+ }
158
+
159
+ // HTML template changed - recompile templates before reload
160
+ if (filename.endsWith(".block.html")) {
161
+ console.log(`📝 Template changed: ${filename}`);
162
+ await compileBlockTemplates({
163
+ blocksDir,
164
+ genDir: join(blocksDir, "gen"),
165
+ });
166
+ // bun --watch will restart the server after recompile
167
+ // which triggers browser reload via SSE reconnect
168
+ return;
169
+ }
170
+
171
+ // For other source files, just broadcast (bun --watch handles the restart)
172
+ if (filename.endsWith(".ts") || filename.endsWith(".js")) {
173
+ broadcastReload("full");
174
+ }
175
+ }
176
+
177
+ for (const dir of watchDirs) {
178
+ try {
179
+ watch(dir, { recursive: true }, (event, filename) => {
180
+ if (!filename) return;
181
+ handleFileChange(filename, dir);
182
+ });
183
+ } catch {
184
+ // Directory might not exist yet
185
+ }
186
+ }
187
+
188
+ console.log(`🚀 Starting dev server on http://localhost:${PORT}`);
189
+ console.log(`👀 Watching for changes...`);
190
+
191
+ Bun.serve({
192
+ port: PORT,
193
+ idleTimeout: 120, // Don't kill SSE connections every 10s
194
+ async fetch(req) {
195
+ const url = new URL(req.url);
196
+ const path = url.pathname;
197
+
198
+ try {
199
+ // Dev overlay (special internal asset)
200
+ if (path === "/__dev-overlay.js") {
201
+ const js = await Bun.file(join(publicDir, "js/dev-overlay.js")).text();
202
+ return new Response(js, {
203
+ headers: { "Content-Type": "application/javascript" },
204
+ });
205
+ }
206
+
207
+ // Hot reload SSE endpoint
208
+ if (path === "/__hmr") {
209
+ const stream = new ReadableStream<Uint8Array>({
210
+ start(controller) {
211
+ sseClients.add(controller);
212
+ const encoder = new TextEncoder();
213
+ // Send initial connected message with server start time
214
+ controller.enqueue(
215
+ encoder.encode(
216
+ `data: ${JSON.stringify({
217
+ type: "connected",
218
+ time: SERVER_START_TIME,
219
+ })}\n\n`
220
+ )
221
+ );
222
+ // Heartbeat every 30s to keep connection alive
223
+ const heartbeat = setInterval(() => {
224
+ try {
225
+ controller.enqueue(encoder.encode(": heartbeat\n\n"));
226
+ } catch {
227
+ clearInterval(heartbeat);
228
+ sseHeartbeats.delete(controller);
229
+ }
230
+ }, 30000);
231
+ sseHeartbeats.set(controller, heartbeat);
232
+ },
233
+ cancel(controller) {
234
+ const heartbeat = sseHeartbeats.get(controller);
235
+ if (heartbeat) {
236
+ clearInterval(heartbeat);
237
+ sseHeartbeats.delete(controller);
238
+ }
239
+ sseClients.delete(controller);
240
+ },
241
+ });
242
+
243
+ return new Response(stream, {
244
+ headers: {
245
+ "Content-Type": "text/event-stream",
246
+ "Cache-Control": "no-cache",
247
+ Connection: "keep-alive",
248
+ },
249
+ });
250
+ }
251
+
252
+ // Dev API endpoints
253
+ if (path === "/__pages") {
254
+ return Response.json(
255
+ pages.map((p) => ({
256
+ id: p.id,
257
+ path: p.path,
258
+ title: p.title,
259
+ }))
260
+ );
261
+ }
262
+
263
+ if (path === "/__site") {
264
+ return Response.json({
265
+ pages: pages.map((p) => ({
266
+ id: p.id,
267
+ path: p.path,
268
+ title: p.title,
269
+ })),
270
+ blocks: blockRegistry.types(),
271
+ schemas: cmsBlocks,
272
+ });
273
+ }
274
+
275
+ if (path === "/__inspect") {
276
+ const address = url.searchParams.get("address");
277
+ if (!address) {
278
+ return Response.json(
279
+ { error: "Missing address parameter" },
280
+ { status: 400 }
281
+ );
282
+ }
283
+
284
+ try {
285
+ const decoded = decodeSchemaAddress(address);
286
+ const page = pages.find((p) => p.id === decoded.pageId);
287
+ const region = page?.regions[decoded.region];
288
+ const block = region?.blocks.find((b) => b.id === decoded.blockId);
289
+ const blockDef = block ? blockRegistry.get(block.type) : null;
290
+
291
+ // Convert file:// URL to absolute path if needed
292
+ const sourceFile = blockDef?.sourceFile?.startsWith("file://")
293
+ ? blockDef.sourceFile.replace("file://", "")
294
+ : blockDef?.sourceFile;
295
+
296
+ return Response.json({
297
+ address: decoded,
298
+ page: page
299
+ ? {
300
+ id: page.id,
301
+ path: page.path,
302
+ title: page.title,
303
+ sourceFile: `${pagesDir}/${decoded.pageId}.page.ts`,
304
+ }
305
+ : null,
306
+ block: block
307
+ ? {
308
+ id: block.id,
309
+ type: block.type,
310
+ props: block.props,
311
+ sourceFile,
312
+ templateFile: `${blocksDir}/${block.type}.block.html`,
313
+ }
314
+ : null,
315
+ schema: block ? cmsBlocks[block.type] : null,
316
+ });
317
+ } catch (err) {
318
+ return Response.json(
319
+ { error: "Invalid address", details: String(err) },
320
+ { status: 400 }
321
+ );
322
+ }
323
+ }
324
+
325
+ // Public static files - serve publicDir at publicPath (1:1 mapping)
326
+ if (path.startsWith(`${publicPath}/`)) {
327
+ const relativePath = path.slice(publicPath.length + 1);
328
+ const filePath = join(publicDir, relativePath);
329
+ const file = Bun.file(filePath);
330
+ if (await file.exists()) {
331
+ // Process CSS through lightningcss (no minification in dev)
332
+ if (filePath.endsWith(".css")) {
333
+ const cssBytes = new Uint8Array(await file.arrayBuffer());
334
+ const result = processCSS({
335
+ filename: filePath,
336
+ code: cssBytes,
337
+ minify: false,
338
+ });
339
+ return new Response(new TextDecoder().decode(result.code), {
340
+ headers: {
341
+ "Content-Type": "text/css",
342
+ "Cache-Control": "no-cache, no-store, must-revalidate",
343
+ },
344
+ });
345
+ }
346
+ return new Response(file, {
347
+ headers: { "Cache-Control": "no-cache, no-store, must-revalidate" },
348
+ });
349
+ }
350
+ }
351
+
352
+ // Page routes
353
+ const pagePath = path === "/" ? "/" : path.replace(/\/$/, "");
354
+ const page = getPageByPath(pagePath);
355
+
356
+ if (page) {
357
+ const html = await renderPage(page, {
358
+ templateDir: pagesDir,
359
+ isDev: true,
360
+ assetBase: "/",
361
+ });
362
+
363
+ return new Response(html, {
364
+ headers: { "Content-Type": "text/html" },
365
+ });
366
+ }
367
+
368
+ // 404
369
+ return new Response("Not Found", { status: 404 });
370
+ } catch (err) {
371
+ console.error("Error handling request:", err);
372
+ return new Response(
373
+ `<html>
374
+ <head><title>Error</title></head>
375
+ <body style="font-family: system-ui; padding: 2rem;">
376
+ <h1>Server Error</h1>
377
+ <pre style="background: #f5f5f5; padding: 1rem; overflow: auto;">${String(
378
+ err
379
+ )}</pre>
380
+ </body>
381
+ </html>`,
382
+ {
383
+ status: 500,
384
+ headers: { "Content-Type": "text/html" },
385
+ }
386
+ );
387
+ }
388
+ },
389
+ });
390
+
391
+ console.log(`
392
+ Dev server running at http://localhost:${PORT}
393
+
394
+ Pages: ${pagesDir}
395
+ Public: ${publicDir} → ${publicPath}/
396
+
397
+ Dev API:
398
+ /__pages → List all pages
399
+ /__site → Site config (pages, blocks, schemas)
400
+ /__inspect → Decode schema address
401
+ `);
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Generate block render functions from templates
4
+ */
5
+
6
+ import { join } from "node:path";
7
+ import { compileBlockTemplates } from "@vojtaholik/static-kit-core";
8
+ import { loadConfig } from "../config-loader.ts";
9
+
10
+ const cwd = process.cwd();
11
+ const config = await loadConfig(cwd);
12
+
13
+ console.log("🔨 Compiling block templates...");
14
+
15
+ await compileBlockTemplates({
16
+ blocksDir: join(cwd, config.blocksDir),
17
+ genDir: join(cwd, config.blocksDir, "gen"),
18
+ });
19
+
20
+ console.log("✅ Done!");
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Static Kit - SVG Sprite CLI Command
4
+ *
5
+ * Compiles individual SVG files from publicDir/svg/ into a single spritesheet.
6
+ *
7
+ * Usage in HTML:
8
+ * <svg><use href="/public/sprite.svg#icon-name"/></svg>
9
+ */
10
+
11
+ import { loadConfig, resolvePath } from "../config-loader.ts";
12
+ import { compileSpritesheet } from "../sprite-compiler.ts";
13
+
14
+ const cwd = process.cwd();
15
+ const config = await loadConfig(cwd);
16
+ const publicDir = resolvePath(config, "publicDir", cwd);
17
+
18
+ console.log("🎨 Compiling SVG spritesheet...\n");
19
+
20
+ try {
21
+ const { count, outputPath } = await compileSpritesheet({ publicDir });
22
+ console.log(` ✓ Compiled ${count} SVGs → ${outputPath}`);
23
+ console.log("\n✅ Sprite compilation complete!");
24
+ } catch (err) {
25
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
26
+ console.log(" ℹ No svg/ directory found, skipping sprite compilation");
27
+ } else {
28
+ console.error("Sprite compilation failed:", err);
29
+ process.exit(1);
30
+ }
31
+ }
@@ -0,0 +1,43 @@
1
+ import { join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { configSchema, type StaticKitConfig } from "@vojtaholik/static-kit-core";
4
+
5
+ /**
6
+ * Load static-kit.config.ts from the current working directory
7
+ */
8
+ export async function loadConfig(
9
+ cwd = process.cwd()
10
+ ): Promise<StaticKitConfig> {
11
+ const configPath = join(cwd, "static-kit.config.ts");
12
+
13
+ // Check if file exists first
14
+ if (!existsSync(configPath)) {
15
+ console.log("No static-kit.config.ts found, using defaults");
16
+ return configSchema.parse({});
17
+ }
18
+
19
+ try {
20
+ // Use file:// URL for dynamic imports
21
+ const configModule = await import(`file://${configPath}`);
22
+ const rawConfig = configModule.default ?? configModule;
23
+ return configSchema.parse(rawConfig);
24
+ } catch (err) {
25
+ console.error("Error loading config:", err);
26
+ throw err;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Resolve a path relative to the project root
32
+ */
33
+ export function resolvePath(
34
+ config: StaticKitConfig,
35
+ key: keyof StaticKitConfig,
36
+ cwd = process.cwd()
37
+ ): string {
38
+ const value = config[key];
39
+ if (typeof value !== "string") {
40
+ throw new Error(`Config key ${key} is not a string`);
41
+ }
42
+ return join(cwd, value);
43
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * CSS Processing with lightningcss
3
+ *
4
+ * Handles:
5
+ * - CSS nesting (native)
6
+ * - Custom media queries
7
+ * - Vendor prefixes (autoprefixer)
8
+ * - Minification (build only)
9
+ */
10
+
11
+ import { transform, browserslistToTargets } from "lightningcss";
12
+ import browserslist from "browserslist";
13
+
14
+ // Cache the targets since browserslist resolution is expensive
15
+ let cachedTargets: ReturnType<typeof browserslistToTargets> | null = null;
16
+
17
+ function getTargets() {
18
+ if (!cachedTargets) {
19
+ cachedTargets = browserslistToTargets(browserslist("> 0.5%, not dead"));
20
+ }
21
+ return cachedTargets;
22
+ }
23
+
24
+ export interface CSSProcessOptions {
25
+ /** Enable minification (typically false for dev, true for build) */
26
+ minify?: boolean;
27
+ /** Filename for error reporting and source maps */
28
+ filename: string;
29
+ /** Raw CSS content as Uint8Array */
30
+ code: Uint8Array;
31
+ }
32
+
33
+ export interface CSSProcessResult {
34
+ code: Uint8Array;
35
+ map: Uint8Array | undefined;
36
+ }
37
+
38
+ /**
39
+ * Process CSS through lightningcss
40
+ *
41
+ * Features enabled:
42
+ * - CSS nesting (lowered for browser compat)
43
+ * - Custom media queries draft spec
44
+ * - Vendor prefixes based on browserslist targets
45
+ */
46
+ export function processCSS(options: CSSProcessOptions): CSSProcessResult {
47
+ const result = transform({
48
+ filename: options.filename,
49
+ code: options.code,
50
+ minify: options.minify ?? false,
51
+ targets: getTargets(),
52
+ drafts: {
53
+ customMedia: true,
54
+ },
55
+ // Error on unknown at-rules instead of passing through
56
+ errorRecovery: false,
57
+ });
58
+
59
+ return {
60
+ code: result.code,
61
+ map: result.map ?? undefined,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Process CSS from a string (convenience wrapper)
67
+ */
68
+ export function processCSSString(
69
+ css: string,
70
+ options: Omit<CSSProcessOptions, "code">
71
+ ): string {
72
+ const encoder = new TextEncoder();
73
+ const decoder = new TextDecoder();
74
+
75
+ const result = processCSS({
76
+ ...options,
77
+ code: encoder.encode(css),
78
+ });
79
+
80
+ return decoder.decode(result.code);
81
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * SVG Sprite Compiler
3
+ *
4
+ * Compiles individual SVG files into a single spritesheet.
5
+ * Each SVG becomes a <symbol> element with id matching the filename.
6
+ */
7
+
8
+ import { join, basename } from "node:path";
9
+ import { Glob } from "bun";
10
+
11
+ export interface SpriteCompileOptions {
12
+ publicDir: string;
13
+ /** Relative path within publicDir for source SVGs */
14
+ svgDir?: string;
15
+ /** Output filename within publicDir */
16
+ outputFile?: string;
17
+ }
18
+
19
+ /**
20
+ * Parse an SVG string and extract viewBox + inner content
21
+ */
22
+ function parseSvg(content: string): { viewBox: string; inner: string } | null {
23
+ // Extract viewBox attribute
24
+ const viewBoxMatch = content.match(/viewBox=["']([^"']+)["']/);
25
+ const viewBox = viewBoxMatch?.[1] ?? "0 0 24 24";
26
+
27
+ // Extract content between <svg> tags
28
+ const svgMatch = content.match(/<svg[^>]*>([\s\S]*)<\/svg>/i);
29
+ if (!svgMatch) return null;
30
+
31
+ const inner = svgMatch[1]?.trim() ?? "";
32
+ return { viewBox, inner };
33
+ }
34
+
35
+ /**
36
+ * Compile SVGs from a directory into a spritesheet
37
+ */
38
+ export async function compileSpritesheet(
39
+ options: SpriteCompileOptions
40
+ ): Promise<{ count: number; outputPath: string }> {
41
+ const { publicDir, svgDir = "svg", outputFile = "sprite.svg" } = options;
42
+
43
+ const inputDir = join(publicDir, svgDir);
44
+ const outputPath = join(publicDir, outputFile);
45
+
46
+ const symbols: string[] = [];
47
+ const glob = new Glob("*.svg");
48
+
49
+ for await (const file of glob.scan(inputDir)) {
50
+ const filePath = join(inputDir, file);
51
+ const content = await Bun.file(filePath).text();
52
+
53
+ const parsed = parseSvg(content);
54
+ if (!parsed) {
55
+ console.warn(` ⚠ Skipping ${file}: could not parse SVG`);
56
+ continue;
57
+ }
58
+
59
+ // Use filename without extension as symbol id
60
+ const id = basename(file, ".svg");
61
+ symbols.push(
62
+ ` <symbol id="${id}" viewBox="${parsed.viewBox}">\n ${parsed.inner}\n </symbol>`
63
+ );
64
+ }
65
+
66
+ // Generate spritesheet
67
+ const spritesheet = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none;">
68
+ ${symbols.join("\n")}
69
+ </svg>
70
+ `;
71
+
72
+ await Bun.write(outputPath, spritesheet);
73
+
74
+ return { count: symbols.length, outputPath };
75
+ }