@zenithbuild/bundler 1.3.1

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/src/plugin.rs ADDED
@@ -0,0 +1,332 @@
1
+ //! ZenithPlugin - Rolldown Plugin for .zen file compilation
2
+ //!
3
+ //! Implements the Rolldown Plugin trait to:
4
+ //! 1. Intercept `.zen` file imports via `resolve_id`
5
+ //! 2. Compile `.zen` files via Zenith compiler in `load`
6
+ //! 3. Buffer CSS for later pruning/stitching
7
+ //! 4. Emit optimized CSS in `generate_bundle`
8
+
9
+ use std::sync::Arc;
10
+
11
+ use dashmap::DashMap;
12
+ use rolldown_plugin::{
13
+ Plugin, PluginContext, HookResolveIdArgs, HookResolveIdOutput,
14
+ HookLoadArgs, HookLoadOutput, HookGenerateBundleArgs, HookUsage,
15
+ HookTransformArgs, HookTransformReturn,
16
+ LoadPluginContext, TransformPluginContext,
17
+ };
18
+ use rolldown_common::{EmittedAsset, ResolvedExternal, OutputAsset, OutputChunk, Output, StrOrBytes};
19
+
20
+ use crate::css::CssBuffer;
21
+ use crate::store::AssetStore;
22
+
23
+ // Re-export ZenManifestExport from compiler-native as our canonical Manifest type
24
+ pub use compiler_native::{ZenManifestExport as ZenManifest, compile_zen_internal, CompileOptions, CompileResult};
25
+
26
+ /// The Zenith Plugin for Rolldown
27
+ #[derive(Debug)]
28
+ pub struct ZenithPlugin {
29
+ /// Buffer for CSS extracted from .zen files
30
+ css_buffer: Arc<CssBuffer>,
31
+ /// Collected CSS classes for pruning
32
+ used_classes: Arc<DashMap<String, ()>>,
33
+ /// Components directory path
34
+ components_dir: Option<String>,
35
+ /// User's entry point (e.g., "./src/main.zen")
36
+ entry_point: String,
37
+
38
+ /// In-memory asset store for Dev Server (optional)
39
+ store: Option<Arc<AssetStore>>,
40
+
41
+ /// Dev mode flag (enables HMR footer injection)
42
+ is_dev: bool,
43
+ }
44
+
45
+ impl ZenithPlugin {
46
+ pub fn new(entry_point: impl Into<String>) -> Self {
47
+ Self {
48
+ css_buffer: Arc::new(CssBuffer::new()),
49
+ used_classes: Arc::new(DashMap::new()),
50
+ components_dir: None,
51
+ entry_point: entry_point.into(),
52
+ store: None,
53
+ is_dev: false,
54
+ }
55
+ }
56
+
57
+ pub fn with_store(mut self, store: Arc<AssetStore>) -> Self {
58
+ self.store = Some(store);
59
+ self
60
+ }
61
+
62
+ pub fn with_dev_mode(mut self, is_dev: bool) -> Self {
63
+ self.is_dev = is_dev;
64
+ self
65
+ }
66
+
67
+ pub fn with_components_dir(mut self, dir: impl Into<String>) -> Self {
68
+ self.components_dir = Some(dir.into());
69
+ self
70
+ }
71
+
72
+ /// Get the collected CSS buffer for final emission
73
+ pub fn css_buffer(&self) -> Arc<CssBuffer> {
74
+ Arc::clone(&self.css_buffer)
75
+ }
76
+
77
+ /// Get all used CSS classes for pruning
78
+ pub fn used_classes(&self) -> Vec<String> {
79
+ self.used_classes.iter().map(|r| r.key().clone()).collect()
80
+ }
81
+ }
82
+
83
+ impl Plugin for ZenithPlugin {
84
+ fn name(&self) -> std::borrow::Cow<'static, str> {
85
+ std::borrow::Cow::Borrowed("zenith")
86
+ }
87
+
88
+ fn register_hook_usage(&self) -> HookUsage {
89
+ HookUsage::ResolveId | HookUsage::Load | HookUsage::GenerateBundle | HookUsage::Transform
90
+ }
91
+
92
+ /// Inject HMR footer if in dev mode
93
+ async fn transform(
94
+ &self,
95
+ _ctx: std::sync::Arc<TransformPluginContext>,
96
+ args: &HookTransformArgs<'_>,
97
+ ) -> HookTransformReturn {
98
+ if !self.is_dev {
99
+ return Ok(None);
100
+ }
101
+
102
+ if args.id.ends_with(".zen") {
103
+ let mut code = args.code.to_string();
104
+ // Inject HMR Logic
105
+ let footer = format!(
106
+ r#"
107
+ if (import.meta.hot) {{
108
+ import.meta.hot.accept((newModule) => {{
109
+ // Surgical Re-Mount Logic
110
+ // Find anchors with data-z-id matching this file?
111
+ // For now, reload page if hydration fails?
112
+ // Or assume the component handles re-mount?
113
+ // newModule.default(target, props);
114
+ }});
115
+ }}
116
+ "#
117
+ );
118
+ code.push_str(&footer);
119
+
120
+ return Ok(Some(rolldown_plugin::HookTransformOutput {
121
+ code: Some(code),
122
+ ..Default::default()
123
+ }));
124
+ }
125
+ Ok(None)
126
+ }
127
+
128
+ /// Intercept .zen file imports
129
+ async fn resolve_id(
130
+ &self,
131
+ _ctx: &PluginContext,
132
+ args: &HookResolveIdArgs<'_>,
133
+ ) -> rolldown_plugin::HookResolveIdReturn {
134
+ let specifier = args.specifier;
135
+
136
+ // Handle .zen files
137
+ if specifier.ends_with(".zen") {
138
+ return Ok(Some(HookResolveIdOutput {
139
+ id: specifier.to_string().into(),
140
+ external: Some(ResolvedExternal::Bool(false)),
141
+ ..Default::default()
142
+ }));
143
+ }
144
+
145
+ // Handle virtual entry
146
+ if specifier == "virtual:zenith-entry" || specifier.starts_with("\0zenith:") {
147
+ return Ok(Some(HookResolveIdOutput {
148
+ id: specifier.to_string().into(),
149
+ external: Some(ResolvedExternal::Bool(false)),
150
+ ..Default::default()
151
+ }));
152
+ }
153
+
154
+ Ok(None)
155
+ }
156
+
157
+ /// Load and compile .zen files
158
+ async fn load(
159
+ &self,
160
+ _ctx: Arc<LoadPluginContext>,
161
+ args: &HookLoadArgs<'_>,
162
+ ) -> rolldown_plugin::HookLoadReturn {
163
+ let id = &args.id;
164
+
165
+ // Handle virtual entry - this is the Hydration Controller
166
+ if &**id == "virtual:zenith-entry" {
167
+ return Ok(Some(HookLoadOutput {
168
+ code: self.generate_hydration_controller().into(),
169
+ ..Default::default()
170
+ }));
171
+ }
172
+
173
+ // Handle .zen files
174
+ if id.ends_with(".zen") {
175
+ let source = match std::fs::read_to_string(&**id) {
176
+ Ok(s) => s,
177
+ Err(e) => {
178
+ return Err(anyhow::anyhow!("Failed to read .zen file: {}", e));
179
+ }
180
+ };
181
+
182
+ // Compile using internal Rust-to-Rust API (no JSON serialization)
183
+ let result = compile_zen_internal(&source, &**id, CompileOptions::default())
184
+ .map_err(|e| anyhow::anyhow!("{}", e))?;
185
+
186
+ if result.has_errors {
187
+ return Err(anyhow::anyhow!("Compilation errors: {:?}", result.errors));
188
+ }
189
+
190
+ // Get manifest for capability info
191
+ let manifest = result.manifest.ok_or_else(|| anyhow::anyhow!("No manifest generated"))?;
192
+
193
+ // Buffer the CSS for later pruning/emission
194
+ if !manifest.styles.is_empty() {
195
+ self.css_buffer.insert(id.to_string(), manifest.styles.clone());
196
+ }
197
+
198
+ // Collect CSS classes for pruning
199
+ for class in &manifest.css_classes {
200
+ self.used_classes.insert(class.to_owned(), ());
201
+ }
202
+
203
+ // Generate the module code (script + expressions)
204
+ let js_code = self.generate_module_code(&manifest);
205
+
206
+ return Ok(Some(HookLoadOutput {
207
+ code: js_code.into(),
208
+ ..Default::default()
209
+ }));
210
+ }
211
+
212
+ Ok(None)
213
+ }
214
+
215
+ /// Emit the final optimized CSS asset
216
+ async fn generate_bundle(
217
+ &self,
218
+ ctx: &PluginContext,
219
+ args: &mut HookGenerateBundleArgs<'_>,
220
+ ) -> rolldown_plugin::HookNoopReturn {
221
+ // 1. Populate Store (if present)
222
+ if let Some(store) = &self.store {
223
+ for output in args.bundle.iter() {
224
+ match output {
225
+ Output::Asset(a) => {
226
+ // Attempt to extract source string
227
+ // rolldown_common::StrOrBytes (Assuming Str/Bytes variants)
228
+ let source = match &a.source {
229
+ StrOrBytes::Str(s) => s.to_string(),
230
+ StrOrBytes::Bytes(b) => String::from_utf8_lossy(b).to_string(),
231
+ };
232
+ store.update(a.filename.to_string(), source);
233
+ }
234
+ Output::Chunk(c) => {
235
+ store.update(c.filename.to_string(), c.code.clone());
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ let used_classes = self.used_classes();
242
+ let css_content = self.css_buffer.stitch_and_prune(&used_classes)
243
+ .map_err(|e| anyhow::anyhow!("{}", e))?;
244
+
245
+ if !css_content.is_empty() {
246
+ // Emit the CSS asset
247
+ let asset = EmittedAsset {
248
+ name: Some("zenith.css".into()),
249
+ file_name: None,
250
+ original_file_name: None,
251
+ source: css_content.into_bytes().into(),
252
+ };
253
+ ctx.emit_file(asset, None, None)?;
254
+ }
255
+
256
+ Ok(())
257
+ }
258
+ }
259
+
260
+ impl ZenithPlugin {
261
+ /// Generate the Hydration Controller (Bootstrap Loader)
262
+ ///
263
+ /// This is the entry point that:
264
+ /// 1. Immediately: Sets up event delegation (zero-cost, <2KB)
265
+ /// 2. Deferred: Imports the actual app logic via dynamic import
266
+ /// 3. Trigger: Idle callback or timeout
267
+ fn generate_hydration_controller(&self) -> String {
268
+ let entry = &self.entry_point;
269
+ format!(r#"
270
+ // === ZENITH HYDRATION CONTROLLER ===
271
+ // Generated by ZenithPlugin
272
+
273
+ import {{ delegateEvents }} from 'zenith/runtime/core';
274
+
275
+ // 1. Immediate: Global listeners (The "Zero-Cost" part)
276
+ delegateEvents();
277
+
278
+ // 2. Deferred: The actual App Logic (The "Heavy" part)
279
+ // We wrap the user's entry in a dynamic import to keep it off the main thread
280
+ const hydrate = () => import('{entry}');
281
+
282
+ // 3. Trigger: Interaction or Idle
283
+ if ('requestIdleCallback' in window) {{
284
+ requestIdleCallback(hydrate, {{ timeout: 2000 }});
285
+ }} else {{
286
+ // Fallback for Safari/older browsers
287
+ setTimeout(hydrate, 200);
288
+ }}
289
+ "#)
290
+ }
291
+
292
+ /// Generate the module code for a compiled .zen file
293
+ fn generate_module_code(&self, manifest: &ZenManifest) -> String {
294
+ let mut code = String::new();
295
+
296
+ // NPM imports first
297
+ if !manifest.npm_imports.is_empty() {
298
+ code.push_str(&manifest.npm_imports);
299
+ code.push('\n');
300
+ }
301
+
302
+ // Author script (component logic)
303
+ if !manifest.script.is_empty() {
304
+ code.push_str("\n// --- COMPONENT SCRIPT ---\n");
305
+ code.push_str(&manifest.script);
306
+ code.push('\n');
307
+ }
308
+
309
+ // Expressions (reactive bindings)
310
+ if !manifest.expressions.is_empty() {
311
+ code.push_str("\n// --- EXPRESSIONS ---\n");
312
+ code.push_str(&manifest.expressions);
313
+ code.push('\n');
314
+ }
315
+
316
+ // Template (for hydration)
317
+ if !manifest.template.is_empty() {
318
+ code.push_str("\n// --- TEMPLATE (for hydration) ---\n");
319
+ code.push_str(&format!("export const __ZENITH_TEMPLATE__ = `{}`;\n",
320
+ manifest.template.replace("`", "\\`").replace("${", "\\${")));
321
+ }
322
+
323
+ // Export capabilities for code splitting
324
+ code.push_str("\n// --- CAPABILITIES ---\n");
325
+ code.push_str(&format!("export const __ZENITH_CAPABILITIES__ = {:?};\n", manifest.required_capabilities));
326
+ code.push_str(&format!("export const __ZENITH_USES_STATE__ = {};\n", manifest.uses_state));
327
+ code.push_str(&format!("export const __ZENITH_HAS_EVENTS__ = {};\n", manifest.has_events));
328
+ code.push_str(&format!("export const __ZENITH_IS_STATIC__ = {};\n", manifest.is_static));
329
+
330
+ code
331
+ }
332
+ }
package/src/store.rs ADDED
@@ -0,0 +1,44 @@
1
+ //! In-memory Asset Store for Dev Server
2
+ //!
3
+ //! Provides a thread-safe DashMap to store compiled assets (JS/CSS)
4
+ //! for memory-only serving in dev mode.
5
+
6
+ use dashmap::DashMap;
7
+ use std::sync::Arc;
8
+
9
+ /// Thread-safe in-memory asset store
10
+ #[derive(Debug, Clone)]
11
+ pub struct AssetStore {
12
+ /// Map of normalized file path (starts with /) to content
13
+ assets: Arc<DashMap<String, String>>,
14
+ }
15
+
16
+ impl AssetStore {
17
+ pub fn new() -> Self {
18
+ Self {
19
+ assets: Arc::new(DashMap::new()),
20
+ }
21
+ }
22
+
23
+ /// Update asset content
24
+ /// Automatically ensures path starts with /
25
+ pub fn update(&self, path: String, content: String) {
26
+ let normalized = if path.starts_with('/') {
27
+ path
28
+ } else {
29
+ format!("/{}", path)
30
+ };
31
+ self.assets.insert(normalized, content);
32
+ }
33
+
34
+ /// Retrieve asset content
35
+ pub fn get(&self, path: &str) -> Option<String> {
36
+ self.assets.get(path).map(|r| r.value().clone())
37
+ }
38
+ }
39
+
40
+ impl Default for AssetStore {
41
+ fn default() -> Self {
42
+ Self::new()
43
+ }
44
+ }
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <div class="hello">
3
+ <h1>Hello {{ name }}</h1>
4
+ <p class="description">Welcome to Zenith</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script>
9
+ prop name: string;
10
+
11
+ const greeting = "Hello";
12
+ console.log(greeting, name);
13
+ </script>
14
+
15
+ <style>
16
+ .hello { color: red; }
17
+ .description { font-size: 1.2rem; }
18
+ .unused { color: blue; }
19
+ </style>
@@ -0,0 +1,53 @@
1
+ //! Integration Tests for Zenith Bundler
2
+ //!
3
+ //! Verifies the full compilation pipeline using the Rolldown integration.
4
+
5
+ use std::path::PathBuf;
6
+ use zenith_bundler::bundler::create_zenith_bundler;
7
+
8
+ #[tokio::test]
9
+ async fn test_compile_hello_zen() {
10
+ let fixture_path = PathBuf::from("tests/fixtures/hello.zen");
11
+ let cwd = std::env::current_dir().unwrap();
12
+ let absolute_path = cwd.join(fixture_path);
13
+ let entry_str = absolute_path.to_str().unwrap();
14
+
15
+ // 1. Create the Bundler
16
+ let mut bundler = create_zenith_bundler(entry_str, None);
17
+
18
+ // 2. Generate the bundle (in-memory)
19
+ // bundler.generate() takes no arguments in the current version
20
+ let result = bundler.generate().await;
21
+
22
+ match result {
23
+ Ok(outputs) => {
24
+ println!("Bundle generation success!");
25
+
26
+ // 3. Verify Assets
27
+ for item in outputs.assets {
28
+ // Rolldown output is an enum (Asset or Chunk)
29
+ match item {
30
+ rolldown_common::Output::Asset(asset) => {
31
+ println!("Asset: {}", asset.filename);
32
+ if asset.filename.ends_with(".css") {
33
+ let content = match &asset.source {
34
+ rolldown_common::StrOrBytes::Str(s) => s.to_string(),
35
+ rolldown_common::StrOrBytes::Bytes(b) => String::from_utf8_lossy(b).to_string(),
36
+ };
37
+ assert!(content.contains(".hello"), "CSS should contain used classes");
38
+ }
39
+ }
40
+ rolldown_common::Output::Chunk(chunk) => {
41
+ println!("Chunk: {}", chunk.filename);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ Err(e) => {
47
+ // It's possible that without proper semantic analysis setup (OXC stuff)
48
+ // or node modules resolution, this might fail.
49
+ // But for a simple .zen file with no imports, it should work.
50
+ panic!("Bundler failed: {:?}", e);
51
+ }
52
+ }
53
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }