@zenithbuild/bundler 1.3.2 → 1.3.4
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/index.node +0 -0
- package/package.json +12 -2
- package/CLAUDE.md +0 -111
- package/Cargo.lock +0 -4244
- package/Cargo.toml +0 -65
- package/build.rs +0 -5
- package/src/bundler.rs +0 -81
- package/src/css.rs +0 -266
- package/src/html.rs +0 -147
- package/src/lib.rs +0 -116
- package/src/plugin.rs +0 -332
- package/src/store.rs +0 -44
- package/tests/fixtures/hello.zen +0 -19
- package/tests/integration.rs +0 -53
- package/tsconfig.json +0 -29
package/Cargo.toml
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "zenith-bundler"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
description = "Rolldown Plugin for the Zenith Framework - Zero-Cost Abstraction Bundler"
|
|
6
|
-
license = "MIT"
|
|
7
|
-
|
|
8
|
-
[lib]
|
|
9
|
-
crate-type = ["cdylib", "rlib"]
|
|
10
|
-
|
|
11
|
-
[features]
|
|
12
|
-
napi = []
|
|
13
|
-
|
|
14
|
-
[dependencies]
|
|
15
|
-
# CSS Optimization & Pruning
|
|
16
|
-
lightningcss = "1.0.0-alpha.70"
|
|
17
|
-
|
|
18
|
-
# Serialization
|
|
19
|
-
serde = { version = "1.0", features = ["derive"] }
|
|
20
|
-
serde_json = "1.0"
|
|
21
|
-
|
|
22
|
-
# Zenith Compiler (local crate)
|
|
23
|
-
compiler-native = { path = "../zenith-compiler/native/compiler-native", default-features = false }
|
|
24
|
-
|
|
25
|
-
# Node.js FFI (for TypeScript interface)
|
|
26
|
-
napi = { version = "2.16.0", features = ["async", "serde-json"] }
|
|
27
|
-
napi-derive = "2.16.0"
|
|
28
|
-
|
|
29
|
-
# Async runtime
|
|
30
|
-
tokio = { version = "1.0", features = ["fs", "macros", "rt-multi-thread"] }
|
|
31
|
-
|
|
32
|
-
# Concurrent collections for CSS buffering
|
|
33
|
-
dashmap = "5.5"
|
|
34
|
-
|
|
35
|
-
# ArcStr for Rolldown compatibility
|
|
36
|
-
arcstr = "1.2"
|
|
37
|
-
|
|
38
|
-
# Error handling
|
|
39
|
-
anyhow = "1.0"
|
|
40
|
-
|
|
41
|
-
# Regex for CSS class extraction
|
|
42
|
-
regex = "1.10"
|
|
43
|
-
|
|
44
|
-
# Rolldown from git (latest compatible version)
|
|
45
|
-
# Note: Using git source because crates.io version 0.1.0 has broken oxc_resolver compat
|
|
46
|
-
[dependencies.rolldown]
|
|
47
|
-
git = "https://github.com/rolldown/rolldown"
|
|
48
|
-
branch = "main"
|
|
49
|
-
default-features = false
|
|
50
|
-
|
|
51
|
-
[dependencies.rolldown_plugin]
|
|
52
|
-
git = "https://github.com/rolldown/rolldown"
|
|
53
|
-
branch = "main"
|
|
54
|
-
default-features = false
|
|
55
|
-
|
|
56
|
-
[dependencies.rolldown_common]
|
|
57
|
-
git = "https://github.com/rolldown/rolldown"
|
|
58
|
-
branch = "main"
|
|
59
|
-
default-features = false
|
|
60
|
-
|
|
61
|
-
[build-dependencies]
|
|
62
|
-
napi-build = "2.1.0"
|
|
63
|
-
|
|
64
|
-
[dev-dependencies]
|
|
65
|
-
pretty_assertions = "1.4"
|
package/build.rs
DELETED
package/src/bundler.rs
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
//! Zenith Bundler Configuration
|
|
2
|
-
//!
|
|
3
|
-
//! Provides helper functions to create a pre-configured Rolldown bundler
|
|
4
|
-
//! with Zenith's required settings (capability splitting, plugin injection, etc).
|
|
5
|
-
|
|
6
|
-
use rolldown::{Bundler, BundlerBuilder, BundlerOptions, InputItem};
|
|
7
|
-
use std::sync::Arc;
|
|
8
|
-
|
|
9
|
-
use crate::plugin::ZenithPlugin;
|
|
10
|
-
|
|
11
|
-
/// Create a configured Rolldown bundler for a Zenith project
|
|
12
|
-
pub fn create_zenith_bundler(entry_point: &str, components_dir: Option<&str>) -> Bundler {
|
|
13
|
-
// 1. Initialize the Zenith Plugin
|
|
14
|
-
let mut plugin = ZenithPlugin::new(entry_point);
|
|
15
|
-
if let Some(dir) = components_dir {
|
|
16
|
-
plugin = plugin.with_components_dir(dir);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// 2. Configure Bundler Options
|
|
20
|
-
// Note: manual_chunks is currently experimental in Rolldown Rust API,
|
|
21
|
-
// we'll need to verify the exact API surface.
|
|
22
|
-
// For now, we rely on the plugin's dynamic imports to drive chunking naturally,
|
|
23
|
-
// and we can enhance this with explicit manual_chunks if the API allows.
|
|
24
|
-
|
|
25
|
-
let options = BundlerOptions {
|
|
26
|
-
input: Some(vec![InputItem {
|
|
27
|
-
name: Some("index".into()),
|
|
28
|
-
import: "virtual:zenith-entry".into(), // Start with our Hydration Controller
|
|
29
|
-
}]),
|
|
30
|
-
// Enable code splitting
|
|
31
|
-
format: Some(rolldown_common::OutputFormat::Esm),
|
|
32
|
-
// Ensure we target browser environment
|
|
33
|
-
platform: Some(rolldown_common::Platform::Browser),
|
|
34
|
-
// Capability-Based Chunking:
|
|
35
|
-
// GSAP should be handled as a separate chunk (capability: "anim").
|
|
36
|
-
// We rely on dynamic imports (import('gsap')) in the user's code to automatically split it.
|
|
37
|
-
// Explicit manual_chunks can be added here if stricter control is needed.
|
|
38
|
-
// Capability-based chunking configuration will go here once verified
|
|
39
|
-
..Default::default()
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
let builder = BundlerBuilder::default()
|
|
43
|
-
.with_options(options)
|
|
44
|
-
.with_plugins(vec![Arc::new(plugin)]);
|
|
45
|
-
|
|
46
|
-
builder.build().expect("Failed to build bundler")
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/// Create a configured Rolldown bundler for Dev Mode (Watch + HMR + InMemory)
|
|
50
|
-
pub fn create_dev_bundler(
|
|
51
|
-
entry_point: &str,
|
|
52
|
-
components_dir: Option<&str>,
|
|
53
|
-
store: std::sync::Arc<crate::store::AssetStore>,
|
|
54
|
-
) -> Bundler {
|
|
55
|
-
// 1. Initialize the Zenith Plugin with Store and Dev Mode
|
|
56
|
-
let mut plugin = ZenithPlugin::new(entry_point)
|
|
57
|
-
.with_store(store)
|
|
58
|
-
.with_dev_mode(true);
|
|
59
|
-
|
|
60
|
-
if let Some(dir) = components_dir {
|
|
61
|
-
plugin = plugin.with_components_dir(dir);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 2. Configure Bundler Options (Dev Optimized)
|
|
65
|
-
let options = BundlerOptions {
|
|
66
|
-
input: Some(vec![InputItem {
|
|
67
|
-
name: Some("index".into()),
|
|
68
|
-
import: "virtual:zenith-entry".into(),
|
|
69
|
-
}]),
|
|
70
|
-
format: Some(rolldown_common::OutputFormat::Esm),
|
|
71
|
-
platform: Some(rolldown_common::Platform::Browser),
|
|
72
|
-
sourcemap: Some(rolldown_common::SourceMapType::File), // Enable sourcemaps for dev
|
|
73
|
-
..Default::default()
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
let builder = BundlerBuilder::default()
|
|
77
|
-
.with_options(options)
|
|
78
|
-
.with_plugins(vec![Arc::new(plugin)]);
|
|
79
|
-
|
|
80
|
-
builder.build().expect("Failed to build dev bundler")
|
|
81
|
-
}
|
package/src/css.rs
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
//! CSS Buffer and Pruning Module
|
|
2
|
-
//!
|
|
3
|
-
//! Handles buffering CSS from .zen files and pruning unused classes
|
|
4
|
-
//! using lightningcss with ZenManifest.css_classes as the allow-list.
|
|
5
|
-
//!
|
|
6
|
-
//! Uses AST-based pruning via lightningcss to ensure safety and correctness.
|
|
7
|
-
|
|
8
|
-
use dashmap::DashMap;
|
|
9
|
-
use lightningcss::rules::CssRule;
|
|
10
|
-
use lightningcss::selector::Component;
|
|
11
|
-
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet};
|
|
12
|
-
use lightningcss::targets::Browsers;
|
|
13
|
-
use std::collections::HashSet;
|
|
14
|
-
|
|
15
|
-
/// Thread-safe CSS buffer for collecting styles from .zen files
|
|
16
|
-
#[derive(Debug)]
|
|
17
|
-
pub struct CssBuffer {
|
|
18
|
-
/// CSS content keyed by file path
|
|
19
|
-
styles: DashMap<String, String>,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
impl CssBuffer {
|
|
23
|
-
pub fn new() -> Self {
|
|
24
|
-
Self {
|
|
25
|
-
styles: DashMap::new(),
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/// Insert CSS content for a file
|
|
30
|
-
pub fn insert(&self, file_id: String, css: String) {
|
|
31
|
-
self.styles.insert(file_id, css);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/// Get all buffered CSS
|
|
35
|
-
pub fn get_all(&self) -> Vec<String> {
|
|
36
|
-
self.styles.iter().map(|r| r.value().clone()).collect()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// Stitch all CSS and prune unused classes
|
|
40
|
-
///
|
|
41
|
-
/// Strategy:
|
|
42
|
-
/// 1. Parse the CSS into AST using lightningcss
|
|
43
|
-
/// 2. Walk the AST and remove rules/selectors that allow pruning
|
|
44
|
-
/// 3. Minify and print the result
|
|
45
|
-
pub fn stitch_and_prune(&self, used_classes: &[String]) -> Result<String, String> {
|
|
46
|
-
let all_css: String = self
|
|
47
|
-
.styles
|
|
48
|
-
.iter()
|
|
49
|
-
.map(|r| r.value().clone())
|
|
50
|
-
.collect::<Vec<_>>()
|
|
51
|
-
.join("\n");
|
|
52
|
-
|
|
53
|
-
if all_css.is_empty() {
|
|
54
|
-
return Ok(String::new());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Build allow-list from used classes
|
|
58
|
-
let used_set: HashSet<&str> = used_classes.iter().map(|s| s.as_str()).collect();
|
|
59
|
-
|
|
60
|
-
// 1. Parse CSS
|
|
61
|
-
let mut stylesheet = StyleSheet::parse(&all_css, ParserOptions::default())
|
|
62
|
-
.map_err(|e| format!("CSS parse error: {:?}", e))?;
|
|
63
|
-
|
|
64
|
-
// 2. Prune AST (Recursive)
|
|
65
|
-
// Accessing rules directly requires ensuring we can iterate mutably
|
|
66
|
-
let rules_vec = &mut stylesheet.rules.0;
|
|
67
|
-
prune_rules(rules_vec, &used_set);
|
|
68
|
-
|
|
69
|
-
// 3. Minify and Print
|
|
70
|
-
|
|
71
|
-
stylesheet
|
|
72
|
-
.minify(MinifyOptions {
|
|
73
|
-
targets: Browsers::default().into(),
|
|
74
|
-
..Default::default()
|
|
75
|
-
})
|
|
76
|
-
.map_err(|e| format!("CSS minify error: {:?}", e))?;
|
|
77
|
-
|
|
78
|
-
let result = stylesheet
|
|
79
|
-
.to_css(PrinterOptions {
|
|
80
|
-
minify: true,
|
|
81
|
-
..Default::default()
|
|
82
|
-
})
|
|
83
|
-
.map_err(|e| format!("CSS print error: {:?}", e))?;
|
|
84
|
-
|
|
85
|
-
Ok(result.code)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Clear all buffered CSS
|
|
89
|
-
pub fn clear(&self) {
|
|
90
|
-
self.styles.clear();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
impl Default for CssBuffer {
|
|
95
|
-
fn default() -> Self {
|
|
96
|
-
Self::new()
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/// Recursively prune CSS rules
|
|
101
|
-
///
|
|
102
|
-
/// Returns true if rule should be kept, false if it should be removed (if single rule context)
|
|
103
|
-
/// But here we operate on Vec<CssRule>, so we use retain_mut.
|
|
104
|
-
fn prune_rules(rules: &mut Vec<CssRule>, used_set: &HashSet<&str>) {
|
|
105
|
-
rules.retain_mut(|rule| {
|
|
106
|
-
match rule {
|
|
107
|
-
CssRule::Style(style_rule) => {
|
|
108
|
-
// Filter selectors in this rule
|
|
109
|
-
// style_rule.selectors is SelectorList.
|
|
110
|
-
|
|
111
|
-
// We iterate and keep selectors that are "used"
|
|
112
|
-
style_rule
|
|
113
|
-
.selectors
|
|
114
|
-
.0
|
|
115
|
-
.retain(|selector| is_selector_used(selector, used_set));
|
|
116
|
-
|
|
117
|
-
// Determine if we keep the rule:
|
|
118
|
-
// If NO selectors remain, the rule is empty and should be removed.
|
|
119
|
-
!style_rule.selectors.0.is_empty()
|
|
120
|
-
}
|
|
121
|
-
CssRule::Media(media_rule) => {
|
|
122
|
-
// Recursively prune rules inside @media
|
|
123
|
-
// media_rule.rules is CssRuleList (which wraps Vec<CssRule>).
|
|
124
|
-
// Access via .0
|
|
125
|
-
prune_rules(&mut media_rule.rules.0, used_set);
|
|
126
|
-
|
|
127
|
-
// Keep media rule only if it still has rules inside
|
|
128
|
-
!media_rule.rules.0.is_empty()
|
|
129
|
-
}
|
|
130
|
-
CssRule::Supports(supports_rule) => {
|
|
131
|
-
prune_rules(&mut supports_rule.rules.0, used_set);
|
|
132
|
-
!supports_rule.rules.0.is_empty()
|
|
133
|
-
}
|
|
134
|
-
// For other rules (Keyframes, FontFace, etc.), we keep them ALWAYS.
|
|
135
|
-
// We do not prune keyframes based on usage yet (harder analysis).
|
|
136
|
-
_ => true,
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/// Determine if a selector usage deems it valid to keep.
|
|
142
|
-
///
|
|
143
|
-
/// POLICY: CONSERVATIVE
|
|
144
|
-
/// - If selector has NO classes -> KEEP (Element, ID, *, etc.)
|
|
145
|
-
/// - If selector has ANY class that is in `used_set` -> KEEP.
|
|
146
|
-
/// - Only remove if ALL classes in the selector are KNOWN UNUSED.
|
|
147
|
-
fn is_selector_used(selector: &lightningcss::selector::Selector, used_set: &HashSet<&str>) -> bool {
|
|
148
|
-
let mut has_classes = false;
|
|
149
|
-
let mut any_used = false;
|
|
150
|
-
|
|
151
|
-
// Iterate over components in the selector
|
|
152
|
-
// Selector iteration yields &Component
|
|
153
|
-
for component in selector.iter() {
|
|
154
|
-
if let Component::Class(ident) = component {
|
|
155
|
-
has_classes = true;
|
|
156
|
-
// ident is Atom or similar string-like. as_ref() works for AsRef<str>.
|
|
157
|
-
if used_set.contains(ident.as_ref()) {
|
|
158
|
-
any_used = true;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if !has_classes {
|
|
164
|
-
// No classes involved (e.g. "div", "#app", "*"), so keep it.
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Has classes. Keep ONLY if at least one class is used.
|
|
169
|
-
any_used
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
#[cfg(test)]
|
|
173
|
-
mod tests {
|
|
174
|
-
use super::*;
|
|
175
|
-
|
|
176
|
-
#[test]
|
|
177
|
-
fn test_css_buffer_insert_and_get() {
|
|
178
|
-
let buffer = CssBuffer::new();
|
|
179
|
-
buffer.insert("a.zen".into(), ".foo { color: red; }".into());
|
|
180
|
-
buffer.insert("b.zen".into(), ".bar { color: blue; }".into());
|
|
181
|
-
|
|
182
|
-
let all = buffer.get_all();
|
|
183
|
-
assert_eq!(all.len(), 2);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
#[test]
|
|
187
|
-
fn test_css_stitch_and_minify() {
|
|
188
|
-
let buffer = CssBuffer::new();
|
|
189
|
-
buffer.insert("a.zen".into(), ".foo { color: red; }".into());
|
|
190
|
-
|
|
191
|
-
let result = buffer.stitch_and_prune(&["foo".into()]).unwrap();
|
|
192
|
-
assert!(result.contains("color:") || result.contains("color:red"));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
#[test]
|
|
196
|
-
fn test_css_pruning_removes_unused() {
|
|
197
|
-
let buffer = CssBuffer::new();
|
|
198
|
-
buffer.insert(
|
|
199
|
-
"a.zen".into(),
|
|
200
|
-
".foo { color: red; } .bar { color: blue; } .baz { color: green; }".into(),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
// Only "foo" is used, "bar" and "baz" should be pruned
|
|
204
|
-
let result = buffer.stitch_and_prune(&["foo".into()]).unwrap();
|
|
205
|
-
assert!(
|
|
206
|
-
result.contains("red"),
|
|
207
|
-
"Should keep .foo (used): {}",
|
|
208
|
-
result
|
|
209
|
-
);
|
|
210
|
-
assert!(
|
|
211
|
-
!result.contains("blue"),
|
|
212
|
-
"Should prune .bar (unused): {}",
|
|
213
|
-
result
|
|
214
|
-
);
|
|
215
|
-
assert!(
|
|
216
|
-
!result.contains("green"),
|
|
217
|
-
"Should prune .baz (unused): {}",
|
|
218
|
-
result
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
#[test]
|
|
223
|
-
fn test_keeps_element_selectors() {
|
|
224
|
-
let buffer = CssBuffer::new();
|
|
225
|
-
buffer.insert(
|
|
226
|
-
"a.zen".into(),
|
|
227
|
-
"body { margin: 0; } h1 { font-size: 2rem; }".into(),
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// Element selectors should always be kept
|
|
231
|
-
let result = buffer.stitch_and_prune(&[]).unwrap();
|
|
232
|
-
assert!(
|
|
233
|
-
result.contains("margin") || result.contains("0"),
|
|
234
|
-
"Should keep body selector: {}",
|
|
235
|
-
result
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
#[test]
|
|
240
|
-
fn test_keeps_id_selectors() {
|
|
241
|
-
let buffer = CssBuffer::new();
|
|
242
|
-
buffer.insert("a.zen".into(), "#app { display: flex; }".into());
|
|
243
|
-
|
|
244
|
-
// ID selectors should always be kept
|
|
245
|
-
let result = buffer.stitch_and_prune(&[]).unwrap();
|
|
246
|
-
assert!(
|
|
247
|
-
result.contains("flex"),
|
|
248
|
-
"Should keep #app selector: {}",
|
|
249
|
-
result
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
#[test]
|
|
254
|
-
fn test_keeps_used_class_in_compound() {
|
|
255
|
-
let buffer = CssBuffer::new();
|
|
256
|
-
buffer.insert("a.zen".into(), ".foo.bar { color: red; }".into());
|
|
257
|
-
|
|
258
|
-
// If either class is used, keep the rule
|
|
259
|
-
let result = buffer.stitch_and_prune(&["foo".into()]).unwrap();
|
|
260
|
-
assert!(
|
|
261
|
-
result.contains("red"),
|
|
262
|
-
"Should keep .foo.bar when foo is used: {}",
|
|
263
|
-
result
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
}
|
package/src/html.rs
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
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/lib.rs
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
}
|