@ape-egg/vibe 1.3.2 → 1.6.0
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/CHANGELOG.md +177 -0
- package/README.md +97 -0
- package/boot.js +0 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +719 -40
- package/compiler/src/Cargo.toml +11 -2
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +552 -175
- package/compiler/src/compiler/component_tagger.rs +234 -0
- package/compiler/src/compiler/iteration_optimizer.rs +351 -0
- package/compiler/src/compiler/js_analyzer.rs +572 -0
- package/compiler/src/compiler/manifest_builder.rs +251 -26
- package/compiler/src/compiler/mod.rs +5 -1
- package/compiler/src/compiler/state_extractor.rs +140 -25
- package/compiler/src/compiler/value_stamper.rs +579 -88
- package/compiler/src/compiler/watcher.rs +579 -0
- package/compiler/src/config.rs +51 -8
- package/compiler/src/main.rs +41 -28
- package/compiler/src/parser/html.rs +229 -118
- package/component.js +23 -11
- package/package.json +1 -1
- package/runtime/cleanup.js +4 -4
- package/runtime/component.js +98 -21
- package/runtime/conditionals.js +2 -2
- package/runtime/constants.js +2 -1
- package/runtime/index.js +152 -30
- package/runtime/iterate.js +27 -5
- package/runtime/parse.js +2 -1
- package/runtime/pre-compiled-iterations.js +153 -0
- package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +204 -132
- package/runtime/utils.js +2 -1
- package/test-results/.last-run.json +4 -0
- package/vibe.css +19 -0
- package/runtime/component-state.js +0 -63
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::sync::mpsc::channel;
|
|
4
|
+
use std::time::Duration;
|
|
5
|
+
use colored::Colorize;
|
|
6
|
+
use notify_debouncer_full::{new_debouncer, notify::{RecursiveMode, Watcher}};
|
|
7
|
+
use regex::Regex;
|
|
8
|
+
|
|
9
|
+
use super::{Compiler, compile::should_skip_path};
|
|
10
|
+
use crate::config::Config;
|
|
11
|
+
|
|
12
|
+
/// Tracks which pages and components depend on which components
|
|
13
|
+
pub struct DependencyGraph {
|
|
14
|
+
/// component path -> set of page paths that use it
|
|
15
|
+
component_to_pages: HashMap<PathBuf, HashSet<PathBuf>>,
|
|
16
|
+
/// component path -> set of component paths that use it (for transitive dependencies)
|
|
17
|
+
component_to_components: HashMap<PathBuf, HashSet<PathBuf>>,
|
|
18
|
+
/// page path -> set of component paths it uses
|
|
19
|
+
page_to_components: HashMap<PathBuf, HashSet<PathBuf>>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl DependencyGraph {
|
|
23
|
+
pub fn new() -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
component_to_pages: HashMap::new(),
|
|
26
|
+
component_to_components: HashMap::new(),
|
|
27
|
+
page_to_components: HashMap::new(),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Add a dependency: file (page or component) uses component
|
|
32
|
+
pub fn add_dependency(&mut self, file: PathBuf, component: PathBuf, file_is_component: bool) {
|
|
33
|
+
if file_is_component {
|
|
34
|
+
// Component uses another component
|
|
35
|
+
self.component_to_components
|
|
36
|
+
.entry(component.clone())
|
|
37
|
+
.or_insert_with(HashSet::new)
|
|
38
|
+
.insert(file);
|
|
39
|
+
} else {
|
|
40
|
+
// Page uses component
|
|
41
|
+
self.component_to_pages
|
|
42
|
+
.entry(component.clone())
|
|
43
|
+
.or_insert_with(HashSet::new)
|
|
44
|
+
.insert(file.clone());
|
|
45
|
+
|
|
46
|
+
self.page_to_components
|
|
47
|
+
.entry(file)
|
|
48
|
+
.or_insert_with(HashSet::new)
|
|
49
|
+
.insert(component);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Get all pages that transitively depend on a component (includes component -> component -> page chains)
|
|
54
|
+
pub fn get_all_dependent_pages(&self, component: &Path) -> HashSet<PathBuf> {
|
|
55
|
+
let mut all_pages = HashSet::new();
|
|
56
|
+
let mut visited_components = HashSet::new();
|
|
57
|
+
self.collect_dependent_pages(component, &mut all_pages, &mut visited_components);
|
|
58
|
+
all_pages
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn collect_dependent_pages(
|
|
62
|
+
&self,
|
|
63
|
+
component: &Path,
|
|
64
|
+
all_pages: &mut HashSet<PathBuf>,
|
|
65
|
+
visited: &mut HashSet<PathBuf>,
|
|
66
|
+
) {
|
|
67
|
+
// Avoid infinite loops in circular dependencies
|
|
68
|
+
if !visited.insert(component.to_path_buf()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add pages that directly use this component
|
|
73
|
+
if let Some(pages) = self.component_to_pages.get(component) {
|
|
74
|
+
all_pages.extend(pages.iter().cloned());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Recursively add pages that use components that use this component
|
|
78
|
+
if let Some(dependent_components) = self.component_to_components.get(component) {
|
|
79
|
+
for dep_component in dependent_components {
|
|
80
|
+
self.collect_dependent_pages(dep_component, all_pages, visited);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Extract component references from HTML content
|
|
87
|
+
pub fn extract_component_dependencies(html: &str, components_dir: &Path) -> HashSet<PathBuf> {
|
|
88
|
+
let mut deps = HashSet::new();
|
|
89
|
+
|
|
90
|
+
// 1. <component src="/components/card.html"> - works in both compiled and runtime modes
|
|
91
|
+
let component_tag_re = Regex::new(r#"<component\s+[^>]*src=["']([^"']+)["']"#).unwrap();
|
|
92
|
+
for cap in component_tag_re.captures_iter(html) {
|
|
93
|
+
if let Some(src) = cap.get(1) {
|
|
94
|
+
let src_path = src.as_str().trim_start_matches('/');
|
|
95
|
+
deps.insert(PathBuf::from(src_path));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2. Custom elements: <UserCard> -> /components/user-card.html
|
|
100
|
+
let custom_element_re = Regex::new(r"<([A-Z][a-zA-Z0-9]*)[\s>]").unwrap();
|
|
101
|
+
for cap in custom_element_re.captures_iter(html) {
|
|
102
|
+
if let Some(tag) = cap.get(1) {
|
|
103
|
+
let kebab = to_kebab_case(tag.as_str());
|
|
104
|
+
let component_path = components_dir.join(format!("{}.html", kebab));
|
|
105
|
+
// Store relative to source root
|
|
106
|
+
if let Ok(rel_path) = component_path.strip_prefix("/") {
|
|
107
|
+
deps.insert(rel_path.to_path_buf());
|
|
108
|
+
} else {
|
|
109
|
+
deps.insert(component_path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
deps
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Convert PascalCase to kebab-case
|
|
118
|
+
fn to_kebab_case(s: &str) -> String {
|
|
119
|
+
let mut result = String::new();
|
|
120
|
+
for (i, ch) in s.chars().enumerate() {
|
|
121
|
+
if ch.is_uppercase() && i > 0 {
|
|
122
|
+
result.push('-');
|
|
123
|
+
}
|
|
124
|
+
result.push(ch.to_ascii_lowercase());
|
|
125
|
+
}
|
|
126
|
+
result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Check if a path should be blacklisted based on SKIP_FILES patterns
|
|
130
|
+
/// This checks both the filename and all path components relative to source root
|
|
131
|
+
fn is_path_blacklisted(path: &Path, source_root: &Path) -> bool {
|
|
132
|
+
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
|
133
|
+
|
|
134
|
+
// Check filename against blacklist
|
|
135
|
+
if should_skip_path(path, file_name) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if any path component (relative to source) matches blacklist
|
|
140
|
+
if let Ok(relative) = path.strip_prefix(source_root) {
|
|
141
|
+
for component in relative.components() {
|
|
142
|
+
if let Some(component_str) = component.as_os_str().to_str() {
|
|
143
|
+
if should_skip_path(path, component_str) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Build dependency graph by scanning all HTML files
|
|
154
|
+
pub fn build_dependency_graph(config: &Config) -> std::result::Result<DependencyGraph, std::io::Error> {
|
|
155
|
+
let mut graph = DependencyGraph::new();
|
|
156
|
+
|
|
157
|
+
// Canonicalize output path for reliable comparison
|
|
158
|
+
let canonical_output = config.output.canonicalize()
|
|
159
|
+
.unwrap_or_else(|_| config.output.clone());
|
|
160
|
+
|
|
161
|
+
// Scan all HTML files in source directory
|
|
162
|
+
scan_directory(&config.source, &config.source, &config.components, &canonical_output, &mut graph)?;
|
|
163
|
+
|
|
164
|
+
Ok(graph)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn scan_directory(
|
|
168
|
+
dir: &Path,
|
|
169
|
+
source_root: &Path,
|
|
170
|
+
components_dir: &str,
|
|
171
|
+
output_dir: &Path,
|
|
172
|
+
graph: &mut DependencyGraph,
|
|
173
|
+
) -> std::result::Result<(), std::io::Error> {
|
|
174
|
+
if !dir.is_dir() {
|
|
175
|
+
return Ok(());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let components_path = source_root.join(components_dir);
|
|
179
|
+
|
|
180
|
+
for entry in std::fs::read_dir(dir)? {
|
|
181
|
+
let entry = entry?;
|
|
182
|
+
let path = entry.path();
|
|
183
|
+
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
|
184
|
+
|
|
185
|
+
if path.is_dir() {
|
|
186
|
+
// Skip output directory (dynamically check, not hardcoded "compiled")
|
|
187
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
188
|
+
if canonical_path.starts_with(output_dir) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Skip directories using shared skip logic
|
|
193
|
+
if should_skip_path(&path, file_name) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
scan_directory(&path, source_root, components_dir, output_dir, graph)?;
|
|
198
|
+
} else if let Some(ext) = path.extension() {
|
|
199
|
+
if ext == "html" {
|
|
200
|
+
let html = std::fs::read_to_string(&path)?;
|
|
201
|
+
let deps = extract_component_dependencies(&html, &components_path);
|
|
202
|
+
|
|
203
|
+
// Check if this file is itself a component
|
|
204
|
+
let file_is_component = path.starts_with(&components_path);
|
|
205
|
+
|
|
206
|
+
for dep in deps {
|
|
207
|
+
let dep_absolute = source_root.join(&dep);
|
|
208
|
+
// Canonicalize to handle case-insensitive filesystems (macOS)
|
|
209
|
+
let dep_canonical = dep_absolute.canonicalize().unwrap_or(dep_absolute);
|
|
210
|
+
graph.add_dependency(path.clone(), dep_canonical, file_is_component);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Ok(())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Start watching for file changes
|
|
220
|
+
pub fn watch(config: Config, verbose: bool) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
221
|
+
println!("{}", "Building dependency graph...".cyan());
|
|
222
|
+
let mut graph = build_dependency_graph(&config)?;
|
|
223
|
+
|
|
224
|
+
println!("{}", "Running initial compilation...".cyan());
|
|
225
|
+
let mut compiler = Compiler::new(config.clone(), verbose);
|
|
226
|
+
|
|
227
|
+
match compiler.compile() {
|
|
228
|
+
Ok(stats) => {
|
|
229
|
+
// Generate manifests BEFORE showing success (unless runtime-as-is is enabled)
|
|
230
|
+
let mut manifest_time_ms = 0.0;
|
|
231
|
+
let mut manifest_stats_result = None;
|
|
232
|
+
if !config.runtime_as_is {
|
|
233
|
+
match compiler.generate_manifests() {
|
|
234
|
+
Ok(manifest_stats) => {
|
|
235
|
+
manifest_time_ms = manifest_stats.total_time_ms;
|
|
236
|
+
manifest_stats_result = Some(manifest_stats);
|
|
237
|
+
}
|
|
238
|
+
Err(e) => {
|
|
239
|
+
eprintln!("\n{}: Manifest generation failed: {}", "Warning".yellow(), e);
|
|
240
|
+
eprintln!("Compilation succeeded but manifests were not generated.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Show success headline
|
|
246
|
+
println!("\n{}", "Initial compilation complete!".green().bold());
|
|
247
|
+
println!();
|
|
248
|
+
|
|
249
|
+
// Show individual phase timings (same as main.rs output)
|
|
250
|
+
|
|
251
|
+
// Show validation time if it happened
|
|
252
|
+
if let Some(validation_time) = stats.validation_time_ms {
|
|
253
|
+
println!("* Validated components in {:.0}ms", validation_time);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let total_components_unique = stats.internal_components_unique + stats.external_components_unique;
|
|
257
|
+
|
|
258
|
+
// Always show components line
|
|
259
|
+
if stats.components_as_is {
|
|
260
|
+
println!("* Compiled components (0 internal, 0 external) - \"components-as-is\": true");
|
|
261
|
+
} else if total_components_unique > 0 {
|
|
262
|
+
println!("* Compiled components ({} internal, {} external) in {:.0}ms",
|
|
263
|
+
stats.internal_components_unique,
|
|
264
|
+
stats.external_components_unique,
|
|
265
|
+
stats.components_time_ms
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if stats.files_compiled > 0 {
|
|
270
|
+
println!("* Compiled HTML ({} file{}) in {:.0}ms",
|
|
271
|
+
stats.files_compiled,
|
|
272
|
+
if stats.files_compiled == 1 { "" } else { "s" },
|
|
273
|
+
stats.compile_time_ms
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Show manifest stats between HTML and Copied files
|
|
278
|
+
if let Some(manifest_stats) = manifest_stats_result {
|
|
279
|
+
println!("* Generated manifests ({} file{}, {} skipped) in {:.0}ms",
|
|
280
|
+
manifest_stats.pages_processed,
|
|
281
|
+
if manifest_stats.pages_processed == 1 { "" } else { "s" },
|
|
282
|
+
manifest_stats.pages_skipped,
|
|
283
|
+
manifest_stats.total_time_ms
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if stats.files_copied > 0 {
|
|
288
|
+
println!("* Copied files ({} file{}) in {:.0}ms",
|
|
289
|
+
stats.files_copied,
|
|
290
|
+
if stats.files_copied == 1 { "" } else { "s" },
|
|
291
|
+
stats.copy_time_ms
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Show node_modules handling
|
|
296
|
+
if let Some(nm_time) = stats.node_modules_time_ms {
|
|
297
|
+
if stats.node_modules_copied_as_is {
|
|
298
|
+
println!("* copied node_modules in {:.0}ms", nm_time);
|
|
299
|
+
} else if let Some(ref pkg_manager) = stats.package_manager {
|
|
300
|
+
println!("* {} install in {:.0}ms", pkg_manager, nm_time);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Calculate total duration as sum of all individual operations
|
|
305
|
+
let total_duration_ms = stats.validation_time_ms.unwrap_or(0.0)
|
|
306
|
+
+ stats.components_time_ms
|
|
307
|
+
+ stats.compile_time_ms
|
|
308
|
+
+ stats.copy_time_ms
|
|
309
|
+
+ stats.node_modules_time_ms.unwrap_or(0.0)
|
|
310
|
+
+ manifest_time_ms;
|
|
311
|
+
|
|
312
|
+
println!("\n{} in {:.0}ms", "Compiled".green(), total_duration_ms);
|
|
313
|
+
println!();
|
|
314
|
+
}
|
|
315
|
+
Err(e) => {
|
|
316
|
+
eprintln!("{}: {}", "Error".red(), e);
|
|
317
|
+
eprintln!("Fix the errors and save to retry.");
|
|
318
|
+
println!();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
println!("{}", "Watching for changes... (Ctrl+C to stop)".cyan());
|
|
323
|
+
println!();
|
|
324
|
+
|
|
325
|
+
// Canonicalize output path for reliable comparison
|
|
326
|
+
let canonical_output = config.output.canonicalize()
|
|
327
|
+
.unwrap_or_else(|_| config.output.clone());
|
|
328
|
+
|
|
329
|
+
// Keep compiler and parser alive to reuse component cache across incremental compilations
|
|
330
|
+
let mut watch_compiler = Compiler::new(config.clone(), false);
|
|
331
|
+
let parser = {
|
|
332
|
+
use crate::parser::HtmlParser;
|
|
333
|
+
let mut p = HtmlParser::new(config.components_path());
|
|
334
|
+
if let Err(e) = p.load_elements() {
|
|
335
|
+
eprintln!("{}: Failed to load parser: {}", "Error".red(), e);
|
|
336
|
+
return Err(Box::new(e));
|
|
337
|
+
}
|
|
338
|
+
p
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
let (tx, rx) = channel();
|
|
342
|
+
let mut debouncer = new_debouncer(Duration::from_millis(100), None, tx)?;
|
|
343
|
+
|
|
344
|
+
debouncer.watcher().watch(&config.source, RecursiveMode::Recursive)?;
|
|
345
|
+
|
|
346
|
+
loop {
|
|
347
|
+
match rx.recv() {
|
|
348
|
+
Ok(result) => {
|
|
349
|
+
match result {
|
|
350
|
+
Ok(events) => {
|
|
351
|
+
// Collect unique paths from events
|
|
352
|
+
let mut changed_paths: HashSet<PathBuf> = HashSet::new();
|
|
353
|
+
|
|
354
|
+
for event in events {
|
|
355
|
+
for path in &event.paths {
|
|
356
|
+
// Skip blacklisted files/directories (check entire path, not just filename)
|
|
357
|
+
if is_path_blacklisted(path, &config.source) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if let Some(ext) = path.extension() {
|
|
362
|
+
if ext == "html" || ext == "css" || ext == "js" {
|
|
363
|
+
changed_paths.insert(path.clone());
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if changed_paths.is_empty() {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Determine what needs recompiling
|
|
374
|
+
let mut pages_to_recompile: HashSet<PathBuf> = HashSet::new();
|
|
375
|
+
|
|
376
|
+
for path in &changed_paths {
|
|
377
|
+
// Skip files in output directory (avoid infinite loop)
|
|
378
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
379
|
+
if canonical_path.starts_with(&canonical_output) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Skip blacklisted files/directories
|
|
384
|
+
if is_path_blacklisted(path, &config.source) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let relative_path = path.strip_prefix(&config.source)
|
|
389
|
+
.unwrap_or(path);
|
|
390
|
+
|
|
391
|
+
// Check if it's a component
|
|
392
|
+
if path.starts_with(&config.source.join(&config.components)) {
|
|
393
|
+
// Component changed - recompile all transitively dependent pages
|
|
394
|
+
// Canonicalize to match how dependencies were stored (handles case sensitivity)
|
|
395
|
+
let path_canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
396
|
+
let dependent_pages = graph.get_all_dependent_pages(&path_canonical);
|
|
397
|
+
if !dependent_pages.is_empty() {
|
|
398
|
+
println!("{} {} changed", "[watch]".cyan(), relative_path.display());
|
|
399
|
+
pages_to_recompile.extend(dependent_pages);
|
|
400
|
+
}
|
|
401
|
+
} else if let Some(ext) = path.extension() {
|
|
402
|
+
if ext == "html" {
|
|
403
|
+
// Page changed - recompile just this page
|
|
404
|
+
println!("{} {} changed", "[watch]".cyan(), relative_path.display());
|
|
405
|
+
pages_to_recompile.insert(path.clone());
|
|
406
|
+
|
|
407
|
+
// Rebuild dependencies for this page
|
|
408
|
+
if let Ok(html) = std::fs::read_to_string(path) {
|
|
409
|
+
let components_path = config.source.join(&config.components);
|
|
410
|
+
let deps = extract_component_dependencies(&html, &components_path);
|
|
411
|
+
|
|
412
|
+
// Clear old dependencies for this page
|
|
413
|
+
if let Some(old_deps) = graph.page_to_components.get(path) {
|
|
414
|
+
for dep in old_deps {
|
|
415
|
+
if let Some(pages) = graph.component_to_pages.get_mut(dep) {
|
|
416
|
+
pages.remove(path);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Add new dependencies
|
|
422
|
+
graph.page_to_components.insert(path.clone(), HashSet::new());
|
|
423
|
+
for dep in deps {
|
|
424
|
+
let dep_absolute = config.source.join(&dep);
|
|
425
|
+
let dep_canonical = dep_absolute.canonicalize().unwrap_or(dep_absolute);
|
|
426
|
+
graph.add_dependency(path.clone(), dep_canonical, false);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Separate HTML files (from dependency graph) and CSS/JS assets (from changed_paths)
|
|
434
|
+
let html_files: Vec<PathBuf> = pages_to_recompile.into_iter().collect();
|
|
435
|
+
|
|
436
|
+
let mut asset_files: Vec<PathBuf> = Vec::new();
|
|
437
|
+
for path in &changed_paths {
|
|
438
|
+
// Skip files in output directory
|
|
439
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
440
|
+
if canonical_path.starts_with(&canonical_output) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Skip blacklisted files/directories
|
|
445
|
+
if is_path_blacklisted(path, &config.source) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
450
|
+
if ext == "css" || ext == "js" {
|
|
451
|
+
asset_files.push(path.clone());
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if html_files.is_empty() && asset_files.is_empty() {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let start = std::time::Instant::now();
|
|
461
|
+
|
|
462
|
+
// Use persistent compiler and parser (cache is preserved)
|
|
463
|
+
let mut total_stats = super::CompileStats {
|
|
464
|
+
files_compiled: 0,
|
|
465
|
+
files_copied: 0,
|
|
466
|
+
internal_components_unique: 0,
|
|
467
|
+
external_components_unique: 0,
|
|
468
|
+
internal_components_total: 0,
|
|
469
|
+
external_components_total: 0,
|
|
470
|
+
compile_time_ms: 0.0,
|
|
471
|
+
copy_time_ms: 0.0,
|
|
472
|
+
components_time_ms: 0.0,
|
|
473
|
+
validation_time_ms: None,
|
|
474
|
+
node_modules_time_ms: None,
|
|
475
|
+
package_manager: None,
|
|
476
|
+
node_modules_copied_as_is: false,
|
|
477
|
+
components_as_is: config.components_as_is,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
let mut manifest_stats_result = None;
|
|
481
|
+
let mut had_errors = false;
|
|
482
|
+
|
|
483
|
+
// Compile HTML files
|
|
484
|
+
if !html_files.is_empty() {
|
|
485
|
+
match watch_compiler.compile_specific_html_files(&html_files, &parser) {
|
|
486
|
+
Ok(stats) => {
|
|
487
|
+
total_stats.files_compiled = stats.files_compiled;
|
|
488
|
+
total_stats.internal_components_total = stats.internal_components_total;
|
|
489
|
+
total_stats.external_components_total = stats.external_components_total;
|
|
490
|
+
total_stats.internal_components_unique = stats.internal_components_unique;
|
|
491
|
+
total_stats.external_components_unique = stats.external_components_unique;
|
|
492
|
+
total_stats.compile_time_ms = stats.compile_time_ms;
|
|
493
|
+
|
|
494
|
+
// Generate manifests for affected HTML files
|
|
495
|
+
if !config.runtime_as_is {
|
|
496
|
+
match watch_compiler.generate_manifests_for_files(&html_files) {
|
|
497
|
+
Ok(manifest_stats) => {
|
|
498
|
+
manifest_stats_result = Some(manifest_stats);
|
|
499
|
+
}
|
|
500
|
+
Err(e) => {
|
|
501
|
+
eprintln!("{}: Manifest generation failed: {}", "Warning".yellow(), e);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
Err(e) => {
|
|
507
|
+
eprintln!("{}: {}", "Error".red(), e);
|
|
508
|
+
eprintln!("Fix the errors and save to retry.");
|
|
509
|
+
println!();
|
|
510
|
+
had_errors = true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Copy asset files
|
|
516
|
+
if !asset_files.is_empty() && !had_errors {
|
|
517
|
+
match watch_compiler.copy_specific_asset_files(&asset_files) {
|
|
518
|
+
Ok(stats) => {
|
|
519
|
+
total_stats.files_copied = stats.files_copied;
|
|
520
|
+
total_stats.copy_time_ms = stats.copy_time_ms;
|
|
521
|
+
}
|
|
522
|
+
Err(e) => {
|
|
523
|
+
eprintln!("{}: {}", "Error".red(), e);
|
|
524
|
+
eprintln!("Fix the errors and save to retry.");
|
|
525
|
+
println!();
|
|
526
|
+
had_errors = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if !had_errors {
|
|
532
|
+
// Show what was updated
|
|
533
|
+
if total_stats.files_compiled > 0 {
|
|
534
|
+
println!("{} Compiled {} HTML file{}",
|
|
535
|
+
"✓".green(),
|
|
536
|
+
total_stats.files_compiled,
|
|
537
|
+
if total_stats.files_compiled == 1 { "" } else { "s" }
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if let Some(manifest_stats) = manifest_stats_result {
|
|
542
|
+
if manifest_stats.pages_processed > 0 {
|
|
543
|
+
println!("{} Generated {} manifest{}",
|
|
544
|
+
"✓".green(),
|
|
545
|
+
manifest_stats.pages_processed,
|
|
546
|
+
if manifest_stats.pages_processed == 1 { "" } else { "s" }
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if total_stats.files_copied > 0 {
|
|
552
|
+
println!("{} Copied {} asset file{}",
|
|
553
|
+
"✓".green(),
|
|
554
|
+
total_stats.files_copied,
|
|
555
|
+
if total_stats.files_copied == 1 { "" } else { "s" }
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let elapsed = start.elapsed().as_millis();
|
|
560
|
+
println!("{} Updated in {}ms", "✓".green(), elapsed);
|
|
561
|
+
}
|
|
562
|
+
println!();
|
|
563
|
+
}
|
|
564
|
+
Err(errors) => {
|
|
565
|
+
for error in errors {
|
|
566
|
+
eprintln!("{}: {:?}", "Watch error".red(), error);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
Err(e) => {
|
|
572
|
+
eprintln!("{}: {}", "Channel error".red(), e);
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
Ok(())
|
|
579
|
+
}
|