@ape-egg/vibe 2.1.4 → 2.1.7
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 +25 -0
- package/README.md +48 -0
- 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 +137 -2
- package/compiler/src/compiler/component_tagger.rs +80 -28
- package/compiler/src/compiler/js_analyzer.rs +122 -36
- package/compiler/src/compiler/mod.rs +1 -0
- package/compiler/src/compiler/reassignment_analyzer.rs +456 -0
- package/compiler/src/compiler/value_stamper.rs +128 -16
- package/compiler/src/parser/html.rs +81 -3
- package/package.json +1 -1
- package/runtime/component.js +30 -3
- package/runtime/conditionals.js +8 -1
|
@@ -9,17 +9,20 @@ use std::fs;
|
|
|
9
9
|
|
|
10
10
|
/// Analyzes JavaScript code to extract state from vibe() calls
|
|
11
11
|
pub struct JsAnalyzer {
|
|
12
|
-
source_map: Lrc<SourceMap>,
|
|
13
12
|
base_path: PathBuf,
|
|
14
13
|
_module_cache: HashMap<PathBuf, Module>,
|
|
14
|
+
/// Memoizes resolved imports by (file, export name). Dedupes diamond imports
|
|
15
|
+
/// (without it, recursive resolution re-parses shared modules exponentially)
|
|
16
|
+
/// and breaks cycles (an in-progress entry is seeded `None`).
|
|
17
|
+
import_cache: HashMap<(PathBuf, String), Option<Value>>,
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
impl JsAnalyzer {
|
|
18
21
|
pub fn new(base_path: PathBuf) -> Self {
|
|
19
22
|
Self {
|
|
20
|
-
source_map: Lrc::new(SourceMap::default()),
|
|
21
23
|
base_path,
|
|
22
24
|
_module_cache: HashMap::new(),
|
|
25
|
+
import_cache: HashMap::new(),
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -34,11 +37,11 @@ impl JsAnalyzer {
|
|
|
34
37
|
module.visit_with(&mut scope_builder);
|
|
35
38
|
// eprintln!("[JsAnalyzer] Found {} imports", scope_builder.imports.len());
|
|
36
39
|
|
|
37
|
-
// Resolve imports to actual Values
|
|
40
|
+
// Resolve imports to actual Values (keyed by the LOCAL name used in code)
|
|
38
41
|
let mut resolved_imports: HashMap<String, Value> = HashMap::new();
|
|
39
|
-
for (
|
|
40
|
-
if let Some(value) = self.resolve_import_value(&import_path,
|
|
41
|
-
resolved_imports.insert(
|
|
42
|
+
for (local_name, (import_path, imported_name)) in scope_builder.imports.clone() {
|
|
43
|
+
if let Some(value) = self.resolve_import_value(&import_path, &imported_name) {
|
|
44
|
+
resolved_imports.insert(local_name, value);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -50,35 +53,73 @@ impl JsAnalyzer {
|
|
|
50
53
|
state_extractor.merged_state
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
/// Resolve an import and return the exported value (fully resolved)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
/// Resolve an import and return the exported value (fully resolved).
|
|
57
|
+
/// `imported_name` is the EXPORT name to look up ("default" for a default
|
|
58
|
+
/// import). The imported module's OWN imports are resolved too, so a chain
|
|
59
|
+
/// like `import appState` → `export default { version: VERSION }` →
|
|
60
|
+
/// `import VERSION from './version.js'` resolves all the way down.
|
|
61
|
+
fn resolve_import_value(&mut self, import_path: &str, imported_name: &str) -> Option<Value> {
|
|
62
|
+
self.resolve_import_value_depth(import_path, imported_name, 0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn resolve_import_value_depth(
|
|
66
|
+
&mut self,
|
|
67
|
+
import_path: &str,
|
|
68
|
+
imported_name: &str,
|
|
69
|
+
depth: usize,
|
|
70
|
+
) -> Option<Value> {
|
|
71
|
+
// Guard against pathological depth.
|
|
72
|
+
if depth > 16 {
|
|
73
|
+
return None;
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
let resolved_path = self.resolve_path(import_path)?;
|
|
58
|
-
|
|
77
|
+
let cache_key = (resolved_path.clone(), imported_name.to_string());
|
|
78
|
+
if let Some(cached) = self.import_cache.get(&cache_key) {
|
|
79
|
+
return cached.clone();
|
|
80
|
+
}
|
|
81
|
+
// Seed the in-progress entry so an import cycle resolves to None instead
|
|
82
|
+
// of recursing forever.
|
|
83
|
+
self.import_cache.insert(cache_key.clone(), None);
|
|
84
|
+
|
|
85
|
+
let result = self.resolve_export_from_file(&resolved_path, imported_name, depth);
|
|
86
|
+
self.import_cache.insert(cache_key, result.clone());
|
|
87
|
+
result
|
|
88
|
+
}
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
fn resolve_export_from_file(
|
|
91
|
+
&mut self,
|
|
92
|
+
resolved_path: &PathBuf,
|
|
93
|
+
imported_name: &str,
|
|
94
|
+
depth: usize,
|
|
95
|
+
) -> Option<Value> {
|
|
96
|
+
let code = fs::read_to_string(resolved_path).ok()?;
|
|
62
97
|
let module = self.parse_module(&code)?;
|
|
63
98
|
|
|
64
|
-
// Build scope for the imported module
|
|
99
|
+
// Build scope for the imported module.
|
|
65
100
|
let mut scope_builder = ScopeBuilder::new(resolved_path.parent()?.to_path_buf());
|
|
66
101
|
module.visit_with(&mut scope_builder);
|
|
67
102
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let state_extractor = StateExtractor::new(scope_builder.bindings);
|
|
78
|
-
return state_extractor.resolve_expr(&expr);
|
|
103
|
+
// Recursively resolve THIS module's own imports so a property like
|
|
104
|
+
// `version: VERSION` (itself a default import) can be resolved. Import
|
|
105
|
+
// paths in this codebase are absolute (`/js/...`), resolved against the
|
|
106
|
+
// analyzer's fixed base, so no per-module base swap is needed.
|
|
107
|
+
let mut resolved_imports: HashMap<String, Value> = HashMap::new();
|
|
108
|
+
for (local_name, (path, name)) in scope_builder.imports.clone() {
|
|
109
|
+
if let Some(value) = self.resolve_import_value_depth(&path, &name, depth + 1) {
|
|
110
|
+
resolved_imports.insert(local_name, value);
|
|
111
|
+
}
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
|
|
114
|
+
// Find the requested export.
|
|
115
|
+
let mut export_finder =
|
|
116
|
+
ExportFinder::new(imported_name.to_string(), scope_builder.bindings.clone());
|
|
117
|
+
module.visit_with(&mut export_finder);
|
|
118
|
+
|
|
119
|
+
let expr = export_finder.found_export?;
|
|
120
|
+
let mut state_extractor = StateExtractor::new(scope_builder.bindings);
|
|
121
|
+
state_extractor.resolved_imports = resolved_imports;
|
|
122
|
+
state_extractor.resolve_expr(&expr)
|
|
82
123
|
}
|
|
83
124
|
|
|
84
125
|
/// Resolve import path relative to base_path
|
|
@@ -115,9 +156,12 @@ impl JsAnalyzer {
|
|
|
115
156
|
}
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
/// Parse JavaScript/TypeScript module
|
|
159
|
+
/// Parse JavaScript/TypeScript module. Uses a FRESH source map per call —
|
|
160
|
+
/// recursive import resolution parses many files, and a shared map would
|
|
161
|
+
/// accumulate byte positions until they overflow (`start <= end` panic).
|
|
119
162
|
fn parse_module(&self, code: &str) -> Option<Module> {
|
|
120
|
-
let
|
|
163
|
+
let source_map: Lrc<SourceMap> = Default::default();
|
|
164
|
+
let fm = source_map.new_source_file(
|
|
121
165
|
Lrc::new(FileName::Anon),
|
|
122
166
|
code.to_string(),
|
|
123
167
|
);
|
|
@@ -147,7 +191,9 @@ impl JsAnalyzer {
|
|
|
147
191
|
/// Builds a scope of variable bindings and tracks imports
|
|
148
192
|
struct ScopeBuilder {
|
|
149
193
|
bindings: HashMap<String, Expr>,
|
|
150
|
-
|
|
194
|
+
// local binding name -> (import path, imported export name; "default" for a
|
|
195
|
+
// default import, "*" for a namespace import)
|
|
196
|
+
imports: HashMap<String, (String, String)>,
|
|
151
197
|
_base_path: PathBuf,
|
|
152
198
|
}
|
|
153
199
|
|
|
@@ -167,21 +213,29 @@ impl Visit for ScopeBuilder {
|
|
|
167
213
|
|
|
168
214
|
for specifier in &import.specifiers {
|
|
169
215
|
match specifier {
|
|
170
|
-
// import {
|
|
216
|
+
// import { orig as local } from './file.js'
|
|
171
217
|
ImportSpecifier::Named(named) => {
|
|
172
|
-
let
|
|
218
|
+
let imported = match &named.imported {
|
|
173
219
|
Some(ModuleExportName::Ident(ident)) => ident.sym.to_string(),
|
|
174
|
-
|
|
220
|
+
Some(ModuleExportName::Str(s)) => s.value.to_string(),
|
|
221
|
+
None => named.local.sym.to_string(),
|
|
175
222
|
};
|
|
176
|
-
self.imports
|
|
223
|
+
self.imports
|
|
224
|
+
.insert(named.local.sym.to_string(), (import_path.clone(), imported));
|
|
177
225
|
}
|
|
178
226
|
// import name from './file.js' (default import)
|
|
179
227
|
ImportSpecifier::Default(default) => {
|
|
180
|
-
self.imports.insert(
|
|
228
|
+
self.imports.insert(
|
|
229
|
+
default.local.sym.to_string(),
|
|
230
|
+
(import_path.clone(), "default".to_string()),
|
|
231
|
+
);
|
|
181
232
|
}
|
|
182
|
-
// import * as name from './file.js'
|
|
233
|
+
// import * as name from './file.js' (namespace import)
|
|
183
234
|
ImportSpecifier::Namespace(namespace) => {
|
|
184
|
-
self.imports.insert(
|
|
235
|
+
self.imports.insert(
|
|
236
|
+
namespace.local.sym.to_string(),
|
|
237
|
+
(import_path.clone(), "*".to_string()),
|
|
238
|
+
);
|
|
185
239
|
}
|
|
186
240
|
}
|
|
187
241
|
}
|
|
@@ -233,6 +287,13 @@ impl ExportFinder {
|
|
|
233
287
|
}
|
|
234
288
|
|
|
235
289
|
impl Visit for ExportFinder {
|
|
290
|
+
// `export default <expr>` — matched when the consumer used a default import.
|
|
291
|
+
fn visit_export_default_expr(&mut self, export: &ExportDefaultExpr) {
|
|
292
|
+
if self.target_name == "default" {
|
|
293
|
+
self.found_export = Some((*export.expr).clone());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
236
297
|
fn visit_export_decl(&mut self, export: &ExportDecl) {
|
|
237
298
|
match &export.decl {
|
|
238
299
|
Decl::Var(var_decl) => {
|
|
@@ -626,4 +687,29 @@ mod tests {
|
|
|
626
687
|
assert_eq!(state["other"].as_str().unwrap(), "value");
|
|
627
688
|
assert_eq!(state["extra"].as_str().unwrap(), "value");
|
|
628
689
|
}
|
|
690
|
+
|
|
691
|
+
#[test]
|
|
692
|
+
fn resolves_default_import_chain() {
|
|
693
|
+
// Two nested DEFAULT imports, mirroring boot.js → app.js → version.js:
|
|
694
|
+
// `import appState` → `export default { version: VERSION }` → `import VERSION`.
|
|
695
|
+
let dir = std::env::temp_dir().join("vibe_default_import_chain_test");
|
|
696
|
+
let _ = fs::remove_dir_all(&dir);
|
|
697
|
+
fs::create_dir_all(&dir).unwrap();
|
|
698
|
+
fs::write(dir.join("version.js"), "export default '0.1.5';").unwrap();
|
|
699
|
+
fs::write(
|
|
700
|
+
dir.join("app.js"),
|
|
701
|
+
"import VERSION from '/version.js';\nexport default { version: VERSION, coins: 400 };",
|
|
702
|
+
)
|
|
703
|
+
.unwrap();
|
|
704
|
+
|
|
705
|
+
let mut analyzer = JsAnalyzer::new(dir.clone());
|
|
706
|
+
let state = analyzer
|
|
707
|
+
.extract_state("import appState from '/app.js';\nvibe({ ...appState });")
|
|
708
|
+
.unwrap();
|
|
709
|
+
|
|
710
|
+
assert_eq!(state["version"], "0.1.5");
|
|
711
|
+
assert_eq!(state["coins"].as_f64(), Some(400.0));
|
|
712
|
+
|
|
713
|
+
let _ = fs::remove_dir_all(&dir);
|
|
714
|
+
}
|
|
629
715
|
}
|