@tishlang/tish 2.9.0 → 2.12.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/bin/tish +0 -0
- package/crates/tish/Cargo.toml +2 -1
- package/crates/tish/src/cli_help.rs +47 -0
- package/crates/tish/src/main.rs +92 -6
- package/crates/tish/tests/integration_test.rs +177 -0
- package/crates/tish_compile/src/codegen.rs +255 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +139 -0
- package/crates/tish_compile_js/src/codegen.rs +161 -3
- package/crates/tish_compile_js/src/lib.rs +7 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +341 -4
- package/crates/tish_ui/src/jsx.rs +250 -0
- 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.
|
|
@@ -20,16 +24,31 @@ enum EmitMode {
|
|
|
20
24
|
Esm,
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
/// How ESM import specifiers are rewritten. `Disk` (production `--format esm`) rewrites `.tish`
|
|
28
|
+
/// specifiers to their sibling `.js` output paths and resolves bare specifiers to relative paths
|
|
29
|
+
/// into the emitted module tree. `ViteDev` keeps relative `.tish` specifiers and bare specifiers
|
|
30
|
+
/// as-is so Vite's `resolveId`/`load` re-enters the plugin per module (in-graph HMR, issue #284).
|
|
31
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
32
|
+
pub enum ImportRewrite {
|
|
33
|
+
Disk,
|
|
34
|
+
ViteDev,
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
struct Codegen {
|
|
24
38
|
output: String,
|
|
25
39
|
indent: usize,
|
|
26
40
|
in_async: bool,
|
|
27
41
|
emit_mode: EmitMode,
|
|
42
|
+
/// ESM only: how import specifiers are rewritten (disk `.js` paths vs Vite-dev `.tish`).
|
|
43
|
+
import_rewrite: ImportRewrite,
|
|
28
44
|
/// ESM only: the absolute path of the module being emitted (the importer), used to rewrite
|
|
29
45
|
/// relative/bare import specifiers to sibling `.js` output paths.
|
|
30
46
|
module_path: PathBuf,
|
|
31
47
|
/// ESM only: the project root, so import targets can be located relative to the output tree.
|
|
32
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,
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
|
|
@@ -48,19 +67,28 @@ impl Codegen {
|
|
|
48
67
|
indent: 0,
|
|
49
68
|
in_async: false,
|
|
50
69
|
emit_mode: EmitMode::Bundle,
|
|
70
|
+
import_rewrite: ImportRewrite::Disk,
|
|
51
71
|
module_path: PathBuf::new(),
|
|
52
72
|
project_root: PathBuf::new(),
|
|
73
|
+
jsx_import_source: DEFAULT_JSX_IMPORT_SOURCE.to_string(),
|
|
53
74
|
}
|
|
54
75
|
}
|
|
55
76
|
|
|
56
|
-
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 {
|
|
57
83
|
Self {
|
|
58
84
|
output: String::new(),
|
|
59
85
|
indent: 0,
|
|
60
86
|
in_async: false,
|
|
61
87
|
emit_mode: EmitMode::Esm,
|
|
88
|
+
import_rewrite,
|
|
62
89
|
module_path,
|
|
63
90
|
project_root,
|
|
91
|
+
jsx_import_source,
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
|
|
@@ -125,6 +153,7 @@ impl Codegen {
|
|
|
125
153
|
map_builder: Option<&mut SourceMapBuilder>,
|
|
126
154
|
) -> Result<(), CompileError> {
|
|
127
155
|
self.write("// Generated by tishlang_compile_js\n");
|
|
156
|
+
self.emit_jsx_runtime_auto_import(program)?;
|
|
128
157
|
match (map_sources, map_builder) {
|
|
129
158
|
(Some((srcs, root)), Some(sm)) => {
|
|
130
159
|
for (i, stmt) in program.statements.iter().enumerate() {
|
|
@@ -162,6 +191,34 @@ impl Codegen {
|
|
|
162
191
|
Ok(())
|
|
163
192
|
}
|
|
164
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
|
+
|
|
165
222
|
fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
166
223
|
match stmt {
|
|
167
224
|
Statement::Block { statements, .. } => {
|
|
@@ -907,7 +964,12 @@ impl Codegen {
|
|
|
907
964
|
specifiers: &[ImportSpecifier],
|
|
908
965
|
from: &str,
|
|
909
966
|
) -> Result<(), CompileError> {
|
|
910
|
-
let spec = rewrite_import_to_js(
|
|
967
|
+
let spec = rewrite_import_to_js(
|
|
968
|
+
from,
|
|
969
|
+
&self.module_path,
|
|
970
|
+
&self.project_root,
|
|
971
|
+
self.import_rewrite,
|
|
972
|
+
)?;
|
|
911
973
|
let mut named: Vec<String> = Vec::new();
|
|
912
974
|
let mut default_local: Option<String> = None;
|
|
913
975
|
let mut namespace_local: Option<String> = None;
|
|
@@ -987,6 +1049,7 @@ fn rewrite_import_to_js(
|
|
|
987
1049
|
spec: &str,
|
|
988
1050
|
importer_abs: &Path,
|
|
989
1051
|
project_root: &Path,
|
|
1052
|
+
rewrite: ImportRewrite,
|
|
990
1053
|
) -> Result<String, CompileError> {
|
|
991
1054
|
if tishlang_compile::is_native_import(spec) {
|
|
992
1055
|
return Err(CompileError {
|
|
@@ -996,6 +1059,11 @@ fn rewrite_import_to_js(
|
|
|
996
1059
|
),
|
|
997
1060
|
});
|
|
998
1061
|
}
|
|
1062
|
+
// Vite dev keeps specifiers verbatim: relative `.tish` paths and bare packages are left for the
|
|
1063
|
+
// plugin's `resolveId`/`load` (relative) or Node/Vite resolution (bare) to handle per-module.
|
|
1064
|
+
if rewrite == ImportRewrite::ViteDev {
|
|
1065
|
+
return Ok(spec.to_string());
|
|
1066
|
+
}
|
|
999
1067
|
if spec.starts_with("./") || spec.starts_with("../") {
|
|
1000
1068
|
return Ok(spec_ext_to_js(spec));
|
|
1001
1069
|
}
|
|
@@ -1210,6 +1278,7 @@ pub fn compile_project_esm(
|
|
|
1210
1278
|
entry_path: &Path,
|
|
1211
1279
|
project_root: Option<&Path>,
|
|
1212
1280
|
optimize: bool,
|
|
1281
|
+
jsx_import_source: &str,
|
|
1213
1282
|
) -> Result<Vec<EmittedJsModule>, CompileError> {
|
|
1214
1283
|
let modules = tishlang_compile::resolve_project(entry_path, project_root)
|
|
1215
1284
|
.map_err(|e| CompileError { message: e })?;
|
|
@@ -1249,7 +1318,12 @@ pub fn compile_project_esm(
|
|
|
1249
1318
|
} else {
|
|
1250
1319
|
module.program.clone()
|
|
1251
1320
|
};
|
|
1252
|
-
let mut gen = Codegen::new_esm(
|
|
1321
|
+
let mut gen = Codegen::new_esm(
|
|
1322
|
+
mod_canon.clone(),
|
|
1323
|
+
res_root_canon.clone(),
|
|
1324
|
+
ImportRewrite::Disk,
|
|
1325
|
+
jsx_import_source.to_string(),
|
|
1326
|
+
);
|
|
1253
1327
|
gen.emit_program(&program, None, None)?;
|
|
1254
1328
|
out.push(EmittedJsModule {
|
|
1255
1329
|
relative_path: rel_js,
|
|
@@ -1259,6 +1333,90 @@ pub fn compile_project_esm(
|
|
|
1259
1333
|
Ok(out)
|
|
1260
1334
|
}
|
|
1261
1335
|
|
|
1336
|
+
/// Compile a **single** `.tish` module to one ES module (issue #284, Vite dev / HMR). Unlike
|
|
1337
|
+
/// [`compile_project_esm`], the dependency graph is **not** resolved — only `module_path` is read
|
|
1338
|
+
/// and parsed — so a Vite plugin can compile one file per `load()` and let Vite own the module
|
|
1339
|
+
/// graph. With `ImportRewrite::ViteDev` relative `.tish` specifiers and bare packages are preserved
|
|
1340
|
+
/// so Vite re-enters the plugin per dependency. When `source_map` is set, a v3 map back to the
|
|
1341
|
+
/// `.tish` source is returned (requires `optimize == false`, matching the bundle source-map rule).
|
|
1342
|
+
pub fn compile_module_esm(
|
|
1343
|
+
module_path: &Path,
|
|
1344
|
+
project_root: Option<&Path>,
|
|
1345
|
+
optimize: bool,
|
|
1346
|
+
import_rewrite: ImportRewrite,
|
|
1347
|
+
source_map: bool,
|
|
1348
|
+
jsx_import_source: &str,
|
|
1349
|
+
) -> Result<JsBundle, CompileError> {
|
|
1350
|
+
if source_map && optimize {
|
|
1351
|
+
return Err(CompileError {
|
|
1352
|
+
message: "source map requires no optimization (mappings follow unmerged statement order)."
|
|
1353
|
+
.into(),
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
let source = std::fs::read_to_string(module_path).map_err(|e| CompileError {
|
|
1357
|
+
message: format!("Cannot read {}: {}", module_path.display(), e),
|
|
1358
|
+
})?;
|
|
1359
|
+
let parsed = tishlang_parser::parse(&source).map_err(|e| CompileError {
|
|
1360
|
+
message: format!("Parse error in {}: {}", module_path.display(), e),
|
|
1361
|
+
})?;
|
|
1362
|
+
let program = if optimize {
|
|
1363
|
+
tishlang_opt::optimize(&parsed)
|
|
1364
|
+
} else {
|
|
1365
|
+
parsed
|
|
1366
|
+
};
|
|
1367
|
+
let module_canon = module_path
|
|
1368
|
+
.canonicalize()
|
|
1369
|
+
.unwrap_or_else(|_| module_path.to_path_buf());
|
|
1370
|
+
let root = project_root
|
|
1371
|
+
.map(Path::to_path_buf)
|
|
1372
|
+
.or_else(|| module_path.parent().map(Path::to_path_buf))
|
|
1373
|
+
.unwrap_or_else(|| PathBuf::from("."));
|
|
1374
|
+
let root_canon = root.canonicalize().unwrap_or(root);
|
|
1375
|
+
|
|
1376
|
+
let mut gen = Codegen::new_esm(
|
|
1377
|
+
module_canon.clone(),
|
|
1378
|
+
root_canon.clone(),
|
|
1379
|
+
import_rewrite,
|
|
1380
|
+
jsx_import_source.to_string(),
|
|
1381
|
+
);
|
|
1382
|
+
if source_map {
|
|
1383
|
+
let stmt_sources = vec![module_canon.clone(); program.statements.len()];
|
|
1384
|
+
let mut builder = SourceMapBuilder::new(Some("module.js"));
|
|
1385
|
+
builder.set_source_root(Some(""));
|
|
1386
|
+
gen.emit_program(
|
|
1387
|
+
&program,
|
|
1388
|
+
Some((stmt_sources.as_slice(), root_canon.as_path())),
|
|
1389
|
+
Some(&mut builder),
|
|
1390
|
+
)?;
|
|
1391
|
+
let mut sm = builder.into_sourcemap();
|
|
1392
|
+
// Embed the original `.tish` so consumers (Vite, browser devtools) never resolve
|
|
1393
|
+
// `sources` from disk. The map's `sources` are project-root-relative, but Vite resolves
|
|
1394
|
+
// them against the module's own directory; without inline content it logs
|
|
1395
|
+
// "Sourcemap ... points to missing source files" and devtools can't show the original.
|
|
1396
|
+
let source_count = sm.get_source_count();
|
|
1397
|
+
for id in 0..source_count {
|
|
1398
|
+
sm.set_source_contents(id, Some(&source));
|
|
1399
|
+
}
|
|
1400
|
+
let mut v = Vec::new();
|
|
1401
|
+
sm.to_writer(&mut v).map_err(|e| CompileError {
|
|
1402
|
+
message: e.to_string(),
|
|
1403
|
+
})?;
|
|
1404
|
+
let map_json = String::from_utf8(v).map_err(|e| CompileError {
|
|
1405
|
+
message: e.to_string(),
|
|
1406
|
+
})?;
|
|
1407
|
+
Ok(JsBundle {
|
|
1408
|
+
js: gen.output,
|
|
1409
|
+
source_map_json: Some(map_json),
|
|
1410
|
+
})
|
|
1411
|
+
} else {
|
|
1412
|
+
gen.emit_program(&program, None, None)?;
|
|
1413
|
+
Ok(JsBundle {
|
|
1414
|
+
js: gen.output,
|
|
1415
|
+
source_map_json: None,
|
|
1416
|
+
})
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1262
1420
|
/// Deepest directory that is an ancestor of every given file path, compared component-wise. Used as
|
|
1263
1421
|
/// the base of the `--format esm` output tree so modules outside the entry's project root (sibling
|
|
1264
1422
|
/// packages, `node_modules`) still map to a stable, collision-free location. For a single file this
|
|
@@ -8,12 +8,16 @@ mod error;
|
|
|
8
8
|
mod tests_jsx;
|
|
9
9
|
|
|
10
10
|
pub use codegen::{
|
|
11
|
-
compile_project_esm, compile_project_with_jsx,
|
|
12
|
-
compile_with_jsx, EmittedJsModule,
|
|
11
|
+
compile_module_esm, compile_project_esm, compile_project_with_jsx,
|
|
12
|
+
compile_project_with_jsx_and_source_map, compile_with_jsx, EmittedJsModule, ImportRewrite,
|
|
13
|
+
JsBundle, DEFAULT_JSX_IMPORT_SOURCE,
|
|
13
14
|
};
|
|
14
15
|
pub use error::CompileError;
|
|
15
16
|
|
|
16
|
-
/// 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).
|
|
17
21
|
pub fn compile(program: &tishlang_ast::Program, optimize: bool) -> Result<String, CompileError> {
|
|
18
22
|
compile_with_jsx(program, optimize)
|
|
19
23
|
}
|
|
@@ -4,7 +4,10 @@ mod tests {
|
|
|
4
4
|
|
|
5
5
|
use tishlang_parser::parse;
|
|
6
6
|
|
|
7
|
-
use crate::{
|
|
7
|
+
use crate::{
|
|
8
|
+
compile_module_esm, compile_project_esm, compile_project_with_jsx, compile_with_jsx,
|
|
9
|
+
EmittedJsModule, ImportRewrite,
|
|
10
|
+
};
|
|
8
11
|
|
|
9
12
|
#[test]
|
|
10
13
|
fn lattish_jsx_emits_h_with_children_array() {
|
|
@@ -472,7 +475,8 @@ fn factory() {
|
|
|
472
475
|
f.write_all(src.as_bytes()).unwrap();
|
|
473
476
|
f.sync_all().unwrap();
|
|
474
477
|
}
|
|
475
|
-
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")
|
|
476
480
|
}
|
|
477
481
|
|
|
478
482
|
fn module_js<'a>(mods: &'a [EmittedJsModule], rel: &str) -> &'a str {
|
|
@@ -554,6 +558,7 @@ fn factory() {
|
|
|
554
558
|
&base.join("app/src/main.tish"),
|
|
555
559
|
Some(&base.join("app")),
|
|
556
560
|
false,
|
|
561
|
+
"lattish",
|
|
557
562
|
)
|
|
558
563
|
.expect("compile_project_esm failed for sibling dep");
|
|
559
564
|
let rels: Vec<String> = mods
|
|
@@ -604,7 +609,7 @@ fn factory() {
|
|
|
604
609
|
"import { ping } from \"pkg\"\nconsole.log(ping())\n",
|
|
605
610
|
)
|
|
606
611
|
.unwrap();
|
|
607
|
-
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")
|
|
608
613
|
.expect("compile_project_esm failed for node_modules dep");
|
|
609
614
|
let rels: Vec<String> = mods
|
|
610
615
|
.iter()
|
|
@@ -632,11 +637,343 @@ fn factory() {
|
|
|
632
637
|
let dir = tmp.path();
|
|
633
638
|
let p = dir.join("main.tish");
|
|
634
639
|
std::fs::write(&p, "import { readFile } from \"fs\"\nconsole.log(1)\n").unwrap();
|
|
635
|
-
let err = compile_project_esm(&p, Some(dir), false).unwrap_err();
|
|
640
|
+
let err = compile_project_esm(&p, Some(dir), false, "lattish").unwrap_err();
|
|
636
641
|
assert!(
|
|
637
642
|
err.message.contains("Native module import") && err.message.contains("esm"),
|
|
638
643
|
"expected a native-import rejection for ESM, got: {}",
|
|
639
644
|
err.message
|
|
640
645
|
);
|
|
641
646
|
}
|
|
647
|
+
|
|
648
|
+
// ── #284: single-module compile for Vite dev (in-graph HMR) ───────────────────────────────
|
|
649
|
+
|
|
650
|
+
/// Write `modules` into a fresh temp dir and compile just `entry` (no graph resolution),
|
|
651
|
+
/// in Vite-dev import-rewrite mode without a source map.
|
|
652
|
+
fn build_module_vite(entry: &str, modules: &[(&str, &str)]) -> String {
|
|
653
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
654
|
+
let dir = tmp.path();
|
|
655
|
+
for (rel, src) in modules {
|
|
656
|
+
let p = dir.join(rel);
|
|
657
|
+
if let Some(parent) = p.parent() {
|
|
658
|
+
std::fs::create_dir_all(parent).unwrap();
|
|
659
|
+
}
|
|
660
|
+
let mut f = std::fs::File::create(&p).unwrap();
|
|
661
|
+
f.write_all(src.as_bytes()).unwrap();
|
|
662
|
+
f.sync_all().unwrap();
|
|
663
|
+
}
|
|
664
|
+
compile_module_esm(
|
|
665
|
+
&dir.join(entry),
|
|
666
|
+
Some(dir),
|
|
667
|
+
false,
|
|
668
|
+
ImportRewrite::ViteDev,
|
|
669
|
+
false,
|
|
670
|
+
"lattish",
|
|
671
|
+
)
|
|
672
|
+
.expect("compile_module_esm failed")
|
|
673
|
+
.js
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
#[test]
|
|
677
|
+
fn compile_module_esm_vite_preserves_relative_tish_imports() {
|
|
678
|
+
let js = build_module_vite(
|
|
679
|
+
"main.tish",
|
|
680
|
+
&[(
|
|
681
|
+
"main.tish",
|
|
682
|
+
"import { id } from \"./dep.tish\"\nconsole.log(id(1))\n",
|
|
683
|
+
)],
|
|
684
|
+
);
|
|
685
|
+
assert!(
|
|
686
|
+
js.contains("import { id } from \"./dep.tish\";"),
|
|
687
|
+
"Vite dev keeps the .tish specifier so resolveId re-enters the plugin:\n{js}"
|
|
688
|
+
);
|
|
689
|
+
assert!(
|
|
690
|
+
!js.contains("./dep.js"),
|
|
691
|
+
"Vite dev must NOT rewrite .tish to .js:\n{js}"
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#[test]
|
|
696
|
+
fn compile_module_esm_vite_leaves_bare_specifiers_unchanged() {
|
|
697
|
+
let js = build_module_vite(
|
|
698
|
+
"main.tish",
|
|
699
|
+
&[(
|
|
700
|
+
"main.tish",
|
|
701
|
+
"import { h } from \"lattish\"\nconsole.log(h)\n",
|
|
702
|
+
)],
|
|
703
|
+
);
|
|
704
|
+
assert!(
|
|
705
|
+
js.contains("import { h } from \"lattish\";"),
|
|
706
|
+
"bare specifier passes through for normal Node/Vite resolution:\n{js}"
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#[test]
|
|
711
|
+
fn compile_module_esm_vite_emits_exports() {
|
|
712
|
+
let js = build_module_vite(
|
|
713
|
+
"main.tish",
|
|
714
|
+
&[(
|
|
715
|
+
"main.tish",
|
|
716
|
+
"export const VERSION = \"1.0\"\nexport fn greet(n) { return \"hi \" + n }\nexport default greet\n",
|
|
717
|
+
)],
|
|
718
|
+
);
|
|
719
|
+
assert!(js.contains("export const VERSION = \"1.0\";"), "named const export:\n{js}");
|
|
720
|
+
assert!(js.contains("export function greet "), "named fn export:\n{js}");
|
|
721
|
+
assert!(js.contains("export default greet;"), "default export:\n{js}");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
#[test]
|
|
725
|
+
fn compile_module_esm_does_not_resolve_graph() {
|
|
726
|
+
// Compiling a single module must not read or require its dependencies to exist on disk:
|
|
727
|
+
// only the entry file is created, yet the import to a missing `./dep.tish` compiles fine.
|
|
728
|
+
let js = build_module_vite(
|
|
729
|
+
"main.tish",
|
|
730
|
+
&[(
|
|
731
|
+
"main.tish",
|
|
732
|
+
"import { id } from \"./dep.tish\"\nconsole.log(id(1))\n",
|
|
733
|
+
)],
|
|
734
|
+
);
|
|
735
|
+
assert!(
|
|
736
|
+
js.contains("import { id } from \"./dep.tish\";"),
|
|
737
|
+
"single-module compile emits the import without loading the dependency:\n{js}"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
#[test]
|
|
742
|
+
fn compile_module_esm_rejects_native_imports() {
|
|
743
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
744
|
+
let dir = tmp.path();
|
|
745
|
+
let p = dir.join("main.tish");
|
|
746
|
+
std::fs::write(&p, "import { readFile } from \"fs\"\nconsole.log(1)\n").unwrap();
|
|
747
|
+
let err = compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, false, "lattish")
|
|
748
|
+
.unwrap_err();
|
|
749
|
+
assert!(
|
|
750
|
+
err.message.contains("Native module import"),
|
|
751
|
+
"expected a native-import rejection, got: {}",
|
|
752
|
+
err.message
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
#[test]
|
|
757
|
+
fn compile_module_esm_source_map_maps_statement_to_tish_file() {
|
|
758
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
759
|
+
let dir = tmp.path();
|
|
760
|
+
let p = dir.join("main.tish");
|
|
761
|
+
std::fs::write(&p, "export fn greet(n) { return \"hi \" + n }\nconsole.log(greet(\"x\"))\n")
|
|
762
|
+
.unwrap();
|
|
763
|
+
let bundle =
|
|
764
|
+
compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true, "lattish")
|
|
765
|
+
.unwrap();
|
|
766
|
+
let map = bundle
|
|
767
|
+
.source_map_json
|
|
768
|
+
.expect("source map requested → must be present");
|
|
769
|
+
assert!(
|
|
770
|
+
map.contains("\"version\":3") || map.contains("\"version\": 3"),
|
|
771
|
+
"v3 source map:\n{map}"
|
|
772
|
+
);
|
|
773
|
+
assert!(
|
|
774
|
+
map.contains("main.tish"),
|
|
775
|
+
"map sources reference the .tish file:\n{map}"
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
#[test]
|
|
780
|
+
fn compile_module_esm_source_map_embeds_sources_content() {
|
|
781
|
+
// The map must carry sourcesContent so Vite/devtools never resolve `sources` from disk
|
|
782
|
+
// (otherwise Vite warns "Sourcemap ... points to missing source files").
|
|
783
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
784
|
+
let dir = tmp.path();
|
|
785
|
+
let p = dir.join("main.tish");
|
|
786
|
+
let src = "export fn greet(n) { return \"hi \" + n }\n";
|
|
787
|
+
std::fs::write(&p, src).unwrap();
|
|
788
|
+
let bundle =
|
|
789
|
+
compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true, "lattish")
|
|
790
|
+
.unwrap();
|
|
791
|
+
let map = bundle.source_map_json.expect("source map requested");
|
|
792
|
+
assert!(
|
|
793
|
+
map.contains("\"sourcesContent\""),
|
|
794
|
+
"map must include a sourcesContent field:\n{map}"
|
|
795
|
+
);
|
|
796
|
+
assert!(
|
|
797
|
+
map.contains("export fn greet"),
|
|
798
|
+
"sourcesContent must embed the original .tish source:\n{map}"
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
#[test]
|
|
803
|
+
fn compile_module_esm_source_map_requires_no_optimize() {
|
|
804
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
805
|
+
let dir = tmp.path();
|
|
806
|
+
let p = dir.join("main.tish");
|
|
807
|
+
std::fs::write(&p, "console.log(1)\n").unwrap();
|
|
808
|
+
let err = compile_module_esm(&p, Some(dir), true, ImportRewrite::ViteDev, true, "lattish")
|
|
809
|
+
.unwrap_err();
|
|
810
|
+
assert!(
|
|
811
|
+
err.message.to_lowercase().contains("optimiz"),
|
|
812
|
+
"source map + optimize must be rejected, got: {}",
|
|
813
|
+
err.message
|
|
814
|
+
);
|
|
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
|
+
}
|
|
642
979
|
}
|