@ape-egg/vibe-compiler 4.3.25 → 4.3.27
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/native/vibe-compiler-linux-x64 +0 -0
- package/package.json +1 -1
- package/src/Cargo.lock +2032 -0
- package/src/Cargo.toml +1 -1
- package/src/compiler/compile.rs +161 -165
- package/src/compiler/value_stamper.rs +115 -71
- package/src/compiler/watcher.rs +6 -3
- package/src/config.rs +10 -0
- package/src/main.rs +7 -2
- package/src/parser/html.rs +1 -1
package/src/Cargo.toml
CHANGED
package/src/compiler/compile.rs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
use std::fs;
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
|
-
use std::
|
|
4
|
-
use std::time::Instant;
|
|
3
|
+
use std::time::{Duration, Instant, SystemTime};
|
|
5
4
|
use std::collections::{BTreeMap, HashSet, HashMap};
|
|
6
5
|
use thiserror::Error;
|
|
7
6
|
use colored::Colorize;
|
|
@@ -39,17 +38,39 @@ fn manifest_url_path(relative_path: &str, root: Option<&str>) -> String {
|
|
|
39
38
|
.join("/")
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
///
|
|
46
|
-
|
|
41
|
+
/// Overwrite the output file in place. A temp file + rename would keep a torn
|
|
42
|
+
/// file from ever being read, but the temp file itself vanishes mid-build, and
|
|
43
|
+
/// a dev server that lists the output and then stats each entry (wrangler dev)
|
|
44
|
+
/// dies on the one that is gone. A torn read is fixed by the next reload.
|
|
45
|
+
///
|
|
46
|
+
/// A file that shares its bytes with another path — a hard link into a package
|
|
47
|
+
/// manager's cache, as bun installs, or a symlink — is replaced through a
|
|
48
|
+
/// rename instead, since writing through it would rewrite that other path too.
|
|
49
|
+
pub(crate) fn write_in_place(path: &Path, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
|
|
50
|
+
if !shares_bytes(path) {
|
|
51
|
+
return fs::write(path, contents);
|
|
52
|
+
}
|
|
47
53
|
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("out");
|
|
48
54
|
let tmp = path.with_file_name(format!(".{}.{}.vibe-tmp", file_name, std::process::id()));
|
|
49
55
|
fs::write(&tmp, contents)?;
|
|
50
56
|
fs::rename(&tmp, path)
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
fn copy_in_place(src: &Path, dest: &Path) -> std::io::Result<()> {
|
|
60
|
+
write_in_place(dest, fs::read(src)?)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(unix)]
|
|
64
|
+
fn shares_bytes(path: &Path) -> bool {
|
|
65
|
+
use std::os::unix::fs::MetadataExt;
|
|
66
|
+
fs::symlink_metadata(path).is_ok_and(|meta| meta.file_type().is_symlink() || meta.nlink() > 1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(not(unix))]
|
|
70
|
+
fn shares_bytes(path: &Path) -> bool {
|
|
71
|
+
fs::symlink_metadata(path).is_ok_and(|meta| meta.file_type().is_symlink())
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
// =============================================================================
|
|
54
75
|
// MIRROR_MODE: Copy asset files from source to output as-is, preserving
|
|
55
76
|
// directory structure. HTML files are compiled separately.
|
|
@@ -326,10 +347,8 @@ pub enum CompileError {
|
|
|
326
347
|
},
|
|
327
348
|
#[error("Parse error: {0}")]
|
|
328
349
|
ParseError(#[from] crate::parser::html::ParseError),
|
|
329
|
-
#[error("Failed to
|
|
330
|
-
|
|
331
|
-
#[error("Package manager not found")]
|
|
332
|
-
PackageManagerNotFound,
|
|
350
|
+
#[error("Failed to copy packages: {0}")]
|
|
351
|
+
ModulesError(String),
|
|
333
352
|
#[error("Component name '{component_name}' in '{file_path}' conflicts with reserved element. Component filenames cannot match standard HTML elements (case-insensitive). Use PascalCase (e.g., 'Nav.html' instead of 'nav.html') to avoid conflicts.")]
|
|
334
353
|
ReservedComponentName {
|
|
335
354
|
component_name: String,
|
|
@@ -351,7 +370,6 @@ pub struct CompileStats {
|
|
|
351
370
|
pub components_time_ms: f64,
|
|
352
371
|
pub validation_time_ms: Option<f64>,
|
|
353
372
|
pub node_modules_time_ms: Option<f64>,
|
|
354
|
-
pub package_manager: Option<String>,
|
|
355
373
|
pub node_modules_copied_as_is: bool,
|
|
356
374
|
pub components_as_is: bool,
|
|
357
375
|
}
|
|
@@ -630,6 +648,8 @@ pub struct Compiler {
|
|
|
630
648
|
/// disk read-back — so a concurrent writer rewriting the output directory
|
|
631
649
|
/// (e.g. a second compiler) can't feed a torn read into a manifest.
|
|
632
650
|
compiled_html: HashMap<PathBuf, String>,
|
|
651
|
+
/// When the last full compile began; output older than it is stale.
|
|
652
|
+
build_start: SystemTime,
|
|
633
653
|
}
|
|
634
654
|
|
|
635
655
|
impl Compiler {
|
|
@@ -642,6 +662,7 @@ impl Compiler {
|
|
|
642
662
|
unique_components: HashSet::new(),
|
|
643
663
|
component_cache: HashMap::new(),
|
|
644
664
|
compiled_html: HashMap::new(),
|
|
665
|
+
build_start: SystemTime::now(),
|
|
645
666
|
}
|
|
646
667
|
}
|
|
647
668
|
|
|
@@ -683,7 +704,6 @@ impl Compiler {
|
|
|
683
704
|
components_time_ms: 0.0,
|
|
684
705
|
validation_time_ms: None,
|
|
685
706
|
node_modules_time_ms: None,
|
|
686
|
-
package_manager: None,
|
|
687
707
|
node_modules_copied_as_is: false,
|
|
688
708
|
components_as_is: self.config.components_as_is,
|
|
689
709
|
};
|
|
@@ -727,18 +747,10 @@ impl Compiler {
|
|
|
727
747
|
println!(" HTML validation completed successfully");
|
|
728
748
|
}
|
|
729
749
|
|
|
730
|
-
//
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
fs::remove_dir_all(&self.config.output).map_err(|e| {
|
|
736
|
-
CompileError::WriteError {
|
|
737
|
-
path: self.config.output.display().to_string(),
|
|
738
|
-
source: e,
|
|
739
|
-
}
|
|
740
|
-
})?;
|
|
741
|
-
}
|
|
750
|
+
// Output is overwritten in place and stale files are removed only at
|
|
751
|
+
// the end (remove_stale_output), so a dev server reading the output
|
|
752
|
+
// never sees it emptied.
|
|
753
|
+
self.build_start = SystemTime::now();
|
|
742
754
|
|
|
743
755
|
// Create output directory
|
|
744
756
|
if !self.config.output.exists() {
|
|
@@ -814,31 +826,16 @@ impl Compiler {
|
|
|
814
826
|
}
|
|
815
827
|
}
|
|
816
828
|
|
|
817
|
-
//
|
|
829
|
+
// Packages after successful compilation
|
|
818
830
|
if self.config.working_dir.join("node_modules").exists() {
|
|
831
|
+
let nm_start = Instant::now();
|
|
819
832
|
if self.config.node_modules_as_is {
|
|
820
|
-
|
|
821
|
-
let nm_start = Instant::now();
|
|
822
|
-
self.copy_node_modules_as_is()?;
|
|
823
|
-
stats.node_modules_time_ms = Some(nm_start.elapsed().as_secs_f64() * 1000.0);
|
|
824
|
-
stats.node_modules_copied_as_is = true;
|
|
825
|
-
|
|
826
|
-
if self.verbose {
|
|
827
|
-
println!("\n Copied node_modules as-is");
|
|
828
|
-
}
|
|
833
|
+
copy_dir_recursive(&self.config.working_dir.join("node_modules"), &self.config.modules_path())?;
|
|
829
834
|
} else {
|
|
830
|
-
|
|
831
|
-
let pkg_manager = detect_package_manager(&self.config.working_dir).ok();
|
|
832
|
-
stats.package_manager = pkg_manager.clone();
|
|
833
|
-
|
|
834
|
-
let nm_start = Instant::now();
|
|
835
|
-
self.copy_production_node_modules()?;
|
|
836
|
-
stats.node_modules_time_ms = Some(nm_start.elapsed().as_secs_f64() * 1000.0);
|
|
837
|
-
|
|
838
|
-
if self.verbose {
|
|
839
|
-
println!(" Copied production dependencies");
|
|
840
|
-
}
|
|
835
|
+
self.copy_production_modules()?;
|
|
841
836
|
}
|
|
837
|
+
stats.node_modules_time_ms = Some(nm_start.elapsed().as_secs_f64() * 1000.0);
|
|
838
|
+
stats.node_modules_copied_as_is = self.config.node_modules_as_is;
|
|
842
839
|
}
|
|
843
840
|
|
|
844
841
|
Ok(stats)
|
|
@@ -862,7 +859,6 @@ impl Compiler {
|
|
|
862
859
|
components_time_ms: 0.0,
|
|
863
860
|
validation_time_ms: None,
|
|
864
861
|
node_modules_time_ms: None,
|
|
865
|
-
package_manager: None,
|
|
866
862
|
node_modules_copied_as_is: false,
|
|
867
863
|
components_as_is: self.config.components_as_is,
|
|
868
864
|
};
|
|
@@ -945,7 +941,6 @@ impl Compiler {
|
|
|
945
941
|
components_time_ms: 0.0,
|
|
946
942
|
validation_time_ms: None,
|
|
947
943
|
node_modules_time_ms: None,
|
|
948
|
-
package_manager: None,
|
|
949
944
|
node_modules_copied_as_is: false,
|
|
950
945
|
components_as_is: self.config.components_as_is,
|
|
951
946
|
};
|
|
@@ -1118,7 +1113,7 @@ impl Compiler {
|
|
|
1118
1113
|
manifest_json
|
|
1119
1114
|
);
|
|
1120
1115
|
|
|
1121
|
-
|
|
1116
|
+
write_in_place(&manifest_path, &manifest_js)
|
|
1122
1117
|
.map_err(|e| format!("Failed to write manifest: {}", e))?;
|
|
1123
1118
|
|
|
1124
1119
|
// Stamp the compiled HTML with initial state values for FOUC prevention:
|
|
@@ -1143,7 +1138,7 @@ impl Compiler {
|
|
|
1143
1138
|
let stamped = stamper.stamp_html(html.to_string())
|
|
1144
1139
|
.map_err(|e| format!("Failed to stamp HTML: {}", e))?;
|
|
1145
1140
|
|
|
1146
|
-
|
|
1141
|
+
write_in_place(html_path, &stamped)
|
|
1147
1142
|
.map_err(|e| format!("Failed to write stamped HTML: {}", e))?;
|
|
1148
1143
|
}
|
|
1149
1144
|
|
|
@@ -1512,7 +1507,7 @@ impl Compiler {
|
|
|
1512
1507
|
|
|
1513
1508
|
// Write to output
|
|
1514
1509
|
let output_path = self.get_output_path(path, relative_path)?;
|
|
1515
|
-
|
|
1510
|
+
write_in_place(&output_path, &output).map_err(|e| CompileError::WriteError {
|
|
1516
1511
|
path: output_path.display().to_string(),
|
|
1517
1512
|
source: e,
|
|
1518
1513
|
})?;
|
|
@@ -1693,7 +1688,7 @@ impl Compiler {
|
|
|
1693
1688
|
CompileError::CreateDirError(parent.display().to_string())
|
|
1694
1689
|
})?;
|
|
1695
1690
|
}
|
|
1696
|
-
|
|
1691
|
+
write_in_place(&fragment_path, &page.fragment).map_err(|e| CompileError::WriteError {
|
|
1697
1692
|
path: fragment_path.display().to_string(),
|
|
1698
1693
|
source: e,
|
|
1699
1694
|
})?;
|
|
@@ -1798,7 +1793,7 @@ impl Compiler {
|
|
|
1798
1793
|
};
|
|
1799
1794
|
let shell_html = inject_vibe_css(shell_html, self.config.disable_vibe_css);
|
|
1800
1795
|
let shell_path = self.config.output.join("index.html");
|
|
1801
|
-
|
|
1796
|
+
write_in_place(&shell_path, &shell_html).map_err(|e| CompileError::WriteError {
|
|
1802
1797
|
path: shell_path.display().to_string(),
|
|
1803
1798
|
source: e,
|
|
1804
1799
|
})?;
|
|
@@ -1913,7 +1908,7 @@ impl Compiler {
|
|
|
1913
1908
|
fn copy_file(&self, path: &Path, relative_path: &str) -> Result<(), CompileError> {
|
|
1914
1909
|
let output_path = self.get_output_path(path, relative_path)?;
|
|
1915
1910
|
|
|
1916
|
-
|
|
1911
|
+
copy_in_place(path, &output_path).map_err(|e| CompileError::WriteError {
|
|
1917
1912
|
path: output_path.display().to_string(),
|
|
1918
1913
|
source: e,
|
|
1919
1914
|
})?;
|
|
@@ -1928,9 +1923,7 @@ impl Compiler {
|
|
|
1928
1923
|
/// components_as_is — are served from that mirror at request time. Watch
|
|
1929
1924
|
/// mode calls this for every changed component so the mirror tracks edits
|
|
1930
1925
|
/// and deletions even when no page inlines the component (zero graph
|
|
1931
|
-
/// dependents).
|
|
1932
|
-
/// runtime fetch mid-copy never sees a torn file. Returns how many files
|
|
1933
|
-
/// were copied.
|
|
1926
|
+
/// dependents). Returns how many files were copied.
|
|
1934
1927
|
pub fn mirror_component_files(&self, files: &[PathBuf]) -> Result<usize, CompileError> {
|
|
1935
1928
|
let mut copied = 0;
|
|
1936
1929
|
for path in files {
|
|
@@ -1947,7 +1940,7 @@ impl Compiler {
|
|
|
1947
1940
|
path: path.display().to_string(),
|
|
1948
1941
|
source: e,
|
|
1949
1942
|
})?;
|
|
1950
|
-
|
|
1943
|
+
write_in_place(&output_path, &contents).map_err(|e| CompileError::WriteError {
|
|
1951
1944
|
path: output_path.display().to_string(),
|
|
1952
1945
|
source: e,
|
|
1953
1946
|
})?;
|
|
@@ -2494,120 +2487,96 @@ impl Compiler {
|
|
|
2494
2487
|
Ok(())
|
|
2495
2488
|
}
|
|
2496
2489
|
|
|
2497
|
-
///
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
let
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
// 1. Copy package.json to output directory
|
|
2526
|
-
let package_json_src = self.config.working_dir.join("package.json");
|
|
2527
|
-
let package_json_dest = self.config.output.join("package.json");
|
|
2528
|
-
|
|
2529
|
-
fs::copy(&package_json_src, &package_json_dest).map_err(|e| CompileError::WriteError {
|
|
2530
|
-
path: package_json_dest.display().to_string(),
|
|
2531
|
-
source: e,
|
|
2532
|
-
})?;
|
|
2533
|
-
|
|
2534
|
-
// 2. Copy lockfile if it exists (needed for reproducible installs)
|
|
2535
|
-
let lockfile_name = match pkg_manager.as_str() {
|
|
2536
|
-
"bun" if self.config.working_dir.join("bun.lock").exists() => "bun.lock",
|
|
2537
|
-
"bun" => "bun.lockb",
|
|
2538
|
-
"pnpm" => "pnpm-lock.yaml",
|
|
2539
|
-
"yarn" => "yarn.lock",
|
|
2540
|
-
"npm" => "package-lock.json",
|
|
2541
|
-
_ => "",
|
|
2542
|
-
};
|
|
2543
|
-
|
|
2544
|
-
if !lockfile_name.is_empty() {
|
|
2545
|
-
let lockfile_src = self.config.working_dir.join(lockfile_name);
|
|
2546
|
-
let lockfile_dest = self.config.output.join(lockfile_name);
|
|
2547
|
-
if lockfile_src.exists() {
|
|
2548
|
-
fs::copy(&lockfile_src, &lockfile_dest).ok();
|
|
2490
|
+
/// Remove whatever the last full compile did not write, then the folders it
|
|
2491
|
+
/// left empty. Run once the build is done — manifests included — so a
|
|
2492
|
+
/// file disappears only when it is stale. File times come from the
|
|
2493
|
+
/// kernel's coarse clock, which trails SystemTime::now(), hence the margin.
|
|
2494
|
+
pub fn remove_stale_output(&self) -> Result<usize, CompileError> {
|
|
2495
|
+
if self.config.no_clean {
|
|
2496
|
+
return Ok(0);
|
|
2497
|
+
}
|
|
2498
|
+
remove_older_than(&self.config.output, self.build_start - Duration::from_secs(1))
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
/// Copy every production dependency, and theirs, from the project's own
|
|
2502
|
+
/// node_modules into the modules folder of the output. Written over what is
|
|
2503
|
+
/// there, never through a temp tree, so the output never loses a file.
|
|
2504
|
+
/// A `modules` folder other than node_modules holds each package under its
|
|
2505
|
+
/// bare name (`modules/vibe`, not `modules/@ape-egg/vibe`): some static
|
|
2506
|
+
/// hosts (Cloudflare) redirect any path holding `@`.
|
|
2507
|
+
fn copy_production_modules(&self) -> Result<(), CompileError> {
|
|
2508
|
+
let installed = self.config.working_dir.join("node_modules");
|
|
2509
|
+
let bare = self.config.modules != "node_modules";
|
|
2510
|
+
let mut pending = dependency_names(&self.config.working_dir.join("package.json"))?;
|
|
2511
|
+
let mut copied: HashMap<String, String> = HashMap::new();
|
|
2512
|
+
while let Some(name) = pending.pop() {
|
|
2513
|
+
let src = installed.join(&name);
|
|
2514
|
+
// A dependency missing at the top level is nested inside its dependent, and copied with it
|
|
2515
|
+
if copied.contains_key(&name) || !src.exists() {
|
|
2516
|
+
continue;
|
|
2549
2517
|
}
|
|
2518
|
+
let folder = if bare { name.rsplit('/').next().unwrap_or(&name).to_string() } else { name.clone() };
|
|
2519
|
+
if let Some((other, _)) = copied.iter().find(|(_, f)| **f == folder) {
|
|
2520
|
+
return Err(CompileError::ModulesError(format!(
|
|
2521
|
+
"\"{}\" and \"{}\" both land in {}/{} — bare package names must be unique in \"modules\"",
|
|
2522
|
+
other, name, self.config.modules, folder
|
|
2523
|
+
)));
|
|
2524
|
+
}
|
|
2525
|
+
copy_dir_recursive(&src, &self.config.modules_path().join(&folder))?;
|
|
2526
|
+
pending.extend(dependency_names(&src.join("package.json"))?);
|
|
2527
|
+
copied.insert(name, folder);
|
|
2550
2528
|
}
|
|
2551
|
-
|
|
2552
|
-
// 3. Install production dependencies directly in output directory
|
|
2553
|
-
run_command(&pkg_manager, &["install", "--production"], &self.config.output)?;
|
|
2554
|
-
|
|
2555
|
-
// 4. Cleanup: remove package.json and lockfile from output
|
|
2556
|
-
fs::remove_file(&package_json_dest).ok();
|
|
2557
|
-
if !lockfile_name.is_empty() {
|
|
2558
|
-
let lockfile_dest = self.config.output.join(lockfile_name);
|
|
2559
|
-
fs::remove_file(&lockfile_dest).ok();
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
if self.verbose {
|
|
2563
|
-
println!(" Installed production dependencies to output");
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
2529
|
Ok(())
|
|
2567
2530
|
}
|
|
2568
2531
|
}
|
|
2569
2532
|
|
|
2570
|
-
///
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2533
|
+
/// Remove every file under `dir` last written before `cutoff`, and every
|
|
2534
|
+
/// folder that leaves empty. Returns how many files went.
|
|
2535
|
+
fn remove_older_than(dir: &Path, cutoff: SystemTime) -> Result<usize, CompileError> {
|
|
2536
|
+
let read_error = |e| CompileError::ReadError { path: dir.display().to_string(), source: e };
|
|
2537
|
+
let write_error = |path: &Path, e| CompileError::WriteError { path: path.display().to_string(), source: e };
|
|
2538
|
+
let mut removed = 0;
|
|
2539
|
+
for entry in fs::read_dir(dir).map_err(read_error)? {
|
|
2540
|
+
let entry = entry.map_err(read_error)?;
|
|
2541
|
+
let path = entry.path();
|
|
2542
|
+
let meta = entry.metadata().map_err(read_error)?;
|
|
2543
|
+
if meta.is_dir() {
|
|
2544
|
+
removed += remove_older_than(&path, cutoff)?;
|
|
2545
|
+
if fs::read_dir(&path).map_err(read_error)?.next().is_none() {
|
|
2546
|
+
fs::remove_dir(&path).map_err(|e| write_error(&path, e))?;
|
|
2547
|
+
}
|
|
2548
|
+
} else if meta.modified().map_err(read_error)? < cutoff {
|
|
2549
|
+
fs::remove_file(&path).map_err(|e| write_error(&path, e))?;
|
|
2550
|
+
removed += 1;
|
|
2551
|
+
}
|
|
2583
2552
|
}
|
|
2584
|
-
|
|
2585
|
-
Err(CompileError::PackageManagerNotFound)
|
|
2553
|
+
Ok(removed)
|
|
2586
2554
|
}
|
|
2587
2555
|
|
|
2588
|
-
///
|
|
2589
|
-
fn
|
|
2590
|
-
let
|
|
2591
|
-
.
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
args.join(" "),
|
|
2602
|
-
stderr
|
|
2603
|
-
)));
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
Ok(())
|
|
2556
|
+
/// Names under `dependencies`, `optionalDependencies` and `peerDependencies`
|
|
2557
|
+
fn dependency_names(package_json: &Path) -> Result<Vec<String>, CompileError> {
|
|
2558
|
+
let content = fs::read_to_string(package_json).map_err(|e| CompileError::ReadError {
|
|
2559
|
+
path: package_json.display().to_string(),
|
|
2560
|
+
source: e,
|
|
2561
|
+
})?;
|
|
2562
|
+
let package: Value = serde_json::from_str(&content)
|
|
2563
|
+
.map_err(|e| CompileError::ModulesError(format!("{}: {}", package_json.display(), e)))?;
|
|
2564
|
+
Ok(["dependencies", "optionalDependencies", "peerDependencies"]
|
|
2565
|
+
.iter()
|
|
2566
|
+
.filter_map(|key| package.get(key).and_then(Value::as_object))
|
|
2567
|
+
.flat_map(|deps| deps.keys().cloned())
|
|
2568
|
+
.collect())
|
|
2607
2569
|
}
|
|
2608
2570
|
|
|
2609
2571
|
/// Recursively copy a directory
|
|
2610
2572
|
fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), CompileError> {
|
|
2573
|
+
// Copying into a symlinked folder would write into the folder it points at
|
|
2574
|
+
if fs::symlink_metadata(dest).is_ok_and(|meta| meta.file_type().is_symlink()) {
|
|
2575
|
+
fs::remove_file(dest).map_err(|e| CompileError::WriteError {
|
|
2576
|
+
path: dest.display().to_string(),
|
|
2577
|
+
source: e,
|
|
2578
|
+
})?;
|
|
2579
|
+
}
|
|
2611
2580
|
if !dest.exists() {
|
|
2612
2581
|
fs::create_dir_all(dest).map_err(|_| CompileError::CreateDirError(dest.display().to_string()))?;
|
|
2613
2582
|
}
|
|
@@ -2635,7 +2604,7 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), CompileError> {
|
|
|
2635
2604
|
}
|
|
2636
2605
|
copy_dir_recursive(&path, &dest_path)?;
|
|
2637
2606
|
} else {
|
|
2638
|
-
|
|
2607
|
+
copy_in_place(&path, &dest_path).map_err(|e| CompileError::WriteError {
|
|
2639
2608
|
path: dest_path.display().to_string(),
|
|
2640
2609
|
source: e,
|
|
2641
2610
|
})?;
|
|
@@ -2790,17 +2759,16 @@ mod tests {
|
|
|
2790
2759
|
}
|
|
2791
2760
|
|
|
2792
2761
|
#[test]
|
|
2793
|
-
fn
|
|
2794
|
-
let dir = std::env::temp_dir().join("
|
|
2762
|
+
fn write_in_place_replaces_content_without_leaving_tmp_files() {
|
|
2763
|
+
let dir = std::env::temp_dir().join("vibe_write_in_place_test");
|
|
2795
2764
|
let _ = fs::remove_dir_all(&dir);
|
|
2796
2765
|
fs::create_dir_all(&dir).unwrap();
|
|
2797
2766
|
let target = dir.join("page.html");
|
|
2798
2767
|
|
|
2799
|
-
|
|
2768
|
+
write_in_place(&target, "first").unwrap();
|
|
2800
2769
|
assert_eq!(fs::read_to_string(&target).unwrap(), "first");
|
|
2801
2770
|
|
|
2802
|
-
|
|
2803
|
-
atomic_write(&target, "second, longer content").unwrap();
|
|
2771
|
+
write_in_place(&target, "second, longer content").unwrap();
|
|
2804
2772
|
assert_eq!(fs::read_to_string(&target).unwrap(), "second, longer content");
|
|
2805
2773
|
|
|
2806
2774
|
let leftovers: Vec<String> = fs::read_dir(&dir)
|
|
@@ -2812,6 +2780,30 @@ mod tests {
|
|
|
2812
2780
|
assert!(leftovers.is_empty(), "temp artifacts left behind: {leftovers:?}");
|
|
2813
2781
|
}
|
|
2814
2782
|
|
|
2783
|
+
// bun installs packages as hard links into its cache, and an output built
|
|
2784
|
+
// by an install holds more of them. Copying a package over such an output
|
|
2785
|
+
// once truncated the cached package, and every project linked to it.
|
|
2786
|
+
#[test]
|
|
2787
|
+
fn copy_in_place_never_writes_through_a_hard_link() {
|
|
2788
|
+
let dir = std::env::temp_dir().join("vibe_copy_in_place_test");
|
|
2789
|
+
let _ = fs::remove_dir_all(&dir);
|
|
2790
|
+
fs::create_dir_all(&dir).unwrap();
|
|
2791
|
+
let (cache, installed, output) = (dir.join("cache.js"), dir.join("installed.js"), dir.join("output.js"));
|
|
2792
|
+
fs::write(&cache, "cached").unwrap();
|
|
2793
|
+
fs::hard_link(&cache, &installed).unwrap();
|
|
2794
|
+
fs::hard_link(&cache, &output).unwrap();
|
|
2795
|
+
|
|
2796
|
+
copy_in_place(&installed, &output).unwrap();
|
|
2797
|
+
assert_eq!(fs::read_to_string(&cache).unwrap(), "cached");
|
|
2798
|
+
assert_eq!(fs::read_to_string(&output).unwrap(), "cached");
|
|
2799
|
+
|
|
2800
|
+
write_in_place(&installed, "newer").unwrap();
|
|
2801
|
+
assert_eq!(fs::read_to_string(&cache).unwrap(), "cached");
|
|
2802
|
+
assert_eq!(fs::read_to_string(&installed).unwrap(), "newer");
|
|
2803
|
+
|
|
2804
|
+
let _ = fs::remove_dir_all(&dir);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2815
2807
|
// A minimal on-disk project: source with one page carrying a distinctive
|
|
2816
2808
|
// binding, empty components dir, output dir sibling. Returns (config, page
|
|
2817
2809
|
// source path, compiled page output path, manifest path).
|
|
@@ -2881,6 +2873,7 @@ mod tests {
|
|
|
2881
2873
|
reserved_elements: Vec::new(),
|
|
2882
2874
|
skip_files: Vec::new(),
|
|
2883
2875
|
node_modules_as_is: false,
|
|
2876
|
+
modules: "node_modules".to_string(),
|
|
2884
2877
|
components_as_is: true,
|
|
2885
2878
|
runtime_as_is: false,
|
|
2886
2879
|
iterations_as_is: false,
|
|
@@ -2970,6 +2963,7 @@ mod tests {
|
|
|
2970
2963
|
reserved_elements: Vec::new(),
|
|
2971
2964
|
skip_files: Vec::new(),
|
|
2972
2965
|
node_modules_as_is: false,
|
|
2966
|
+
modules: "node_modules".to_string(),
|
|
2973
2967
|
components_as_is: false,
|
|
2974
2968
|
runtime_as_is: false,
|
|
2975
2969
|
iterations_as_is: false,
|
|
@@ -3083,6 +3077,7 @@ mod tests {
|
|
|
3083
3077
|
reserved_elements: Vec::new(),
|
|
3084
3078
|
skip_files: Vec::new(),
|
|
3085
3079
|
node_modules_as_is: false,
|
|
3080
|
+
modules: "node_modules".to_string(),
|
|
3086
3081
|
components_as_is: false,
|
|
3087
3082
|
runtime_as_is: false,
|
|
3088
3083
|
iterations_as_is: false,
|
|
@@ -3155,6 +3150,7 @@ mod tests {
|
|
|
3155
3150
|
reserved_elements: Vec::new(),
|
|
3156
3151
|
skip_files: Vec::new(),
|
|
3157
3152
|
node_modules_as_is: false,
|
|
3153
|
+
modules: "node_modules".to_string(),
|
|
3158
3154
|
components_as_is: false,
|
|
3159
3155
|
runtime_as_is: false,
|
|
3160
3156
|
iterations_as_is: false,
|