@tishlang/tish 2.10.1 → 2.16.13
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/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +16 -0
- package/crates/tish/src/main.rs +24 -4
- package/crates/tish/tests/integration_test.rs +149 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +91 -4
- package/crates/tish_compile_js/src/lib.rs +5 -2
- package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +253 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -11,6 +11,10 @@ use tishlang_ast::{
|
|
|
11
11
|
|
|
12
12
|
use crate::error::CompileError;
|
|
13
13
|
|
|
14
|
+
/// Default module specifier the JSX runtime (`h` / `Fragment`) is auto-imported from (issue #291).
|
|
15
|
+
/// Matches the repo convention (`import { h } from "lattish"`); overridable via `--jsx-import-source`.
|
|
16
|
+
pub const DEFAULT_JSX_IMPORT_SOURCE: &str = "lattish";
|
|
17
|
+
|
|
14
18
|
/// JS output mode. `Bundle` flattens every module into one file (imports/exports are resolved away
|
|
15
19
|
/// by `merge_modules`). `Esm` emits one file per module with real ES `import`/`export` statements so
|
|
16
20
|
/// a bundler (Vite/Rollup) can tree-shake and code-split. See issue #177's sibling, #282.
|
|
@@ -42,6 +46,9 @@ struct Codegen {
|
|
|
42
46
|
module_path: PathBuf,
|
|
43
47
|
/// ESM only: the project root, so import targets can be located relative to the output tree.
|
|
44
48
|
project_root: PathBuf,
|
|
49
|
+
/// ESM only: the module specifier the JSX runtime (`h` / `Fragment`) is auto-imported from when a
|
|
50
|
+
/// module uses JSX but doesn't import them itself (issue #291). Defaults to `lattish`.
|
|
51
|
+
jsx_import_source: String,
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
|
|
@@ -63,10 +70,16 @@ impl Codegen {
|
|
|
63
70
|
import_rewrite: ImportRewrite::Disk,
|
|
64
71
|
module_path: PathBuf::new(),
|
|
65
72
|
project_root: PathBuf::new(),
|
|
73
|
+
jsx_import_source: DEFAULT_JSX_IMPORT_SOURCE.to_string(),
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
fn new_esm(
|
|
77
|
+
fn new_esm(
|
|
78
|
+
module_path: PathBuf,
|
|
79
|
+
project_root: PathBuf,
|
|
80
|
+
import_rewrite: ImportRewrite,
|
|
81
|
+
jsx_import_source: String,
|
|
82
|
+
) -> Self {
|
|
70
83
|
Self {
|
|
71
84
|
output: String::new(),
|
|
72
85
|
indent: 0,
|
|
@@ -75,6 +88,7 @@ impl Codegen {
|
|
|
75
88
|
import_rewrite,
|
|
76
89
|
module_path,
|
|
77
90
|
project_root,
|
|
91
|
+
jsx_import_source,
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
|
|
@@ -139,6 +153,7 @@ impl Codegen {
|
|
|
139
153
|
map_builder: Option<&mut SourceMapBuilder>,
|
|
140
154
|
) -> Result<(), CompileError> {
|
|
141
155
|
self.write("// Generated by tishlang_compile_js\n");
|
|
156
|
+
self.emit_jsx_runtime_auto_import(program)?;
|
|
142
157
|
match (map_sources, map_builder) {
|
|
143
158
|
(Some((srcs, root)), Some(sm)) => {
|
|
144
159
|
for (i, stmt) in program.statements.iter().enumerate() {
|
|
@@ -176,6 +191,34 @@ impl Codegen {
|
|
|
176
191
|
Ok(())
|
|
177
192
|
}
|
|
178
193
|
|
|
194
|
+
/// Auto-import the JSX runtime (`h` / `Fragment`) for ESM modules that use JSX but don't bring
|
|
195
|
+
/// those bindings into scope themselves (issue #291). In `--format bundle` the merged scope makes
|
|
196
|
+
/// a single `lattish` import visible everywhere, but each ES module is its own scope, so a JSX
|
|
197
|
+
/// module that didn't `import { h }` throws `ReferenceError` at load. We inject only the missing
|
|
198
|
+
/// names (so an explicit `import { h }` isn't duplicated) from `jsx_import_source`, rewritten the
|
|
199
|
+
/// same way as user imports (disk `.js` path vs. Vite-dev bare specifier). No-op outside ESM.
|
|
200
|
+
fn emit_jsx_runtime_auto_import(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
201
|
+
if self.emit_mode != EmitMode::Esm {
|
|
202
|
+
return Ok(());
|
|
203
|
+
}
|
|
204
|
+
let needed = tishlang_ui::jsx::jsx_runtime_imports_needed(program);
|
|
205
|
+
if needed.is_empty() {
|
|
206
|
+
return Ok(());
|
|
207
|
+
}
|
|
208
|
+
let spec = rewrite_import_to_js(
|
|
209
|
+
&self.jsx_import_source,
|
|
210
|
+
&self.module_path,
|
|
211
|
+
&self.project_root,
|
|
212
|
+
self.import_rewrite,
|
|
213
|
+
)?;
|
|
214
|
+
self.writeln(&format!(
|
|
215
|
+
"import {{ {} }} from \"{}\";",
|
|
216
|
+
needed.join(", "),
|
|
217
|
+
spec
|
|
218
|
+
));
|
|
219
|
+
Ok(())
|
|
220
|
+
}
|
|
221
|
+
|
|
179
222
|
fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
180
223
|
match stmt {
|
|
181
224
|
Statement::Block { statements, .. } => {
|
|
@@ -993,6 +1036,39 @@ impl Codegen {
|
|
|
993
1036
|
let v = self.emit_expr(e)?;
|
|
994
1037
|
self.writeln(&format!("export default {};", v));
|
|
995
1038
|
}
|
|
1039
|
+
// #305: re-export in ESM mode — emit the native `export { … } from` / `export * from`,
|
|
1040
|
+
// rewriting the `.tish` specifier to the emitted `.js` module path.
|
|
1041
|
+
ExportDeclaration::ReExport {
|
|
1042
|
+
specifiers,
|
|
1043
|
+
all,
|
|
1044
|
+
from,
|
|
1045
|
+
..
|
|
1046
|
+
} => {
|
|
1047
|
+
let spec = rewrite_import_to_js(
|
|
1048
|
+
from.as_ref(),
|
|
1049
|
+
&self.module_path,
|
|
1050
|
+
&self.project_root,
|
|
1051
|
+
self.import_rewrite,
|
|
1052
|
+
)?;
|
|
1053
|
+
if *all {
|
|
1054
|
+
self.writeln(&format!("export * from {:?};", spec));
|
|
1055
|
+
} else {
|
|
1056
|
+
let mut parts: Vec<String> = Vec::new();
|
|
1057
|
+
for s in specifiers {
|
|
1058
|
+
if let ImportSpecifier::Named { name, alias, .. } = s {
|
|
1059
|
+
match alias {
|
|
1060
|
+
Some(a) => parts.push(format!(
|
|
1061
|
+
"{} as {}",
|
|
1062
|
+
Self::escape_ident(name.as_ref()),
|
|
1063
|
+
Self::escape_ident(a.as_ref())
|
|
1064
|
+
)),
|
|
1065
|
+
None => parts.push(Self::escape_ident(name.as_ref()).to_string()),
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
self.writeln(&format!("export {{ {} }} from {:?};", parts.join(", "), spec));
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
996
1072
|
}
|
|
997
1073
|
Ok(())
|
|
998
1074
|
}
|
|
@@ -1235,6 +1311,7 @@ pub fn compile_project_esm(
|
|
|
1235
1311
|
entry_path: &Path,
|
|
1236
1312
|
project_root: Option<&Path>,
|
|
1237
1313
|
optimize: bool,
|
|
1314
|
+
jsx_import_source: &str,
|
|
1238
1315
|
) -> Result<Vec<EmittedJsModule>, CompileError> {
|
|
1239
1316
|
let modules = tishlang_compile::resolve_project(entry_path, project_root)
|
|
1240
1317
|
.map_err(|e| CompileError { message: e })?;
|
|
@@ -1274,8 +1351,12 @@ pub fn compile_project_esm(
|
|
|
1274
1351
|
} else {
|
|
1275
1352
|
module.program.clone()
|
|
1276
1353
|
};
|
|
1277
|
-
let mut gen =
|
|
1278
|
-
|
|
1354
|
+
let mut gen = Codegen::new_esm(
|
|
1355
|
+
mod_canon.clone(),
|
|
1356
|
+
res_root_canon.clone(),
|
|
1357
|
+
ImportRewrite::Disk,
|
|
1358
|
+
jsx_import_source.to_string(),
|
|
1359
|
+
);
|
|
1279
1360
|
gen.emit_program(&program, None, None)?;
|
|
1280
1361
|
out.push(EmittedJsModule {
|
|
1281
1362
|
relative_path: rel_js,
|
|
@@ -1297,6 +1378,7 @@ pub fn compile_module_esm(
|
|
|
1297
1378
|
optimize: bool,
|
|
1298
1379
|
import_rewrite: ImportRewrite,
|
|
1299
1380
|
source_map: bool,
|
|
1381
|
+
jsx_import_source: &str,
|
|
1300
1382
|
) -> Result<JsBundle, CompileError> {
|
|
1301
1383
|
if source_map && optimize {
|
|
1302
1384
|
return Err(CompileError {
|
|
@@ -1324,7 +1406,12 @@ pub fn compile_module_esm(
|
|
|
1324
1406
|
.unwrap_or_else(|| PathBuf::from("."));
|
|
1325
1407
|
let root_canon = root.canonicalize().unwrap_or(root);
|
|
1326
1408
|
|
|
1327
|
-
let mut gen = Codegen::new_esm(
|
|
1409
|
+
let mut gen = Codegen::new_esm(
|
|
1410
|
+
module_canon.clone(),
|
|
1411
|
+
root_canon.clone(),
|
|
1412
|
+
import_rewrite,
|
|
1413
|
+
jsx_import_source.to_string(),
|
|
1414
|
+
);
|
|
1328
1415
|
if source_map {
|
|
1329
1416
|
let stmt_sources = vec![module_canon.clone(); program.statements.len()];
|
|
1330
1417
|
let mut builder = SourceMapBuilder::new(Some("module.js"));
|
|
@@ -10,11 +10,14 @@ mod tests_jsx;
|
|
|
10
10
|
pub use codegen::{
|
|
11
11
|
compile_module_esm, compile_project_esm, compile_project_with_jsx,
|
|
12
12
|
compile_project_with_jsx_and_source_map, compile_with_jsx, EmittedJsModule, ImportRewrite,
|
|
13
|
-
JsBundle,
|
|
13
|
+
JsBundle, DEFAULT_JSX_IMPORT_SOURCE,
|
|
14
14
|
};
|
|
15
15
|
pub use error::CompileError;
|
|
16
16
|
|
|
17
|
-
/// JSX lowers to `h` / `Fragment`;
|
|
17
|
+
/// Compile a single program to a bundle-style JS string. JSX lowers to `h` / `Fragment`; in bundle
|
|
18
|
+
/// mode those resolve against the merged scope (merge the `lattish` runtime for hooks and DOM). The
|
|
19
|
+
/// per-module ESM paths ([`compile_module_esm`] / [`compile_project_esm`]) auto-import the runtime
|
|
20
|
+
/// for JSX modules that don't import it themselves (issue #291).
|
|
18
21
|
pub fn compile(program: &tishlang_ast::Program, optimize: bool) -> Result<String, CompileError> {
|
|
19
22
|
compile_with_jsx(program, optimize)
|
|
20
23
|
}
|
|
@@ -475,7 +475,8 @@ fn factory() {
|
|
|
475
475
|
f.write_all(src.as_bytes()).unwrap();
|
|
476
476
|
f.sync_all().unwrap();
|
|
477
477
|
}
|
|
478
|
-
compile_project_esm(&dir.join(entry), Some(dir), false
|
|
478
|
+
compile_project_esm(&dir.join(entry), Some(dir), false, "lattish")
|
|
479
|
+
.expect("compile_project_esm failed")
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
fn module_js<'a>(mods: &'a [EmittedJsModule], rel: &str) -> &'a str {
|
|
@@ -557,6 +558,7 @@ fn factory() {
|
|
|
557
558
|
&base.join("app/src/main.tish"),
|
|
558
559
|
Some(&base.join("app")),
|
|
559
560
|
false,
|
|
561
|
+
"lattish",
|
|
560
562
|
)
|
|
561
563
|
.expect("compile_project_esm failed for sibling dep");
|
|
562
564
|
let rels: Vec<String> = mods
|
|
@@ -607,7 +609,7 @@ fn factory() {
|
|
|
607
609
|
"import { ping } from \"pkg\"\nconsole.log(ping())\n",
|
|
608
610
|
)
|
|
609
611
|
.unwrap();
|
|
610
|
-
let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false)
|
|
612
|
+
let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false, "lattish")
|
|
611
613
|
.expect("compile_project_esm failed for node_modules dep");
|
|
612
614
|
let rels: Vec<String> = mods
|
|
613
615
|
.iter()
|
|
@@ -635,7 +637,7 @@ fn factory() {
|
|
|
635
637
|
let dir = tmp.path();
|
|
636
638
|
let p = dir.join("main.tish");
|
|
637
639
|
std::fs::write(&p, "import { readFile } from \"fs\"\nconsole.log(1)\n").unwrap();
|
|
638
|
-
let err = compile_project_esm(&p, Some(dir), false).unwrap_err();
|
|
640
|
+
let err = compile_project_esm(&p, Some(dir), false, "lattish").unwrap_err();
|
|
639
641
|
assert!(
|
|
640
642
|
err.message.contains("Native module import") && err.message.contains("esm"),
|
|
641
643
|
"expected a native-import rejection for ESM, got: {}",
|
|
@@ -665,6 +667,7 @@ fn factory() {
|
|
|
665
667
|
false,
|
|
666
668
|
ImportRewrite::ViteDev,
|
|
667
669
|
false,
|
|
670
|
+
"lattish",
|
|
668
671
|
)
|
|
669
672
|
.expect("compile_module_esm failed")
|
|
670
673
|
.js
|
|
@@ -741,7 +744,7 @@ fn factory() {
|
|
|
741
744
|
let dir = tmp.path();
|
|
742
745
|
let p = dir.join("main.tish");
|
|
743
746
|
std::fs::write(&p, "import { readFile } from \"fs\"\nconsole.log(1)\n").unwrap();
|
|
744
|
-
let err = compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, false)
|
|
747
|
+
let err = compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, false, "lattish")
|
|
745
748
|
.unwrap_err();
|
|
746
749
|
assert!(
|
|
747
750
|
err.message.contains("Native module import"),
|
|
@@ -758,7 +761,8 @@ fn factory() {
|
|
|
758
761
|
std::fs::write(&p, "export fn greet(n) { return \"hi \" + n }\nconsole.log(greet(\"x\"))\n")
|
|
759
762
|
.unwrap();
|
|
760
763
|
let bundle =
|
|
761
|
-
compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true)
|
|
764
|
+
compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true, "lattish")
|
|
765
|
+
.unwrap();
|
|
762
766
|
let map = bundle
|
|
763
767
|
.source_map_json
|
|
764
768
|
.expect("source map requested → must be present");
|
|
@@ -782,7 +786,8 @@ fn factory() {
|
|
|
782
786
|
let src = "export fn greet(n) { return \"hi \" + n }\n";
|
|
783
787
|
std::fs::write(&p, src).unwrap();
|
|
784
788
|
let bundle =
|
|
785
|
-
compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true)
|
|
789
|
+
compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true, "lattish")
|
|
790
|
+
.unwrap();
|
|
786
791
|
let map = bundle.source_map_json.expect("source map requested");
|
|
787
792
|
assert!(
|
|
788
793
|
map.contains("\"sourcesContent\""),
|
|
@@ -800,7 +805,7 @@ fn factory() {
|
|
|
800
805
|
let dir = tmp.path();
|
|
801
806
|
let p = dir.join("main.tish");
|
|
802
807
|
std::fs::write(&p, "console.log(1)\n").unwrap();
|
|
803
|
-
let err = compile_module_esm(&p, Some(dir), true, ImportRewrite::ViteDev, true)
|
|
808
|
+
let err = compile_module_esm(&p, Some(dir), true, ImportRewrite::ViteDev, true, "lattish")
|
|
804
809
|
.unwrap_err();
|
|
805
810
|
assert!(
|
|
806
811
|
err.message.to_lowercase().contains("optimiz"),
|
|
@@ -808,4 +813,167 @@ fn factory() {
|
|
|
808
813
|
err.message
|
|
809
814
|
);
|
|
810
815
|
}
|
|
816
|
+
|
|
817
|
+
// ── #291: JSX runtime auto-import (h / Fragment) for per-module ESM ────────────────────────
|
|
818
|
+
|
|
819
|
+
#[test]
|
|
820
|
+
fn compile_module_esm_jsx_auto_imports_h() {
|
|
821
|
+
// A module that uses a JSX element but never imports `h` must get the runtime auto-imported,
|
|
822
|
+
// or it throws `ReferenceError: h` at load in per-module ESM (issue #291).
|
|
823
|
+
let js = build_module_vite("main.tish", &[("main.tish", "fn X() { return <div/> }\n")]);
|
|
824
|
+
assert!(
|
|
825
|
+
js.contains("import { h } from \"lattish\";"),
|
|
826
|
+
"JSX element module must auto-import h:\n{js}"
|
|
827
|
+
);
|
|
828
|
+
assert!(
|
|
829
|
+
!js.contains("Fragment"),
|
|
830
|
+
"no fragment used → Fragment must not be imported:\n{js}"
|
|
831
|
+
);
|
|
832
|
+
assert!(js.contains("h(\"div\""), "JSX still lowers to h(...):\n{js}");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#[test]
|
|
836
|
+
fn compile_module_esm_jsx_auto_imports_fragment() {
|
|
837
|
+
// A fragment lowers to `h(Fragment, …)`, so both `h` and `Fragment` must be auto-imported.
|
|
838
|
+
let js = build_module_vite(
|
|
839
|
+
"main.tish",
|
|
840
|
+
&[("main.tish", "fn X() { return <>{\"a\"}</> }\n")],
|
|
841
|
+
);
|
|
842
|
+
assert!(
|
|
843
|
+
js.contains("import { h, Fragment } from \"lattish\";"),
|
|
844
|
+
"fragment module must auto-import both h and Fragment:\n{js}"
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
#[test]
|
|
849
|
+
fn compile_module_esm_jsx_skips_import_when_present() {
|
|
850
|
+
// An explicit `import { h, Fragment } from "lattish"` must not be duplicated.
|
|
851
|
+
let js = build_module_vite(
|
|
852
|
+
"main.tish",
|
|
853
|
+
&[(
|
|
854
|
+
"main.tish",
|
|
855
|
+
"import { h, Fragment } from \"lattish\"\nfn X() { return <></> }\n",
|
|
856
|
+
)],
|
|
857
|
+
);
|
|
858
|
+
assert_eq!(
|
|
859
|
+
js.matches("from \"lattish\"").count(),
|
|
860
|
+
1,
|
|
861
|
+
"exactly one lattish import (the author's), no auto-import duplicate:\n{js}"
|
|
862
|
+
);
|
|
863
|
+
assert!(
|
|
864
|
+
js.contains("import { h, Fragment } from \"lattish\";"),
|
|
865
|
+
"the author's import is preserved:\n{js}"
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
#[test]
|
|
870
|
+
fn compile_module_esm_jsx_partial_import_extends() {
|
|
871
|
+
// The author imported only `h`; a fragment also needs `Fragment`, so just that one missing
|
|
872
|
+
// binding is auto-imported (no re-import of `h`).
|
|
873
|
+
let js = build_module_vite(
|
|
874
|
+
"main.tish",
|
|
875
|
+
&[(
|
|
876
|
+
"main.tish",
|
|
877
|
+
"import { h } from \"lattish\"\nfn X() { return <></> }\n",
|
|
878
|
+
)],
|
|
879
|
+
);
|
|
880
|
+
assert!(
|
|
881
|
+
js.contains("import { Fragment } from \"lattish\";"),
|
|
882
|
+
"only the missing Fragment binding is auto-imported:\n{js}"
|
|
883
|
+
);
|
|
884
|
+
assert!(
|
|
885
|
+
js.contains("import { h } from \"lattish\";"),
|
|
886
|
+
"the author's h import is preserved:\n{js}"
|
|
887
|
+
);
|
|
888
|
+
assert!(
|
|
889
|
+
!js.contains("import { h, Fragment }"),
|
|
890
|
+
"h must not be re-imported alongside Fragment:\n{js}"
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#[test]
|
|
895
|
+
fn compile_module_esm_vite_dev_jsx_keeps_bare_runtime_import() {
|
|
896
|
+
// In Vite-dev mode the auto-import specifier stays the bare package so Node/Vite resolves it
|
|
897
|
+
// (it is NOT rewritten to a relative `.js` path the way disk ESM does).
|
|
898
|
+
let js = build_module_vite("main.tish", &[("main.tish", "fn X() { return <div/> }\n")]);
|
|
899
|
+
assert!(
|
|
900
|
+
js.contains("from \"lattish\";"),
|
|
901
|
+
"Vite dev keeps the bare runtime specifier:\n{js}"
|
|
902
|
+
);
|
|
903
|
+
assert!(
|
|
904
|
+
!js.contains("lattish.js") && !js.contains("./lattish"),
|
|
905
|
+
"Vite dev must not rewrite the runtime specifier to a path:\n{js}"
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
#[test]
|
|
910
|
+
fn compile_module_esm_jsx_import_source_is_configurable() {
|
|
911
|
+
// The runtime package is overridable (e.g. the scoped `@tishlang/lattish`, or a custom runtime).
|
|
912
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
913
|
+
let dir = tmp.path();
|
|
914
|
+
let p = dir.join("main.tish");
|
|
915
|
+
std::fs::write(&p, "fn X() { return <div/> }\n").unwrap();
|
|
916
|
+
let js = compile_module_esm(
|
|
917
|
+
&p,
|
|
918
|
+
Some(dir),
|
|
919
|
+
false,
|
|
920
|
+
ImportRewrite::ViteDev,
|
|
921
|
+
false,
|
|
922
|
+
"@tishlang/lattish",
|
|
923
|
+
)
|
|
924
|
+
.expect("compile_module_esm failed")
|
|
925
|
+
.js;
|
|
926
|
+
assert!(
|
|
927
|
+
js.contains("import { h } from \"@tishlang/lattish\";"),
|
|
928
|
+
"custom jsx import source used:\n{js}"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
#[test]
|
|
933
|
+
fn compile_project_esm_jsx_auto_import_per_module() {
|
|
934
|
+
// Only the module that actually uses JSX gets the runtime import; plain modules are untouched.
|
|
935
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
936
|
+
let base = tmp.path();
|
|
937
|
+
std::fs::create_dir_all(base.join("src")).unwrap();
|
|
938
|
+
std::fs::create_dir_all(base.join("node_modules/lattish")).unwrap();
|
|
939
|
+
std::fs::write(
|
|
940
|
+
base.join("node_modules/lattish/package.json"),
|
|
941
|
+
"{\"name\":\"lattish\",\"main\":\"index.tish\"}\n",
|
|
942
|
+
)
|
|
943
|
+
.unwrap();
|
|
944
|
+
std::fs::write(
|
|
945
|
+
base.join("node_modules/lattish/index.tish"),
|
|
946
|
+
"export fn h(tag, props, children) { return tag }\nexport let Fragment = \"frag\"\n",
|
|
947
|
+
)
|
|
948
|
+
.unwrap();
|
|
949
|
+
std::fs::write(
|
|
950
|
+
base.join("src/view.tish"),
|
|
951
|
+
"export fn V() { return <div/> }\n",
|
|
952
|
+
)
|
|
953
|
+
.unwrap();
|
|
954
|
+
std::fs::write(
|
|
955
|
+
base.join("src/plain.tish"),
|
|
956
|
+
"export fn add(a, b) { return a + b }\n",
|
|
957
|
+
)
|
|
958
|
+
.unwrap();
|
|
959
|
+
std::fs::write(
|
|
960
|
+
base.join("src/main.tish"),
|
|
961
|
+
"import { V } from \"./view.tish\"\nimport { add } from \"./plain.tish\"\nconsole.log(add(1, 2))\n",
|
|
962
|
+
)
|
|
963
|
+
.unwrap();
|
|
964
|
+
let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false, "lattish")
|
|
965
|
+
.expect("compile_project_esm failed");
|
|
966
|
+
// The runtime isn't imported by any module, so it's not in the graph; the common-ancestor
|
|
967
|
+
// output base is `src/`, making the emitted paths flat (`view.js`, `plain.js`, `main.js`).
|
|
968
|
+
let view = module_js(&mods, "view.js");
|
|
969
|
+
let plain = module_js(&mods, "plain.js");
|
|
970
|
+
assert!(
|
|
971
|
+
view.contains("import { h } from") && view.contains("lattish"),
|
|
972
|
+
"JSX module auto-imports the runtime:\n{view}"
|
|
973
|
+
);
|
|
974
|
+
assert!(
|
|
975
|
+
!plain.contains("import { h }") && !plain.contains("Fragment"),
|
|
976
|
+
"non-JSX module must not get a runtime import:\n{plain}"
|
|
977
|
+
);
|
|
978
|
+
}
|
|
811
979
|
}
|
|
@@ -98,6 +98,21 @@ fn resolve_import_to_key(
|
|
|
98
98
|
))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/// The module specifier a statement imports/re-exports from, if any — covers both
|
|
102
|
+
/// `import … from "x"` and `export … from "x"` (#305 re-export). Dependency discovery and cycle
|
|
103
|
+
/// detection use this so they follow re-export edges, not just plain imports (otherwise a module
|
|
104
|
+
/// reached only via a re-export would never be loaded).
|
|
105
|
+
fn stmt_module_source(stmt: &Statement) -> Option<&str> {
|
|
106
|
+
match stmt {
|
|
107
|
+
Statement::Import { from, .. } => Some(&**from),
|
|
108
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
109
|
+
ExportDeclaration::ReExport { from, .. } => Some(&**from),
|
|
110
|
+
_ => None,
|
|
111
|
+
},
|
|
112
|
+
_ => None,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
/// Resolve all modules starting from the entry file. Returns modules in dependency order.
|
|
102
117
|
pub fn resolve_virtual(
|
|
103
118
|
entry_path: &str,
|
|
@@ -163,7 +178,7 @@ fn load_module_recursive(
|
|
|
163
178
|
|
|
164
179
|
let from_dir = parent_dir(module_path);
|
|
165
180
|
for stmt in &program.statements {
|
|
166
|
-
if let
|
|
181
|
+
if let Some(from) = stmt_module_source(stmt) {
|
|
167
182
|
if is_native_import(from) {
|
|
168
183
|
continue;
|
|
169
184
|
}
|
|
@@ -217,7 +232,7 @@ fn has_cycle_from(
|
|
|
217
232
|
visiting: &mut HashSet<usize>,
|
|
218
233
|
) -> Result<bool, String> {
|
|
219
234
|
for stmt in &program.statements {
|
|
220
|
-
if let
|
|
235
|
+
if let Some(from) = stmt_module_source(stmt) {
|
|
221
236
|
if is_native_import(from) {
|
|
222
237
|
continue;
|
|
223
238
|
}
|
|
@@ -292,6 +307,40 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
292
307
|
let default_name = format!("__default_{}", idx);
|
|
293
308
|
module_exports[idx].insert("default".to_string(), default_name);
|
|
294
309
|
}
|
|
310
|
+
// #305: re-export — map each re-exported name to the DEP's binding (the dep is
|
|
311
|
+
// earlier in load order, so its export table is already built). `export *` copies
|
|
312
|
+
// every dep export without overriding an explicit one; `export { a as b }` maps
|
|
313
|
+
// b -> dep's binding for a. Mirrors `merge_modules` in tish_compile/src/resolve.rs.
|
|
314
|
+
ExportDeclaration::ReExport {
|
|
315
|
+
specifiers,
|
|
316
|
+
all,
|
|
317
|
+
from,
|
|
318
|
+
..
|
|
319
|
+
} => {
|
|
320
|
+
let dir = parent_dir(&module.path);
|
|
321
|
+
let dep = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)
|
|
322
|
+
.ok()
|
|
323
|
+
.and_then(|key| path_to_idx.get(&key).copied())
|
|
324
|
+
.map(|dep_idx| module_exports[dep_idx].clone());
|
|
325
|
+
if let Some(dep) = dep {
|
|
326
|
+
if *all {
|
|
327
|
+
for (k, v) in &dep {
|
|
328
|
+
module_exports[idx]
|
|
329
|
+
.entry(k.clone())
|
|
330
|
+
.or_insert_with(|| v.clone());
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for spec in specifiers {
|
|
334
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
335
|
+
if let Some(binding) = dep.get(name.as_ref()) {
|
|
336
|
+
let export_name =
|
|
337
|
+
alias.as_deref().unwrap_or(name.as_ref()).to_string();
|
|
338
|
+
module_exports[idx].insert(export_name, binding.clone());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
295
344
|
}
|
|
296
345
|
}
|
|
297
346
|
}
|
|
@@ -440,6 +489,10 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
440
489
|
span: espan,
|
|
441
490
|
});
|
|
442
491
|
}
|
|
492
|
+
// #305: re-export emits no code — `module_exports` already maps the re-exported
|
|
493
|
+
// names to the dep's bindings (in scope in the flattened bundle), so downstream
|
|
494
|
+
// imports resolve directly. Nothing to push here.
|
|
495
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
443
496
|
},
|
|
444
497
|
_ => statements.push(stmt.clone()),
|
|
445
498
|
}
|
|
@@ -471,4 +524,49 @@ mod tests {
|
|
|
471
524
|
let program = merge_modules_virtual(modules).unwrap();
|
|
472
525
|
assert!(!program.statements.is_empty());
|
|
473
526
|
}
|
|
527
|
+
|
|
528
|
+
#[test]
|
|
529
|
+
fn test_resolve_virtual_reexport_alias() {
|
|
530
|
+
// #305: `export { add as plus } from "./dep"` re-exports dep's `add` as `plus`; a downstream
|
|
531
|
+
// `import { plus }` must resolve to dep's binding. Regression guard: the merge previously did
|
|
532
|
+
// not handle `ExportDeclaration::ReExport` (E0004 build failure), and discovery did not follow
|
|
533
|
+
// the re-export edge (so a dep reached only via re-export was never loaded).
|
|
534
|
+
let mut files = HashMap::new();
|
|
535
|
+
files.insert(
|
|
536
|
+
"dep.tish".to_string(),
|
|
537
|
+
"export fn add(a, b) { return a + b }".to_string(),
|
|
538
|
+
);
|
|
539
|
+
files.insert(
|
|
540
|
+
"mid.tish".to_string(),
|
|
541
|
+
"export { add as plus } from \"./dep.tish\"".to_string(),
|
|
542
|
+
);
|
|
543
|
+
files.insert(
|
|
544
|
+
"main.tish".to_string(),
|
|
545
|
+
"import { plus } from \"./mid.tish\"\nconsole.log(plus(1, 2))".to_string(),
|
|
546
|
+
);
|
|
547
|
+
let modules = resolve_virtual("main.tish", &files).unwrap();
|
|
548
|
+
// dep.tish is reached ONLY via mid's re-export → discovery must have loaded it.
|
|
549
|
+
assert!(
|
|
550
|
+
modules.iter().any(|m| m.path == "dep.tish"),
|
|
551
|
+
"re-exported dep should be discovered and loaded"
|
|
552
|
+
);
|
|
553
|
+
detect_cycles_virtual(&modules).unwrap();
|
|
554
|
+
let program = merge_modules_virtual(modules).unwrap();
|
|
555
|
+
let has_add_fn = program
|
|
556
|
+
.statements
|
|
557
|
+
.iter()
|
|
558
|
+
.any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "add"));
|
|
559
|
+
let binds_plus_to_add = program.statements.iter().any(|s| {
|
|
560
|
+
matches!(
|
|
561
|
+
s,
|
|
562
|
+
Statement::VarDecl { name, init: Some(Expr::Ident { name: src, .. }), .. }
|
|
563
|
+
if name.as_ref() == "plus" && src.as_ref() == "add"
|
|
564
|
+
)
|
|
565
|
+
});
|
|
566
|
+
assert!(has_add_fn, "dep's `add` fn should be in the merged bundle");
|
|
567
|
+
assert!(
|
|
568
|
+
binds_plus_to_add,
|
|
569
|
+
"import of re-exported `plus` should bind to dep's `add`"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
474
572
|
}
|
|
@@ -25,6 +25,10 @@ smallvec = "1"
|
|
|
25
25
|
indexmap = "2"
|
|
26
26
|
# Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
|
|
27
27
|
itoa = "1"
|
|
28
|
+
# Fast shortest-float→decimal for the number→string general path (JSON/template/log). ryu writes the
|
|
29
|
+
# shortest round-trip digits straight to a buffer with none of `core::fmt`'s `{:e}` Formatter overhead
|
|
30
|
+
# (profiled as ~40% of JSON.stringify self-time on numeric payloads). Already in the workspace lock.
|
|
31
|
+
ryu = "1"
|
|
28
32
|
fancy-regex = { version = "0.17.0", optional = true }
|
|
29
33
|
# Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
|
|
30
34
|
# single atomic with no pthread syscall — profiled as a top cost on object/array access.
|