@zenithbuild/bundler 1.3.7 → 1.3.15
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 +10 -10
- package/src/build-analyzer.ts +122 -0
- package/src/bundle-generator.ts +844 -0
- package/src/bundler.rs +81 -0
- package/src/bundler.ts +98 -0
- package/src/css.rs +266 -0
- package/src/css.ts +294 -0
- package/src/generateFinalBundle.ts +80 -0
- package/src/html.rs +147 -0
- package/src/index.ts +7 -0
- package/src/lib.rs +116 -0
- package/src/plugin.rs +354 -0
- package/src/runtime-generator.ts +24 -0
- package/src/store.rs +44 -0
- package/src/types.ts +6 -0
package/src/css.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
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
|
+
export function compileCss(options: CSSCompileOptions): CSSCompileResult {
|
|
53
|
+
const startTime = performance.now()
|
|
54
|
+
const { input, output, minify = false } = options
|
|
55
|
+
|
|
56
|
+
// Validate input exists
|
|
57
|
+
if (!fs.existsSync(input)) {
|
|
58
|
+
return {
|
|
59
|
+
css: '',
|
|
60
|
+
duration: 0,
|
|
61
|
+
success: false,
|
|
62
|
+
error: `CSS input file not found: ${input}`
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Build Tailwind CLI arguments
|
|
68
|
+
const args = [
|
|
69
|
+
'@tailwindcss/cli',
|
|
70
|
+
'-i', input
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
// For in-memory compilation, use stdout
|
|
74
|
+
const useStdout = output === ':memory:'
|
|
75
|
+
if (!useStdout) {
|
|
76
|
+
args.push('-o', output)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (minify) {
|
|
80
|
+
args.push('--minify')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Execute Tailwind CLI synchronously
|
|
84
|
+
const result = spawnSync('bunx', args, {
|
|
85
|
+
cwd: path.dirname(input),
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
stdio: useStdout ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'pipe'],
|
|
88
|
+
env: { ...process.env }
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const duration = Math.round(performance.now() - startTime)
|
|
92
|
+
|
|
93
|
+
if (result.status !== 0) {
|
|
94
|
+
const errorMsg = result.stderr?.toString() || 'Unknown compilation error'
|
|
95
|
+
return {
|
|
96
|
+
css: '',
|
|
97
|
+
duration,
|
|
98
|
+
success: false,
|
|
99
|
+
error: `Tailwind compilation failed: ${errorMsg}`
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get CSS content
|
|
104
|
+
let css = ''
|
|
105
|
+
if (useStdout) {
|
|
106
|
+
css = result.stdout?.toString() || ''
|
|
107
|
+
} else if (fs.existsSync(output)) {
|
|
108
|
+
css = fs.readFileSync(output, 'utf-8')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
css,
|
|
113
|
+
duration,
|
|
114
|
+
success: true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
return {
|
|
119
|
+
css: '',
|
|
120
|
+
duration: Math.round(performance.now() - startTime),
|
|
121
|
+
success: false,
|
|
122
|
+
error: error.message
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compile CSS asynchronously (non-blocking)
|
|
129
|
+
*/
|
|
130
|
+
export async function compileCssAsync(options: CSSCompileOptions): Promise<CSSCompileResult> {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const startTime = performance.now()
|
|
133
|
+
const { input, output, minify = false } = options
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(input)) {
|
|
136
|
+
resolve({
|
|
137
|
+
css: '',
|
|
138
|
+
duration: 0,
|
|
139
|
+
success: false,
|
|
140
|
+
error: `CSS input file not found: ${input}`
|
|
141
|
+
})
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const args = ['@tailwindcss/cli', '-i', input]
|
|
146
|
+
const useStdout = output === ':memory:'
|
|
147
|
+
|
|
148
|
+
if (!useStdout) {
|
|
149
|
+
args.push('-o', output)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (minify) {
|
|
153
|
+
args.push('--minify')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const child = spawn('bunx', args, {
|
|
157
|
+
cwd: path.dirname(input),
|
|
158
|
+
stdio: useStdout ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'pipe'],
|
|
159
|
+
env: { ...process.env }
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
let stdout = ''
|
|
163
|
+
let stderr = ''
|
|
164
|
+
|
|
165
|
+
if (useStdout && child.stdout) {
|
|
166
|
+
child.stdout.on('data', (data) => { stdout += data.toString() })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (child.stderr) {
|
|
170
|
+
child.stderr.on('data', (data) => { stderr += data.toString() })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
child.on('close', (code) => {
|
|
174
|
+
const duration = Math.round(performance.now() - startTime)
|
|
175
|
+
|
|
176
|
+
if (code !== 0) {
|
|
177
|
+
resolve({
|
|
178
|
+
css: '',
|
|
179
|
+
duration,
|
|
180
|
+
success: false,
|
|
181
|
+
error: `Tailwind compilation failed: ${stderr}`
|
|
182
|
+
})
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let css = ''
|
|
187
|
+
if (useStdout) {
|
|
188
|
+
css = stdout
|
|
189
|
+
} else if (fs.existsSync(output)) {
|
|
190
|
+
css = fs.readFileSync(output, 'utf-8')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
resolve({
|
|
194
|
+
css,
|
|
195
|
+
duration,
|
|
196
|
+
success: true
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
child.on('error', (err) => {
|
|
201
|
+
resolve({
|
|
202
|
+
css: '',
|
|
203
|
+
duration: Math.round(performance.now() - startTime),
|
|
204
|
+
success: false,
|
|
205
|
+
error: err.message
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface CSSWatchOptions extends CSSCompileOptions {
|
|
212
|
+
/** Callback when CSS changes */
|
|
213
|
+
onChange: (result: CSSCompileResult) => void
|
|
214
|
+
/** Debounce delay in ms */
|
|
215
|
+
debounce?: number
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Watch CSS and source files for changes, recompile on change
|
|
220
|
+
*/
|
|
221
|
+
export function watchCss(options: CSSWatchOptions): () => void {
|
|
222
|
+
const { input, output, minify, onChange, debounce = 100 } = options
|
|
223
|
+
|
|
224
|
+
let timeout: NodeJS.Timeout | null = null
|
|
225
|
+
let isCompiling = false
|
|
226
|
+
|
|
227
|
+
const recompile = async () => {
|
|
228
|
+
if (isCompiling) return
|
|
229
|
+
isCompiling = true
|
|
230
|
+
|
|
231
|
+
const result = await compileCssAsync({ input, output, minify })
|
|
232
|
+
onChange(result)
|
|
233
|
+
|
|
234
|
+
isCompiling = false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const debouncedRecompile = () => {
|
|
238
|
+
if (timeout) clearTimeout(timeout)
|
|
239
|
+
timeout = setTimeout(recompile, debounce)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Watch the styles directory
|
|
243
|
+
const stylesDir = path.dirname(input)
|
|
244
|
+
const stylesWatcher = fs.watch(stylesDir, { recursive: true }, (event, filename) => {
|
|
245
|
+
if (filename?.endsWith('.css')) {
|
|
246
|
+
debouncedRecompile()
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Watch source files that Tailwind scans (for class changes)
|
|
251
|
+
const srcDir = path.resolve(stylesDir, '..')
|
|
252
|
+
let srcWatcher: fs.FSWatcher | null = null
|
|
253
|
+
|
|
254
|
+
if (fs.existsSync(srcDir)) {
|
|
255
|
+
srcWatcher = fs.watch(srcDir, { recursive: true }, (event, filename) => {
|
|
256
|
+
if (filename?.endsWith('.zen') || filename?.endsWith('.tsx') || filename?.endsWith('.jsx')) {
|
|
257
|
+
debouncedRecompile()
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Return cleanup function
|
|
263
|
+
return () => {
|
|
264
|
+
if (timeout) clearTimeout(timeout)
|
|
265
|
+
stylesWatcher.close()
|
|
266
|
+
srcWatcher?.close()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Resolve the canonical globals.css path for a Zenith project
|
|
272
|
+
*/
|
|
273
|
+
export function resolveGlobalsCss(projectRoot: string): string | null {
|
|
274
|
+
// Check for globals.css (canonical)
|
|
275
|
+
const globalsPath = path.join(projectRoot, 'src', 'styles', 'globals.css')
|
|
276
|
+
if (fs.existsSync(globalsPath)) return globalsPath
|
|
277
|
+
|
|
278
|
+
// Check for global.css (legacy)
|
|
279
|
+
const globalPath = path.join(projectRoot, 'src', 'styles', 'global.css')
|
|
280
|
+
if (fs.existsSync(globalPath)) return globalPath
|
|
281
|
+
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get the output path for compiled CSS
|
|
287
|
+
*/
|
|
288
|
+
export function getCompiledCssPath(projectRoot: string, mode: 'dev' | 'build'): string {
|
|
289
|
+
if (mode === 'build') {
|
|
290
|
+
return path.join(projectRoot, 'dist', 'assets', 'styles.css')
|
|
291
|
+
}
|
|
292
|
+
// In dev mode, we use in-memory compilation
|
|
293
|
+
return ':memory:'
|
|
294
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Final Bundle
|
|
3
|
+
*
|
|
4
|
+
* Phase 8/9/10: Generate final browser-ready bundle
|
|
5
|
+
*
|
|
6
|
+
* Combines:
|
|
7
|
+
* - Compiled HTML
|
|
8
|
+
* - Runtime JS
|
|
9
|
+
* - Expression functions
|
|
10
|
+
* - Event bindings
|
|
11
|
+
* - Style injection
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { FinalizedOutput } from '@zenithbuild/compiler'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate final bundle code
|
|
18
|
+
*
|
|
19
|
+
* This is the complete JavaScript bundle that will execute in the browser.
|
|
20
|
+
* All expressions are pre-compiled - no template parsing at runtime.
|
|
21
|
+
*/
|
|
22
|
+
export function generateFinalBundle(finalized: FinalizedOutput): string {
|
|
23
|
+
return `// Zenith Compiled Bundle (Phase 8/9/10)
|
|
24
|
+
// Generated at compile time - no .zen parsing in browser
|
|
25
|
+
// All expressions are pre-compiled - deterministic output
|
|
26
|
+
|
|
27
|
+
${finalized.js}
|
|
28
|
+
|
|
29
|
+
// Bundle complete - ready for browser execution
|
|
30
|
+
`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate HTML with inline script
|
|
35
|
+
*/
|
|
36
|
+
export function generateHTMLWithScript(
|
|
37
|
+
html: string,
|
|
38
|
+
jsBundle: string,
|
|
39
|
+
styles: string[]
|
|
40
|
+
): string {
|
|
41
|
+
// Inject styles as <style> tags
|
|
42
|
+
const styleTags = styles.map(style => `<style>${escapeHTML(style)}</style>`).join('\n')
|
|
43
|
+
|
|
44
|
+
// Inject JS bundle as inline script
|
|
45
|
+
const scriptTag = `<script>${jsBundle}</script>`
|
|
46
|
+
|
|
47
|
+
// Find </head> or <body> to inject styles
|
|
48
|
+
// Find </body> to inject script
|
|
49
|
+
let result = html
|
|
50
|
+
|
|
51
|
+
if (styleTags) {
|
|
52
|
+
if (result.includes('</head>')) {
|
|
53
|
+
result = result.replace('</head>', `${styleTags}\n</head>`)
|
|
54
|
+
} else if (result.includes('<body')) {
|
|
55
|
+
result = result.replace('<body', `${styleTags}\n<body`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (scriptTag) {
|
|
60
|
+
if (result.includes('</body>')) {
|
|
61
|
+
result = result.replace('</body>', `${scriptTag}\n</body>`)
|
|
62
|
+
} else {
|
|
63
|
+
result += scriptTag
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Escape HTML for safe embedding
|
|
72
|
+
*/
|
|
73
|
+
function escapeHTML(str: string): string {
|
|
74
|
+
return str
|
|
75
|
+
.replace(/&/g, '&')
|
|
76
|
+
.replace(/</g, '<')
|
|
77
|
+
.replace(/>/g, '>')
|
|
78
|
+
.replace(/"/g, '"')
|
|
79
|
+
.replace(/'/g, ''')
|
|
80
|
+
}
|
package/src/html.rs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//! HTML Injector Module
|
|
2
|
+
//!
|
|
3
|
+
//! Injects hashed asset links into index.html:
|
|
4
|
+
//! - `<script type="module" src="...">` for the Entry Chunk
|
|
5
|
+
//! - `<link rel="stylesheet">` for the CSS Asset
|
|
6
|
+
//! - `<link rel="modulepreload">` for critical chunks
|
|
7
|
+
|
|
8
|
+
/// Asset information for HTML injection
|
|
9
|
+
#[derive(Debug, Clone)]
|
|
10
|
+
pub struct AssetInfo {
|
|
11
|
+
pub filename: String,
|
|
12
|
+
pub is_entry: bool,
|
|
13
|
+
pub is_css: bool,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// The HTML Injector that updates index.html with hashed asset paths
|
|
17
|
+
pub struct HtmlInjector {
|
|
18
|
+
/// Template HTML content
|
|
19
|
+
template: String,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl HtmlInjector {
|
|
23
|
+
pub fn new(template: String) -> Self {
|
|
24
|
+
Self { template }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Load template from file
|
|
28
|
+
pub fn from_file(path: &str) -> Result<Self, std::io::Error> {
|
|
29
|
+
let template = std::fs::read_to_string(path)?;
|
|
30
|
+
Ok(Self::new(template))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Inject asset references into the HTML template
|
|
34
|
+
pub fn inject(&self, assets: &[AssetInfo], preload_chunks: &[String]) -> String {
|
|
35
|
+
let mut html = self.template.clone();
|
|
36
|
+
|
|
37
|
+
// Build injection strings
|
|
38
|
+
let mut script_tags = String::new();
|
|
39
|
+
let mut css_links = String::new();
|
|
40
|
+
let mut preload_links = String::new();
|
|
41
|
+
|
|
42
|
+
// CSS links
|
|
43
|
+
for asset in assets.iter().filter(|a| a.is_css) {
|
|
44
|
+
css_links.push_str(&format!(
|
|
45
|
+
r#" <link rel="stylesheet" href="/{}">"#,
|
|
46
|
+
asset.filename
|
|
47
|
+
));
|
|
48
|
+
css_links.push('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Modulepreload for critical chunks
|
|
52
|
+
for chunk in preload_chunks {
|
|
53
|
+
preload_links.push_str(&format!(
|
|
54
|
+
r#" <link rel="modulepreload" href="/{}">"#,
|
|
55
|
+
chunk
|
|
56
|
+
));
|
|
57
|
+
preload_links.push('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Entry script (should be last, after preloads)
|
|
61
|
+
for asset in assets.iter().filter(|a| a.is_entry && !a.is_css) {
|
|
62
|
+
script_tags.push_str(&format!(
|
|
63
|
+
r#" <script type="module" src="/{}"></script>"#,
|
|
64
|
+
asset.filename
|
|
65
|
+
));
|
|
66
|
+
script_tags.push('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Inject before </head>
|
|
70
|
+
let head_injection = format!("{}{}", css_links, preload_links);
|
|
71
|
+
if let Some(pos) = html.find("</head>") {
|
|
72
|
+
html.insert_str(pos, &head_injection);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Inject scripts before </body>
|
|
76
|
+
if let Some(pos) = html.find("</body>") {
|
|
77
|
+
html.insert_str(pos, &script_tags);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
html
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Generate a minimal HTML template if none exists
|
|
84
|
+
pub fn generate_default(title: &str) -> String {
|
|
85
|
+
format!(
|
|
86
|
+
r#"<!DOCTYPE html>
|
|
87
|
+
<html lang="en">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="UTF-8">
|
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
91
|
+
<title>{}</title>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<div id="app"></div>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
97
|
+
"#,
|
|
98
|
+
title
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[cfg(test)]
|
|
104
|
+
mod tests {
|
|
105
|
+
use super::*;
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn test_inject_assets() {
|
|
109
|
+
let template = r#"<!DOCTYPE html>
|
|
110
|
+
<html>
|
|
111
|
+
<head>
|
|
112
|
+
<title>Test</title>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<div id="app"></div>
|
|
116
|
+
</body>
|
|
117
|
+
</html>"#;
|
|
118
|
+
|
|
119
|
+
let injector = HtmlInjector::new(template.to_string());
|
|
120
|
+
let assets = vec![
|
|
121
|
+
AssetInfo {
|
|
122
|
+
filename: "app-x82z.js".into(),
|
|
123
|
+
is_entry: true,
|
|
124
|
+
is_css: false,
|
|
125
|
+
},
|
|
126
|
+
AssetInfo {
|
|
127
|
+
filename: "zenith-abc1.css".into(),
|
|
128
|
+
is_entry: false,
|
|
129
|
+
is_css: true,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
let preloads = vec!["runtime-core-def2.js".into()];
|
|
133
|
+
|
|
134
|
+
let result = injector.inject(&assets, &preloads);
|
|
135
|
+
|
|
136
|
+
assert!(result.contains(r#"<link rel="stylesheet" href="/zenith-abc1.css">"#));
|
|
137
|
+
assert!(result.contains(r#"<link rel="modulepreload" href="/runtime-core-def2.js">"#));
|
|
138
|
+
assert!(result.contains(r#"<script type="module" src="/app-x82z.js"></script>"#));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn test_generate_default_template() {
|
|
143
|
+
let html = HtmlInjector::generate_default("My App");
|
|
144
|
+
assert!(html.contains("<title>My App</title>"));
|
|
145
|
+
assert!(html.contains("<div id=\"app\"></div>"));
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/index.ts
ADDED
package/src/lib.rs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//! Zenith Bundler
|
|
2
|
+
//!
|
|
3
|
+
//! Rolldown Plugin for the Zenith Framework.
|
|
4
|
+
//!
|
|
5
|
+
//! This crate acts as the **Intelligence Layer** that feeds the Zenith compiler
|
|
6
|
+
//! output to Rolldown, implementing:
|
|
7
|
+
//!
|
|
8
|
+
//! - **Deferred Hydration**: Bootstrap loader with dynamic imports
|
|
9
|
+
//! - **Capability-Based Chunking**: Separate runtime-core and runtime-anim
|
|
10
|
+
//! - **CSS Pruning**: Tree-shake unused Tailwind via ZenManifest.css_classes
|
|
11
|
+
//! - **HTML Injection**: Inject hashed script/CSS links and modulepreload
|
|
12
|
+
//!
|
|
13
|
+
//! # Architecture
|
|
14
|
+
//!
|
|
15
|
+
//! ```text
|
|
16
|
+
//! .zen files → ZenithPlugin (resolve_id/load) → Rolldown Engine → Optimized Output
|
|
17
|
+
//! ```
|
|
18
|
+
|
|
19
|
+
pub mod bundler;
|
|
20
|
+
pub mod css;
|
|
21
|
+
pub mod html;
|
|
22
|
+
pub mod plugin;
|
|
23
|
+
pub mod store;
|
|
24
|
+
|
|
25
|
+
pub use css::CssBuffer;
|
|
26
|
+
pub use html::HtmlInjector;
|
|
27
|
+
pub use plugin::ZenithPlugin;
|
|
28
|
+
|
|
29
|
+
// Re-export Rolldown types for convenience
|
|
30
|
+
pub use rolldown::{Bundler, BundlerBuilder, BundlerOptions};
|
|
31
|
+
pub use rolldown_plugin::Plugin;
|
|
32
|
+
|
|
33
|
+
// --- NAPI Integration ---
|
|
34
|
+
|
|
35
|
+
#[cfg(feature = "napi")]
|
|
36
|
+
use napi_derive::napi;
|
|
37
|
+
#[cfg(feature = "napi")]
|
|
38
|
+
use std::sync::Arc;
|
|
39
|
+
#[cfg(feature = "napi")]
|
|
40
|
+
use crate::store::AssetStore;
|
|
41
|
+
#[cfg(feature = "napi")]
|
|
42
|
+
use tokio::sync::{mpsc, oneshot};
|
|
43
|
+
|
|
44
|
+
#[cfg(feature = "napi")]
|
|
45
|
+
#[napi]
|
|
46
|
+
pub struct ZenithDevController {
|
|
47
|
+
store: Arc<AssetStore>,
|
|
48
|
+
rebuild_tx: mpsc::Sender<oneshot::Sender<()>>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[cfg(feature = "napi")]
|
|
52
|
+
#[napi]
|
|
53
|
+
impl ZenithDevController {
|
|
54
|
+
#[napi(constructor)]
|
|
55
|
+
pub fn new(project_root: String) -> Self {
|
|
56
|
+
let store = Arc::new(AssetStore::new());
|
|
57
|
+
let store_clone = store.clone();
|
|
58
|
+
|
|
59
|
+
// Channel for rebuild signals (Robust HMR Pattern)
|
|
60
|
+
// Main thread sends (reply_channel) -> Builder builds -> Builder replies
|
|
61
|
+
let (tx, mut rx) = mpsc::channel::<oneshot::Sender<()>>(1);
|
|
62
|
+
|
|
63
|
+
// Spawn Watcher/Builder Thread
|
|
64
|
+
std::thread::spawn(move || {
|
|
65
|
+
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
66
|
+
rt.block_on(async move {
|
|
67
|
+
let mut bundler = crate::bundler::create_dev_bundler(
|
|
68
|
+
&format!("{}/src/main.zen", project_root),
|
|
69
|
+
Some(&format!("{}/src/components", project_root)),
|
|
70
|
+
store_clone
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Initial Build
|
|
74
|
+
match bundler.write().await {
|
|
75
|
+
Ok(_outputs) => {}, // println! removed
|
|
76
|
+
Err(_e) => {}, // eprintln! removed for silence? Or keep errors? User said "all logs". I'll keep errors if critical, but silence is cleaner for "library".
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Internal Watch Loop (Driven by NAPI calls)
|
|
80
|
+
while let Some(reply_tx) = rx.recv().await {
|
|
81
|
+
match bundler.write().await {
|
|
82
|
+
Ok(_) => {
|
|
83
|
+
let _ = reply_tx.send(());
|
|
84
|
+
}
|
|
85
|
+
Err(_e) => {
|
|
86
|
+
let _ = reply_tx.send(());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
Self { store, rebuild_tx: tx }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[napi]
|
|
97
|
+
pub fn get_asset(&self, path: String) -> Option<String> {
|
|
98
|
+
self.store.get(&path)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Trigger a rebuild and wait for completion
|
|
102
|
+
#[napi]
|
|
103
|
+
pub async fn rebuild(&self) -> napi::Result<()> {
|
|
104
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
105
|
+
self.rebuild_tx
|
|
106
|
+
.send(reply_tx)
|
|
107
|
+
.await
|
|
108
|
+
.map_err(|_| napi::Error::from_reason("Builder thread disconnected"))?;
|
|
109
|
+
|
|
110
|
+
reply_rx
|
|
111
|
+
.await
|
|
112
|
+
.map_err(|_| napi::Error::from_reason("Builder failed to reply"))?;
|
|
113
|
+
|
|
114
|
+
Ok(())
|
|
115
|
+
}
|
|
116
|
+
}
|