@ape-egg/vibe 2.1.22 → 2.3.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 +37 -0
- package/README.md +98 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +1 -1
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +367 -9
- package/compiler/src/compiler/mod.rs +1 -0
- package/compiler/src/compiler/spa.rs +477 -0
- package/compiler/src/compiler/watcher.rs +149 -18
- package/compiler/src/config.rs +41 -1
- package/compiler/src/main.rs +11 -0
- package/index.js +16 -2
- package/llms.txt +29 -0
- package/package.json +2 -1
- package/runtime/component.js +53 -4
- package/runtime/hydrate.js +30 -3
- package/runtime/index.js +15 -0
- package/runtime/parse.js +12 -6
- package/spa.js +143 -0
|
@@ -281,6 +281,8 @@ pub enum CompileError {
|
|
|
281
281
|
component_name: String,
|
|
282
282
|
file_path: String,
|
|
283
283
|
},
|
|
284
|
+
#[error("SPA mode: {0}")]
|
|
285
|
+
SpaError(String),
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
pub struct CompileStats {
|
|
@@ -695,6 +697,13 @@ impl Compiler {
|
|
|
695
697
|
// Process source directory for assets (CSS, JS, images, etc.)
|
|
696
698
|
self.process_directory_assets_only(&canonical_source, "", &canonical_output, &canonical_source, &mut stats)?;
|
|
697
699
|
|
|
700
|
+
// SPA mode: pages tree → fragments + route table + composed shell.
|
|
701
|
+
// Runs after both passes so the shell wins over any root index.html
|
|
702
|
+
// and fragments land beside the mirrored components.
|
|
703
|
+
if self.config.spa {
|
|
704
|
+
stats.files_compiled += self.compile_spa(&parser)?;
|
|
705
|
+
}
|
|
706
|
+
|
|
698
707
|
let process_time = compile_start.elapsed();
|
|
699
708
|
|
|
700
709
|
// Print verbose output after processing (components first, then HTML, then copied)
|
|
@@ -912,6 +921,7 @@ impl Compiler {
|
|
|
912
921
|
let iterations_as_is = self.config.iterations_as_is;
|
|
913
922
|
let components_as_is = self.config.components_as_is;
|
|
914
923
|
let manifest_root = self.config.root.clone();
|
|
924
|
+
let spa = self.config.spa;
|
|
915
925
|
// Resolve constant global-state keys once, shared read-only across pages.
|
|
916
926
|
let global_constants = self.compute_global_constants();
|
|
917
927
|
|
|
@@ -956,6 +966,7 @@ impl Compiler {
|
|
|
956
966
|
&source_root,
|
|
957
967
|
manifest_root.as_deref(),
|
|
958
968
|
&global_constants,
|
|
969
|
+
!(spa && relative_path == "index.html"),
|
|
959
970
|
) {
|
|
960
971
|
Ok(()) => true,
|
|
961
972
|
Err(e) => {
|
|
@@ -992,6 +1003,7 @@ impl Compiler {
|
|
|
992
1003
|
source_root: &Path,
|
|
993
1004
|
manifest_root: Option<&str>,
|
|
994
1005
|
global_constants: &Map<String, Value>,
|
|
1006
|
+
stamp: bool,
|
|
995
1007
|
) -> Result<(), String> {
|
|
996
1008
|
use crate::compiler::manifest_builder::ManifestBuilder;
|
|
997
1009
|
use crate::compiler::component_tagger::ComponentTagger;
|
|
@@ -1047,13 +1059,22 @@ impl Compiler {
|
|
|
1047
1059
|
// - Iterations are pre-rendered with initial array data
|
|
1048
1060
|
// The manifest (written above) preserves BOTH branches so the runtime can
|
|
1049
1061
|
// restore them and switch between branches reactively.
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1062
|
+
//
|
|
1063
|
+
// The SPA shell opts out (stamp: false): its state is location-dependent
|
|
1064
|
+
// — unstampable by design — and it is served at EVERY route path while
|
|
1065
|
+
// its manifest only resolves at /. Stamping would strip the raw
|
|
1066
|
+
// `@[page.src]` outlet binding and leave deep links with nothing to
|
|
1067
|
+
// hydrate from; unstamped, the runtime processes the raw binding at any
|
|
1068
|
+
// URL (vibe-fouc covers the flash).
|
|
1069
|
+
if stamp {
|
|
1070
|
+
let stamper = ValueStamper::with_constants(&state, components_as_is, global_constants)
|
|
1071
|
+
.map_err(|e| format!("Failed to create value stamper: {}", e))?;
|
|
1072
|
+
let stamped = stamper.stamp_html(html.to_string())
|
|
1073
|
+
.map_err(|e| format!("Failed to stamp HTML: {}", e))?;
|
|
1074
|
+
|
|
1075
|
+
atomic_write(html_path, &stamped)
|
|
1076
|
+
.map_err(|e| format!("Failed to write stamped HTML: {}", e))?;
|
|
1077
|
+
}
|
|
1057
1078
|
|
|
1058
1079
|
Ok(())
|
|
1059
1080
|
}
|
|
@@ -1133,6 +1154,7 @@ impl Compiler {
|
|
|
1133
1154
|
let iterations_as_is = self.config.iterations_as_is;
|
|
1134
1155
|
let components_as_is = self.config.components_as_is;
|
|
1135
1156
|
let manifest_root = self.config.root.clone();
|
|
1157
|
+
let spa = self.config.spa;
|
|
1136
1158
|
// Resolve which global state keys are constant once, then share across all
|
|
1137
1159
|
// pages (read-only; `&Map` is Sync so the parallel map can borrow it).
|
|
1138
1160
|
let global_constants = self.compute_global_constants();
|
|
@@ -1162,8 +1184,10 @@ impl Compiler {
|
|
|
1162
1184
|
}
|
|
1163
1185
|
};
|
|
1164
1186
|
|
|
1165
|
-
// Try to generate manifest for this file (skip on error)
|
|
1166
|
-
|
|
1187
|
+
// Try to generate manifest for this file (skip on error).
|
|
1188
|
+
// The SPA shell keeps its raw bindings (manifest yes, stamp no).
|
|
1189
|
+
let stamp = !(spa && relative_path == "index.html");
|
|
1190
|
+
match Self::generate_file_manifest(html, html_path, &output_dir, relative_path, verbose, iterations_as_is, components_as_is, &source_root, manifest_root.as_deref(), &global_constants, stamp) {
|
|
1167
1191
|
Ok(()) => (true, None),
|
|
1168
1192
|
Err(e) => {
|
|
1169
1193
|
if verbose {
|
|
@@ -1229,6 +1253,12 @@ impl Compiler {
|
|
|
1229
1253
|
continue;
|
|
1230
1254
|
}
|
|
1231
1255
|
|
|
1256
|
+
// SPA mode: the pages tree becomes fragments + shell via
|
|
1257
|
+
// compile_spa — no mirrored MPA documents in the output.
|
|
1258
|
+
if self.config.spa && self.is_spa_pages_dir(&canonical_path) {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1232
1262
|
// Recurse into subdirectory
|
|
1233
1263
|
let new_relative = if relative_path.is_empty() {
|
|
1234
1264
|
file_name.to_string()
|
|
@@ -1413,6 +1443,214 @@ impl Compiler {
|
|
|
1413
1443
|
Ok((internal_count, external_count, component_srcs))
|
|
1414
1444
|
}
|
|
1415
1445
|
|
|
1446
|
+
/// True when a canonicalized directory is the configured pages tree.
|
|
1447
|
+
fn is_spa_pages_dir(&self, canonical_path: &Path) -> bool {
|
|
1448
|
+
self.config
|
|
1449
|
+
.source
|
|
1450
|
+
.join(&self.config.pages)
|
|
1451
|
+
.canonicalize()
|
|
1452
|
+
.map(|pages| pages == *canonical_path)
|
|
1453
|
+
.unwrap_or(false)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
fn collect_page_files(dir: &Path, skip: &[String], files: &mut Vec<PathBuf>) {
|
|
1457
|
+
let Ok(entries) = fs::read_dir(dir) else { return };
|
|
1458
|
+
for entry in entries.flatten() {
|
|
1459
|
+
let path = entry.path();
|
|
1460
|
+
let name = entry.file_name();
|
|
1461
|
+
let name = name.to_string_lossy();
|
|
1462
|
+
if should_skip_path(&path, &name, skip) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if path.is_dir() {
|
|
1466
|
+
Self::collect_page_files(&path, skip, files);
|
|
1467
|
+
} else if path.extension().and_then(|e| e.to_str()) == Some("html") {
|
|
1468
|
+
files.push(path);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/// SPA mode (fetched): transform every page into a fragment under
|
|
1474
|
+
/// output/components/vibe-spa/, then compose the shell at output root.
|
|
1475
|
+
/// Per-page stamped HTML, per-page manifests, and the pages/ output dir
|
|
1476
|
+
/// do not exist in SPA mode — fragments are runtime-parsed on mount; the
|
|
1477
|
+
/// shell flows through the normal manifest pipeline via compiled_html.
|
|
1478
|
+
/// Returns the number of files written (pages + shell) for stats.
|
|
1479
|
+
///
|
|
1480
|
+
/// Watch mode re-runs this whole pass on any pages-tree change — the
|
|
1481
|
+
/// shell depends on every page's head/title/body attrs and the pass is
|
|
1482
|
+
/// a few milliseconds, so per-page incrementality would buy nothing.
|
|
1483
|
+
pub(crate) fn compile_spa(&mut self, parser: &HtmlParser) -> Result<usize, CompileError> {
|
|
1484
|
+
use crate::compiler::spa;
|
|
1485
|
+
|
|
1486
|
+
let pages_dir = self.config.source.join(&self.config.pages);
|
|
1487
|
+
if !pages_dir.exists() {
|
|
1488
|
+
return Err(CompileError::SpaError(format!(
|
|
1489
|
+
"needs a pages directory at {}",
|
|
1490
|
+
pages_dir.display()
|
|
1491
|
+
)));
|
|
1492
|
+
}
|
|
1493
|
+
let reserved = self.config.components_path().join("vibe-spa");
|
|
1494
|
+
if reserved.exists() {
|
|
1495
|
+
return Err(CompileError::SpaError(format!(
|
|
1496
|
+
"{} is reserved for compiled page fragments — move or rename it",
|
|
1497
|
+
reserved.display()
|
|
1498
|
+
)));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
let mut page_files = Vec::new();
|
|
1502
|
+
Self::collect_page_files(&pages_dir, &self.config.skip_files, &mut page_files);
|
|
1503
|
+
page_files.sort();
|
|
1504
|
+
if page_files.is_empty() {
|
|
1505
|
+
return Err(CompileError::SpaError(format!(
|
|
1506
|
+
"found no pages under {}",
|
|
1507
|
+
pages_dir.display()
|
|
1508
|
+
)));
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
let mut pages = Vec::new();
|
|
1512
|
+
for path in &page_files {
|
|
1513
|
+
let rel = path
|
|
1514
|
+
.strip_prefix(&pages_dir)
|
|
1515
|
+
.unwrap()
|
|
1516
|
+
.to_string_lossy()
|
|
1517
|
+
.replace('\\', "/");
|
|
1518
|
+
let content = fs::read_to_string(path).map_err(|e| CompileError::ReadError {
|
|
1519
|
+
path: path.display().to_string(),
|
|
1520
|
+
source: e,
|
|
1521
|
+
})?;
|
|
1522
|
+
let processed = parser.process_html_with_cache(
|
|
1523
|
+
&content,
|
|
1524
|
+
self.config.elements_as_is,
|
|
1525
|
+
&self.config.reserved_elements,
|
|
1526
|
+
self.config.components_as_is,
|
|
1527
|
+
&self.config.components,
|
|
1528
|
+
&self.component_cache,
|
|
1529
|
+
);
|
|
1530
|
+
// Build-inlined children get the compiled-document treatment:
|
|
1531
|
+
// tagged wrapper ids + this.→_cN rewrites, matching their
|
|
1532
|
+
// vibe-module scripts (the runtime's mounted-subtree pass claims
|
|
1533
|
+
// ids via closest wrapper). Same protect→tag order as the
|
|
1534
|
+
// manifest pipeline; the extracted state is a page concern and
|
|
1535
|
+
// fragments have none to stamp.
|
|
1536
|
+
let protected = crate::compiler::name_binding_protect::protect(&processed);
|
|
1537
|
+
let tagged =
|
|
1538
|
+
crate::compiler::component_tagger::ComponentTagger::tag_components(
|
|
1539
|
+
&protected,
|
|
1540
|
+
&self.config.source,
|
|
1541
|
+
)
|
|
1542
|
+
.map_err(CompileError::SpaError)?;
|
|
1543
|
+
let page = spa::dissect_page(&rel, &tagged.html).map_err(CompileError::SpaError)?;
|
|
1544
|
+
|
|
1545
|
+
if let Some(api) = page.hygiene_offender {
|
|
1546
|
+
eprintln!(
|
|
1547
|
+
"{}: pages/{} starts side effects ({}) without an $.on('unmount') teardown — under SPA these outlive navigation",
|
|
1548
|
+
"Warning".yellow(),
|
|
1549
|
+
rel,
|
|
1550
|
+
api
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
let fragment_path = self
|
|
1555
|
+
.config
|
|
1556
|
+
.output
|
|
1557
|
+
.join("components")
|
|
1558
|
+
.join("vibe-spa")
|
|
1559
|
+
.join(&rel);
|
|
1560
|
+
if let Some(parent) = fragment_path.parent() {
|
|
1561
|
+
fs::create_dir_all(parent).map_err(|_| {
|
|
1562
|
+
CompileError::CreateDirError(parent.display().to_string())
|
|
1563
|
+
})?;
|
|
1564
|
+
}
|
|
1565
|
+
atomic_write(&fragment_path, &page.fragment).map_err(|e| CompileError::WriteError {
|
|
1566
|
+
path: fragment_path.display().to_string(),
|
|
1567
|
+
source: e,
|
|
1568
|
+
})?;
|
|
1569
|
+
|
|
1570
|
+
pages.push(page);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// The fragments dir mirrors the pages tree: prune fragments whose
|
|
1574
|
+
// page no longer exists (watch-mode page removals, no-clean runs).
|
|
1575
|
+
let fragments_root = self.config.output.join("components").join("vibe-spa");
|
|
1576
|
+
let live: HashSet<String> = pages.iter().map(|p| p.rel_path.clone()).collect();
|
|
1577
|
+
let mut existing = Vec::new();
|
|
1578
|
+
Self::collect_page_files(&fragments_root, &self.config.skip_files, &mut existing);
|
|
1579
|
+
for stale in existing {
|
|
1580
|
+
let rel = stale
|
|
1581
|
+
.strip_prefix(&fragments_root)
|
|
1582
|
+
.unwrap()
|
|
1583
|
+
.to_string_lossy()
|
|
1584
|
+
.replace('\\', "/");
|
|
1585
|
+
if !live.contains(&rel) {
|
|
1586
|
+
let _ = fs::remove_file(&stale);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
let order = spa::route_order(&pages);
|
|
1591
|
+
let shell = spa::compose_shell(&pages, &order);
|
|
1592
|
+
if self.verbose {
|
|
1593
|
+
for note in &shell.notes {
|
|
1594
|
+
println!(" spa: {}", note);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if self.config.source.join("index.html").exists() {
|
|
1599
|
+
eprintln!(
|
|
1600
|
+
"{}: SPA shell overwrites the compiled root index.html (source has its own index.html outside the pages tree)",
|
|
1601
|
+
"Warning".yellow()
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
let shell_html = if self.config.minify {
|
|
1606
|
+
minify_html(&shell.html)
|
|
1607
|
+
} else {
|
|
1608
|
+
shell.html
|
|
1609
|
+
};
|
|
1610
|
+
let shell_path = self.config.output.join("index.html");
|
|
1611
|
+
atomic_write(&shell_path, &shell_html).map_err(|e| CompileError::WriteError {
|
|
1612
|
+
path: shell_path.display().to_string(),
|
|
1613
|
+
source: e,
|
|
1614
|
+
})?;
|
|
1615
|
+
// The shell rides the normal manifest pipeline like any page.
|
|
1616
|
+
self.compiled_html.insert(shell_path, shell_html);
|
|
1617
|
+
|
|
1618
|
+
Ok(pages.len() + 1)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/// Regenerate the shell's manifest after a watch-mode recompose (the full
|
|
1622
|
+
/// build gets it via generate_manifests; incremental recompiles target
|
|
1623
|
+
/// only changed files, and the shell has no source counterpart to list).
|
|
1624
|
+
/// Manifest yes, stamp no — same rule as the full pipeline.
|
|
1625
|
+
pub(crate) fn generate_shell_manifest(&self) -> Result<(), CompileError> {
|
|
1626
|
+
let shell_path = self.config.output.join("index.html");
|
|
1627
|
+
let disk_html;
|
|
1628
|
+
let html: &str = match self.compiled_html.get(&shell_path) {
|
|
1629
|
+
Some(html) => html,
|
|
1630
|
+
None => {
|
|
1631
|
+
disk_html = fs::read_to_string(&shell_path).map_err(|e| CompileError::ReadError {
|
|
1632
|
+
path: shell_path.display().to_string(),
|
|
1633
|
+
source: e,
|
|
1634
|
+
})?;
|
|
1635
|
+
&disk_html
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
Self::generate_file_manifest(
|
|
1639
|
+
html,
|
|
1640
|
+
&shell_path,
|
|
1641
|
+
&self.config.output,
|
|
1642
|
+
"index.html",
|
|
1643
|
+
self.verbose,
|
|
1644
|
+
self.config.iterations_as_is,
|
|
1645
|
+
self.config.components_as_is,
|
|
1646
|
+
&self.config.source,
|
|
1647
|
+
self.config.root.as_deref(),
|
|
1648
|
+
&self.compute_global_constants(),
|
|
1649
|
+
false,
|
|
1650
|
+
)
|
|
1651
|
+
.map_err(CompileError::SpaError)
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1416
1654
|
fn extract_components(&self, content: &str) -> (usize, usize, Vec<String>) {
|
|
1417
1655
|
let mut internal = 0;
|
|
1418
1656
|
let mut external = 0;
|
|
@@ -2206,6 +2444,14 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), CompileError> {
|
|
|
2206
2444
|
let dest_path = dest.join(&file_name);
|
|
2207
2445
|
|
|
2208
2446
|
if path.is_dir() {
|
|
2447
|
+
// Cache Directory Tagging spec: a directory carrying a signed
|
|
2448
|
+
// CACHEDIR.TAG (cargo target/, many build caches) declares itself
|
|
2449
|
+
// regenerable and skippable for copy tools. A symlinked local
|
|
2450
|
+
// package would otherwise drag gigabytes of build artifacts into
|
|
2451
|
+
// the as-is node_modules copy.
|
|
2452
|
+
if is_cachedir_tagged(&path) {
|
|
2453
|
+
continue;
|
|
2454
|
+
}
|
|
2209
2455
|
copy_dir_recursive(&path, &dest_path)?;
|
|
2210
2456
|
} else {
|
|
2211
2457
|
fs::copy(&path, &dest_path).map_err(|e| CompileError::WriteError {
|
|
@@ -2218,6 +2464,12 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), CompileError> {
|
|
|
2218
2464
|
Ok(())
|
|
2219
2465
|
}
|
|
2220
2466
|
|
|
2467
|
+
fn is_cachedir_tagged(dir: &Path) -> bool {
|
|
2468
|
+
fs::read(dir.join("CACHEDIR.TAG"))
|
|
2469
|
+
.map(|bytes| bytes.starts_with(b"Signature: 8a477f597d28d172789f06886806bc55"))
|
|
2470
|
+
.unwrap_or(false)
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2221
2473
|
/// Basic HTML minification
|
|
2222
2474
|
fn minify_html(html: &str) -> String {
|
|
2223
2475
|
let mut result = String::with_capacity(html.len());
|
|
@@ -2369,6 +2621,111 @@ mod tests {
|
|
|
2369
2621
|
// A minimal on-disk project: source with one page carrying a distinctive
|
|
2370
2622
|
// binding, empty components dir, output dir sibling. Returns (config, page
|
|
2371
2623
|
// source path, compiled page output path, manifest path).
|
|
2624
|
+
// nodeModulesAsIs copies must skip cache-tagged directories (the Cache
|
|
2625
|
+
// Directory Tagging spec: cargo target/, many build caches). A symlinked
|
|
2626
|
+
// local package dragging its cargo target/ along turned a 15MB copy into
|
|
2627
|
+
// gigabytes.
|
|
2628
|
+
#[test]
|
|
2629
|
+
fn as_is_copy_skips_cachedir_tagged_directories() {
|
|
2630
|
+
let dir = std::env::temp_dir().join("vibe-cachedir-copy-test");
|
|
2631
|
+
let _ = fs::remove_dir_all(&dir);
|
|
2632
|
+
let src = dir.join("node_modules/pkg");
|
|
2633
|
+
fs::create_dir_all(src.join("runtime")).unwrap();
|
|
2634
|
+
fs::create_dir_all(src.join("build/target/release")).unwrap();
|
|
2635
|
+
fs::write(src.join("index.js"), "export default 1;").unwrap();
|
|
2636
|
+
fs::write(src.join("runtime/state.js"), "export const s = 1;").unwrap();
|
|
2637
|
+
fs::write(
|
|
2638
|
+
src.join("build/target/CACHEDIR.TAG"),
|
|
2639
|
+
"Signature: 8a477f597d28d172789f06886806bc55\n",
|
|
2640
|
+
)
|
|
2641
|
+
.unwrap();
|
|
2642
|
+
fs::write(src.join("build/target/release/artifact"), "big").unwrap();
|
|
2643
|
+
|
|
2644
|
+
let dest = dir.join("out/node_modules");
|
|
2645
|
+
copy_dir_recursive(&dir.join("node_modules"), &dest).unwrap();
|
|
2646
|
+
|
|
2647
|
+
assert!(dest.join("pkg/index.js").exists());
|
|
2648
|
+
assert!(dest.join("pkg/runtime/state.js").exists());
|
|
2649
|
+
assert!(dest.join("pkg/build").exists());
|
|
2650
|
+
assert!(!dest.join("pkg/build/target").exists(), "cache-tagged dir was copied");
|
|
2651
|
+
|
|
2652
|
+
let _ = fs::remove_dir_all(&dir);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// Watch mode re-runs compile_spa on any pages-tree change: an edited page
|
|
2656
|
+
// re-transforms and the shell recomposes (title/head/route table), a
|
|
2657
|
+
// removed page's fragment is pruned from the output mirror and its route
|
|
2658
|
+
// leaves the table. Full-rerun semantics keep add/edit/remove one path.
|
|
2659
|
+
#[test]
|
|
2660
|
+
fn spa_recompile_syncs_fragments_shell_and_routes() {
|
|
2661
|
+
let dir = std::env::temp_dir().join("vibe-spa-watch-test");
|
|
2662
|
+
let _ = fs::remove_dir_all(&dir);
|
|
2663
|
+
let source = dir.join("src");
|
|
2664
|
+
fs::create_dir_all(source.join("pages")).unwrap();
|
|
2665
|
+
fs::create_dir_all(source.join("components")).unwrap();
|
|
2666
|
+
let page = |title: &str, body: &str| {
|
|
2667
|
+
format!(
|
|
2668
|
+
"<!DOCTYPE html>\n<html><head><title>{}</title></head>\n<body vibe-fouc>{}</body></html>\n",
|
|
2669
|
+
title, body
|
|
2670
|
+
)
|
|
2671
|
+
};
|
|
2672
|
+
fs::write(source.join("pages/index.html"), page("Home", "<h1>home</h1>")).unwrap();
|
|
2673
|
+
fs::write(source.join("pages/about.html"), page("About", "<h1>about</h1>")).unwrap();
|
|
2674
|
+
|
|
2675
|
+
let config = Config {
|
|
2676
|
+
source: source.clone(),
|
|
2677
|
+
output: dir.join("out"),
|
|
2678
|
+
_source_str: String::new(),
|
|
2679
|
+
_output_str: String::new(),
|
|
2680
|
+
components: "components".to_string(),
|
|
2681
|
+
pages: "pages".to_string(),
|
|
2682
|
+
_assets: String::new(),
|
|
2683
|
+
root: None,
|
|
2684
|
+
minify: false,
|
|
2685
|
+
elements_as_is: true,
|
|
2686
|
+
source_maps: false,
|
|
2687
|
+
reserved_elements: Vec::new(),
|
|
2688
|
+
skip_files: Vec::new(),
|
|
2689
|
+
node_modules_as_is: false,
|
|
2690
|
+
components_as_is: true,
|
|
2691
|
+
runtime_as_is: false,
|
|
2692
|
+
iterations_as_is: false,
|
|
2693
|
+
no_clean: false,
|
|
2694
|
+
fouc_as_is: false,
|
|
2695
|
+
spa: true,
|
|
2696
|
+
working_dir: dir.clone(),
|
|
2697
|
+
};
|
|
2698
|
+
|
|
2699
|
+
let mut compiler = Compiler::new(config.clone(), false);
|
|
2700
|
+
let mut parser = HtmlParser::new(config.components_path());
|
|
2701
|
+
parser.load_elements().unwrap();
|
|
2702
|
+
compiler.compile().unwrap();
|
|
2703
|
+
|
|
2704
|
+
let shell_path = config.output.join("index.html");
|
|
2705
|
+
let fragment = config.output.join("components/vibe-spa/about.html");
|
|
2706
|
+
let shell = fs::read_to_string(&shell_path).unwrap();
|
|
2707
|
+
assert!(shell.contains("\"route\": \"/about\""));
|
|
2708
|
+
assert!(shell.contains("<title>Home</title>"));
|
|
2709
|
+
assert!(fragment.exists());
|
|
2710
|
+
|
|
2711
|
+
// Edited page: fragment re-transforms, shell recomposes with the new
|
|
2712
|
+
// harvested title.
|
|
2713
|
+
fs::write(source.join("pages/about.html"), page("Regenerated", "<h1>about v2</h1>")).unwrap();
|
|
2714
|
+
compiler.compile_spa(&parser).unwrap();
|
|
2715
|
+
let shell = fs::read_to_string(&shell_path).unwrap();
|
|
2716
|
+
assert!(shell.contains("\"title\": \"Regenerated\""));
|
|
2717
|
+
assert!(fs::read_to_string(&fragment).unwrap().contains("about v2"));
|
|
2718
|
+
|
|
2719
|
+
// Removed page: route leaves the table, orphan fragment is pruned.
|
|
2720
|
+
fs::remove_file(source.join("pages/about.html")).unwrap();
|
|
2721
|
+
compiler.compile_spa(&parser).unwrap();
|
|
2722
|
+
let shell = fs::read_to_string(&shell_path).unwrap();
|
|
2723
|
+
assert!(!shell.contains("\"route\": \"/about\""));
|
|
2724
|
+
assert!(!fragment.exists());
|
|
2725
|
+
|
|
2726
|
+
let _ = fs::remove_dir_all(&dir);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2372
2729
|
fn manifest_test_project(name: &str) -> (Config, PathBuf, PathBuf, PathBuf) {
|
|
2373
2730
|
let dir = std::env::temp_dir().join(format!("vibe_{}_test", name));
|
|
2374
2731
|
let _ = fs::remove_dir_all(&dir);
|
|
@@ -2402,6 +2759,7 @@ mod tests {
|
|
|
2402
2759
|
iterations_as_is: false,
|
|
2403
2760
|
no_clean: false,
|
|
2404
2761
|
fouc_as_is: false,
|
|
2762
|
+
spa: false,
|
|
2405
2763
|
working_dir: dir.clone(),
|
|
2406
2764
|
};
|
|
2407
2765
|
|