Skip to content

Commit d704fd2

Browse files
vojtaholikclaude
andauthored
fix: high-priority bugs and ship dev overlay from CLI package (#4)
* fix: high-priority bugs and ship dev overlay from CLI package - Fix propPath bug in template compiler: `:index` without parent propPath now derives a base name from the `:props` expression instead of generating bare "[0]" paths - Fix interpolation regex to support multiline expressions ([\s\S] vs .) and detect unmatched braces at compile time - Fix unsafe string replacement in html-renderer: use split/join instead of replace() to avoid $ replacement patterns in region content - Prevent double cache-busting: skip ?v= append if URL already has one - Move dev-overlay.js into @vojtaholik/static-kit-cli package so it ships from the package rather than being copied into every project. Removes drift between example, template, and actual served version. - Remove devOnlyFiles filter from build (no longer needed since dev-overlay.js no longer lives in publicDir) https://claude.ai/code/session_01XkNESeZ6fwXRAYsnw1BhqR * feat: add cssOutput config for customizable CSS minification - Introduced `cssOutput` option ("formatted" | "minified", default "formatted") to control CSS output format during the build process. - Updated build command to conditionally minify CSS based on the new configuration, enhancing flexibility for users. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 97bbe70 commit d704fd2

7 files changed

Lines changed: 27 additions & 377 deletions

File tree

packages/cli/src/commands/build.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,8 @@ async function build() {
9999
console.log("\n📦 Copying public assets...");
100100
const outPublicDir = join(outDir, publicPathDir);
101101

102-
const devOnlyFiles = new Set(["js/dev-overlay.js"]);
103102
const publicGlob = new Glob("**/*");
104103
for await (const file of publicGlob.scan(publicDir)) {
105-
if (devOnlyFiles.has(file)) continue;
106104
const srcFile = join(publicDir, file);
107105
const destFile = join(outPublicDir, file);
108106

@@ -113,13 +111,14 @@ async function build() {
113111
// Process CSS files through lightningcss
114112
if (file.endsWith(".css")) {
115113
const cssBytes = new Uint8Array(await bunFile.arrayBuffer());
114+
const shouldMinify = config.cssOutput === "minified";
116115
const result = processCSS({
117116
filename: srcFile,
118117
code: cssBytes,
119-
minify: true,
118+
minify: shouldMinify,
120119
});
121120
await Bun.write(destFile, result.code);
122-
console.log(` ✓ ${config.publicPath}/${file} (minified)`);
121+
console.log(` ✓ ${config.publicPath}/${file}${shouldMinify ? " (minified)" : ""}`);
123122
} else {
124123
await Bun.write(destFile, await bunFile.arrayBuffer());
125124
console.log(` ✓ ${config.publicPath}/${file}`);

packages/cli/src/commands/dev.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,10 @@ Bun.serve({
210210
const path = url.pathname;
211211

212212
try {
213-
// Dev overlay (special internal asset)
213+
// Dev overlay — served from the CLI package (not the user's publicDir)
214214
if (path === "/__dev-overlay.js") {
215-
const js = await Bun.file(join(publicDir, "js/dev-overlay.js")).text();
215+
const overlayPath = join(import.meta.dir, "../client/dev-overlay.js");
216+
const js = await Bun.file(overlayPath).text();
216217
return new Response(js, {
217218
headers: { "Content-Type": "application/javascript" },
218219
});

packages/core/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const configSchema = z.object({
1818
devPort: z.number().default(3000),
1919
/** HTML output format: "formatted" (pretty-printed) or "minified" */
2020
htmlOutput: z.enum(["formatted", "minified"]).default("formatted"),
21+
/** CSS output format: "formatted" (as-is) or "minified" */
22+
cssOutput: z.enum(["formatted", "minified"]).default("formatted"),
2123
/** URL path prefix for production builds — all internal links get this prefix */
2224
basePath: z
2325
.string()

packages/core/src/html-renderer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ export async function renderPage(
134134
if (node.nodeName === "link") {
135135
const rel = getAttr(node, "rel");
136136
const href = getAttr(node, "href");
137-
if (rel === "stylesheet" && href && href.endsWith(".css")) {
137+
if (rel === "stylesheet" && href && href.endsWith(".css") && !href.includes("?v=")) {
138138
setAttr(node, "href", `${href}?v=${options.cacheBust}`);
139139
}
140140
}
141141
if (node.nodeName === "script") {
142142
const src = getAttr(node, "src");
143-
if (src && src.endsWith(".js")) {
143+
if (src && src.endsWith(".js") && !src.includes("?v=")) {
144144
setAttr(node, "src", `${src}?v=${options.cacheBust}`);
145145
}
146146
}
@@ -176,9 +176,11 @@ export async function renderPage(
176176
);
177177

178178
// Replace markers with actual region content
179+
// Uses split/join instead of replace() to avoid issues with $ in content
180+
// being interpreted as replacement patterns
179181
for (const [regionName, content] of Object.entries(regionContent)) {
180182
const marker = `<!--__REGION_CONTENT_${regionName}__-->`;
181-
html = html.replace(marker, content);
183+
html = html.split(marker).join(content);
182184
}
183185

184186
// Czech typography: non-breaking spaces after single-char prepositions

packages/core/src/template-compiler.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,10 @@ function compileRenderSlot(element: Element, indent: number): string {
434434
} else if (indexExpr) {
435435
// Build prop path from index (assumes we're iterating over an array prop)
436436
// This creates paths like "posts[0]", "posts[1]" etc.
437-
addrExpr = `{ ...addr, propPath: (addr.propPath ? addr.propPath + "[" + ${indexExpr} + "]" : "[" + ${indexExpr} + "]") }`;
437+
// When addr.propPath is set, appends index: "posts[0]"
438+
// When addr.propPath is missing, uses the :props expression base as prefix
439+
const propsBase = propsExpr.replace(/^props\./, "").split(/[.[]/)[0];
440+
addrExpr = `{ ...addr, propPath: (addr.propPath ? addr.propPath + "[" + ${indexExpr} + "]" : "${propsBase}[" + ${indexExpr} + "]") }`;
438441
} else {
439442
// No prop path
440443
addrExpr = "addr";
@@ -477,7 +480,16 @@ function compileTextWithInterpolation(text: string, indent: number): string {
477480

478481
// Split on {{{ ... }}} (raw) and {{ ... }} (escaped)
479482
// Order matters: match triple braces first
480-
const parts = text.split(/(\{\{\{.+?\}\}\}|\{\{.+?\}\})/g);
483+
// Uses [\s\S] instead of . to support multiline expressions
484+
const parts = text.split(/(\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\})/g);
485+
486+
// Detect unmatched braces in static text parts (every other part is static)
487+
for (let j = 0; j < parts.length; j += 2) {
488+
const staticPart = parts[j];
489+
if (staticPart && /\{\{/.test(staticPart)) {
490+
throw new Error(`Unmatched interpolation braces in template text: "${staticPart.trim().slice(0, 60)}"`);
491+
}
492+
}
481493

482494
for (let i = 0; i < parts.length; i++) {
483495
const part = parts[i];

0 commit comments

Comments
 (0)