@zenithbuild/core 0.4.4 → 0.4.6

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.
@@ -13,6 +13,7 @@ import { loadContent } from '../utils/content'
13
13
  import { loadZenithConfig } from '../../core/config/loader'
14
14
  import { PluginRegistry, createPluginContext } from '../../core/plugins/registry'
15
15
  import type { ContentItem } from '../../core/config/types'
16
+ import { compileCssAsync, resolveGlobalsCss } from '../../compiler/css'
16
17
 
17
18
  export interface DevOptions {
18
19
  port?: number
@@ -67,6 +68,23 @@ export async function dev(options: DevOptions = {}): Promise<void> {
67
68
 
68
69
  console.log('[Zenith] Content collections loaded:', Object.keys(contentData))
69
70
 
71
+ // ============================================
72
+ // CSS Compilation (Compiler-Owned)
73
+ // ============================================
74
+ const globalsCssPath = resolveGlobalsCss(rootDir)
75
+ let compiledCss = ''
76
+
77
+ if (globalsCssPath) {
78
+ console.log('[Zenith] Compiling CSS:', path.relative(rootDir, globalsCssPath))
79
+ const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
80
+ if (cssResult.success) {
81
+ compiledCss = cssResult.css
82
+ console.log(`[Zenith] CSS compiled in ${cssResult.duration}ms`)
83
+ } else {
84
+ console.error('[Zenith] CSS compilation failed:', cssResult.error)
85
+ }
86
+ }
87
+
70
88
  const clients = new Set<ServerWebSocket<unknown>>()
71
89
 
72
90
  // Branded Startup Panel
@@ -120,22 +138,38 @@ export async function dev(options: DevOptions = {}): Promise<void> {
120
138
  }
121
139
 
122
140
  // Set up file watching for HMR
123
- const watcher = fs.watch(path.join(pagesDir, '..'), { recursive: true }, (event, filename) => {
141
+ const watcher = fs.watch(path.join(pagesDir, '..'), { recursive: true }, async (event, filename) => {
124
142
  if (!filename) return
125
143
 
126
144
  if (filename.endsWith('.zen')) {
127
145
  logger.hmr('Page', filename)
128
- // Broadcast reload
146
+
147
+ // Clear page cache to force fresh compilation on next request
148
+ pageCache.clear()
149
+
150
+ // Recompile CSS for new Tailwind classes in .zen files
151
+ if (globalsCssPath) {
152
+ const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
153
+ if (cssResult.success) {
154
+ compiledCss = cssResult.css
155
+ }
156
+ }
157
+
158
+ // Broadcast page reload AFTER cache cleared and CSS ready
129
159
  for (const client of clients) {
130
160
  client.send(JSON.stringify({ type: 'reload' }))
131
161
  }
132
162
  } else if (filename.endsWith('.css')) {
133
163
  logger.hmr('CSS', filename)
164
+ // Recompile CSS
165
+ if (globalsCssPath) {
166
+ const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
167
+ if (cssResult.success) {
168
+ compiledCss = cssResult.css
169
+ }
170
+ }
134
171
  for (const client of clients) {
135
- client.send(JSON.stringify({
136
- type: 'style-update',
137
- url: filename.includes('global.css') ? '/styles/global.css' : `/${filename}`
138
- }))
172
+ client.send(JSON.stringify({ type: 'style-update', url: '/assets/styles.css' }))
139
173
  }
140
174
  } else if (filename.startsWith('content') || filename.includes('zenith-docs')) {
141
175
  logger.hmr('Content', filename)
@@ -176,14 +210,22 @@ export async function dev(options: DevOptions = {}): Promise<void> {
176
210
  return response
177
211
  }
178
212
 
179
- if (pathname === '/styles/global.css') {
180
- const globalCssPath = path.join(pagesDir, '../styles/global.css')
181
- if (fs.existsSync(globalCssPath)) {
182
- const css = fs.readFileSync(globalCssPath, 'utf-8')
183
- const response = new Response(css, { headers: { 'Content-Type': 'text/css' } })
184
- logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
185
- return response
186
- }
213
+ // Serve compiler-owned CSS (Tailwind compiled)
214
+ if (pathname === '/assets/styles.css') {
215
+ const response = new Response(compiledCss, {
216
+ headers: { 'Content-Type': 'text/css; charset=utf-8' }
217
+ })
218
+ logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
219
+ return response
220
+ }
221
+
222
+ // Legacy: also support /styles/globals.css or /styles/global.css for backwards compat
223
+ if (pathname === '/styles/globals.css' || pathname === '/styles/global.css') {
224
+ const response = new Response(compiledCss, {
225
+ headers: { 'Content-Type': 'text/css; charset=utf-8' }
226
+ })
227
+ logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
228
+ return response
187
229
  }
188
230
 
189
231
  // Static files
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Zenith CSS Compiler Module
3
+ *
4
+ * Compiler-owned CSS processing that integrates Tailwind CSS v4 JIT
5
+ * at compile time. This module ensures:
6
+ *
7
+ * 1. All CSS is processed at build time (no runtime generation)
8
+ * 2. Tailwind sees all .zen templates for class scanning
9
+ * 3. HMR support for instant CSS updates in dev mode
10
+ * 4. Deterministic, cacheable output for production
11
+ *
12
+ * Per Zenith CSS Directive: The compiler owns styles.
13
+ */
14
+
15
+ import { spawn, spawnSync } from 'child_process'
16
+ import path from 'path'
17
+ import fs from 'fs'
18
+
19
+ // ============================================
20
+ // Types
21
+ // ============================================
22
+
23
+ export interface CSSCompileOptions {
24
+ /** Input CSS file path (e.g., src/styles/globals.css) */
25
+ input: string
26
+ /** Output CSS file path, or ':memory:' for in-memory result */
27
+ output: string
28
+ /** Enable minification for production */
29
+ minify?: boolean
30
+ /** Watch mode for HMR */
31
+ watch?: boolean
32
+ }
33
+
34
+ export interface CSSCompileResult {
35
+ /** Compiled CSS content */
36
+ css: string
37
+ /** Compilation time in milliseconds */
38
+ duration: number
39
+ /** Whether compilation succeeded */
40
+ success: boolean
41
+ /** Error message if failed */
42
+ error?: string
43
+ }
44
+
45
+ // ============================================
46
+ // CSS Compilation
47
+ // ============================================
48
+
49
+ /**
50
+ * Compile CSS using Tailwind CSS v4 CLI
51
+ *
52
+ * This function synchronously compiles CSS for use in:
53
+ * - Dev server startup
54
+ * - SSG build
55
+ * - On-demand recompilation
56
+ *
57
+ * @param options Compilation options
58
+ * @returns Compiled CSS result
59
+ */
60
+ export function compileCss(options: CSSCompileOptions): CSSCompileResult {
61
+ const startTime = performance.now()
62
+ const { input, output, minify = false } = options
63
+
64
+ // Validate input exists
65
+ if (!fs.existsSync(input)) {
66
+ return {
67
+ css: '',
68
+ duration: 0,
69
+ success: false,
70
+ error: `CSS input file not found: ${input}`
71
+ }
72
+ }
73
+
74
+ try {
75
+ // Build Tailwind CLI arguments
76
+ const args = [
77
+ '@tailwindcss/cli',
78
+ '-i', input
79
+ ]
80
+
81
+ // For in-memory compilation, use stdout
82
+ const useStdout = output === ':memory:'
83
+ if (!useStdout) {
84
+ args.push('-o', output)
85
+ }
86
+
87
+ if (minify) {
88
+ args.push('--minify')
89
+ }
90
+
91
+ // Execute Tailwind CLI synchronously
92
+ const result = spawnSync('bunx', args, {
93
+ cwd: path.dirname(input),
94
+ encoding: 'utf-8',
95
+ stdio: useStdout ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'pipe'],
96
+ env: { ...process.env }
97
+ })
98
+
99
+ const duration = Math.round(performance.now() - startTime)
100
+
101
+ if (result.status !== 0) {
102
+ const errorMsg = result.stderr?.toString() || 'Unknown compilation error'
103
+ return {
104
+ css: '',
105
+ duration,
106
+ success: false,
107
+ error: `Tailwind compilation failed: ${errorMsg}`
108
+ }
109
+ }
110
+
111
+ // Get CSS content
112
+ let css = ''
113
+ if (useStdout) {
114
+ css = result.stdout?.toString() || ''
115
+ } else if (fs.existsSync(output)) {
116
+ css = fs.readFileSync(output, 'utf-8')
117
+ }
118
+
119
+ return {
120
+ css,
121
+ duration,
122
+ success: true
123
+ }
124
+
125
+ } catch (error: any) {
126
+ return {
127
+ css: '',
128
+ duration: Math.round(performance.now() - startTime),
129
+ success: false,
130
+ error: error.message
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Compile CSS asynchronously (non-blocking)
137
+ *
138
+ * Used for HMR updates where we don't want to block the main thread.
139
+ */
140
+ export async function compileCssAsync(options: CSSCompileOptions): Promise<CSSCompileResult> {
141
+ return new Promise((resolve) => {
142
+ const startTime = performance.now()
143
+ const { input, output, minify = false } = options
144
+
145
+ if (!fs.existsSync(input)) {
146
+ resolve({
147
+ css: '',
148
+ duration: 0,
149
+ success: false,
150
+ error: `CSS input file not found: ${input}`
151
+ })
152
+ return
153
+ }
154
+
155
+ const args = ['@tailwindcss/cli', '-i', input]
156
+ const useStdout = output === ':memory:'
157
+
158
+ if (!useStdout) {
159
+ args.push('-o', output)
160
+ }
161
+
162
+ if (minify) {
163
+ args.push('--minify')
164
+ }
165
+
166
+ const child = spawn('bunx', args, {
167
+ cwd: path.dirname(input),
168
+ stdio: useStdout ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'pipe'],
169
+ env: { ...process.env }
170
+ })
171
+
172
+ let stdout = ''
173
+ let stderr = ''
174
+
175
+ if (useStdout && child.stdout) {
176
+ child.stdout.on('data', (data) => { stdout += data.toString() })
177
+ }
178
+
179
+ if (child.stderr) {
180
+ child.stderr.on('data', (data) => { stderr += data.toString() })
181
+ }
182
+
183
+ child.on('close', (code) => {
184
+ const duration = Math.round(performance.now() - startTime)
185
+
186
+ if (code !== 0) {
187
+ resolve({
188
+ css: '',
189
+ duration,
190
+ success: false,
191
+ error: `Tailwind compilation failed: ${stderr}`
192
+ })
193
+ return
194
+ }
195
+
196
+ let css = ''
197
+ if (useStdout) {
198
+ css = stdout
199
+ } else if (fs.existsSync(output)) {
200
+ css = fs.readFileSync(output, 'utf-8')
201
+ }
202
+
203
+ resolve({
204
+ css,
205
+ duration,
206
+ success: true
207
+ })
208
+ })
209
+
210
+ child.on('error', (err) => {
211
+ resolve({
212
+ css: '',
213
+ duration: Math.round(performance.now() - startTime),
214
+ success: false,
215
+ error: err.message
216
+ })
217
+ })
218
+ })
219
+ }
220
+
221
+ // ============================================
222
+ // CSS Watcher for HMR
223
+ // ============================================
224
+
225
+ export interface CSSWatchOptions extends CSSCompileOptions {
226
+ /** Callback when CSS changes */
227
+ onChange: (result: CSSCompileResult) => void
228
+ /** Debounce delay in ms */
229
+ debounce?: number
230
+ }
231
+
232
+ /**
233
+ * Watch CSS and source files for changes, recompile on change
234
+ *
235
+ * This is used by the dev server for HMR support.
236
+ * It watches both the CSS entry point AND all .zen files
237
+ * that Tailwind scans for class names.
238
+ */
239
+ export function watchCss(options: CSSWatchOptions): () => void {
240
+ const { input, output, minify, onChange, debounce = 100 } = options
241
+
242
+ let timeout: NodeJS.Timeout | null = null
243
+ let isCompiling = false
244
+
245
+ const recompile = async () => {
246
+ if (isCompiling) return
247
+ isCompiling = true
248
+
249
+ const result = await compileCssAsync({ input, output, minify })
250
+ onChange(result)
251
+
252
+ isCompiling = false
253
+ }
254
+
255
+ const debouncedRecompile = () => {
256
+ if (timeout) clearTimeout(timeout)
257
+ timeout = setTimeout(recompile, debounce)
258
+ }
259
+
260
+ // Watch the styles directory
261
+ const stylesDir = path.dirname(input)
262
+ const stylesWatcher = fs.watch(stylesDir, { recursive: true }, (event, filename) => {
263
+ if (filename?.endsWith('.css')) {
264
+ debouncedRecompile()
265
+ }
266
+ })
267
+
268
+ // Watch source files that Tailwind scans (for class changes)
269
+ // This assumes standard Zenith structure: src/pages, src/components, src/layouts
270
+ const srcDir = path.resolve(stylesDir, '..')
271
+ let srcWatcher: fs.FSWatcher | null = null
272
+
273
+ if (fs.existsSync(srcDir)) {
274
+ srcWatcher = fs.watch(srcDir, { recursive: true }, (event, filename) => {
275
+ if (filename?.endsWith('.zen') || filename?.endsWith('.tsx') || filename?.endsWith('.jsx')) {
276
+ debouncedRecompile()
277
+ }
278
+ })
279
+ }
280
+
281
+ // Return cleanup function
282
+ return () => {
283
+ if (timeout) clearTimeout(timeout)
284
+ stylesWatcher.close()
285
+ srcWatcher?.close()
286
+ }
287
+ }
288
+
289
+ // ============================================
290
+ // Path Utilities
291
+ // ============================================
292
+
293
+ /**
294
+ * Resolve the canonical globals.css path for a Zenith project
295
+ */
296
+ export function resolveGlobalsCss(projectRoot: string): string | null {
297
+ // Check for globals.css (canonical)
298
+ const globalsPath = path.join(projectRoot, 'src', 'styles', 'globals.css')
299
+ if (fs.existsSync(globalsPath)) return globalsPath
300
+
301
+ // Check for global.css (legacy)
302
+ const globalPath = path.join(projectRoot, 'src', 'styles', 'global.css')
303
+ if (fs.existsSync(globalPath)) return globalPath
304
+
305
+ return null
306
+ }
307
+
308
+ /**
309
+ * Get the output path for compiled CSS
310
+ */
311
+ export function getCompiledCssPath(projectRoot: string, mode: 'dev' | 'build'): string {
312
+ if (mode === 'build') {
313
+ return path.join(projectRoot, 'dist', 'assets', 'styles.css')
314
+ }
315
+ // In dev mode, we use in-memory compilation
316
+ return ':memory:'
317
+ }
@@ -20,6 +20,7 @@ import { discoverPages, generateRouteDefinition } from "../router/manifest"
20
20
  import { analyzePageSource, getAnalysisSummary, getBuildOutputType, type PageAnalysis } from "./build-analyzer"
21
21
  import { generateBundleJS } from "../runtime/bundle-generator"
22
22
  import { loadContent } from "../cli/utils/content"
23
+ import { compileCss, resolveGlobalsCss } from "./css"
23
24
 
24
25
  // ============================================
25
26
  // Types
@@ -292,12 +293,23 @@ export function buildSSG(options: SSGBuildOptions): void {
292
293
 
293
294
  console.log('')
294
295
 
295
- // Load global styles
296
+ // Compile global styles (Tailwind CSS)
296
297
  let globalStyles = ''
297
- const globalCssPath = path.join(baseDir, 'styles', 'global.css')
298
- if (fs.existsSync(globalCssPath)) {
299
- globalStyles = fs.readFileSync(globalCssPath, 'utf-8')
300
- console.log('📦 Loaded global.css')
298
+ const globalsCssPath = resolveGlobalsCss(baseDir)
299
+ if (globalsCssPath) {
300
+ console.log('📦 Compiling CSS:', path.relative(baseDir, globalsCssPath))
301
+ const cssOutputPath = path.join(outDir, 'assets', 'styles.css')
302
+ const result = compileCss({
303
+ input: globalsCssPath,
304
+ output: cssOutputPath,
305
+ minify: true
306
+ })
307
+ if (result.success) {
308
+ globalStyles = result.css
309
+ console.log(`📦 Generated assets/styles.css (${result.duration}ms)`)
310
+ } else {
311
+ console.error('❌ CSS compilation failed:', result.error)
312
+ }
301
313
  }
302
314
 
303
315
  // Write bundle.js if any pages need hydration
@@ -307,12 +319,6 @@ export function buildSSG(options: SSGBuildOptions): void {
307
319
  console.log('📦 Generated assets/bundle.js')
308
320
  }
309
321
 
310
- // Write global styles
311
- if (globalStyles) {
312
- fs.writeFileSync(path.join(outDir, 'assets', 'styles.css'), globalStyles)
313
- console.log('📦 Generated assets/styles.css')
314
- }
315
-
316
322
  // Write each page
317
323
  for (const page of compiledPages) {
318
324
  // Create output directory
package/dist/cli.js CHANGED
@@ -5,6 +5,8 @@
5
5
  #!/usr/bin/env bun
6
6
  #!/usr/bin/env bun
7
7
  #!/usr/bin/env bun
8
+ #!/usr/bin/env bun
9
+ #!/usr/bin/env bun
8
10
  // @bun
9
11
  var __create = Object.create;
10
12
  var __getProtoOf = Object.getPrototypeOf;