@tishlang/tish 2.8.0 → 2.10.1

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.
@@ -4,7 +4,10 @@ mod tests {
4
4
 
5
5
  use tishlang_parser::parse;
6
6
 
7
- use crate::{compile_project_with_jsx, compile_with_jsx};
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() {
@@ -455,4 +458,354 @@ fn factory() {
455
458
  let neg = crate::compile(&parse("console.log(-1 / 0)\n").unwrap(), true).unwrap();
456
459
  assert!(neg.contains("-Infinity"), "-1/0 must emit -Infinity:\n{neg}");
457
460
  }
461
+
462
+ // ── #282: ESM module output (one file per module, real import/export) ──────────────────────
463
+
464
+ /// Write a set of `(relative_path, source)` modules into a fresh temp dir and compile the entry
465
+ /// in ESM mode. Returns the emitted modules keyed for easy lookup by their output relative path.
466
+ fn build_esm(entry: &str, modules: &[(&str, &str)]) -> Vec<EmittedJsModule> {
467
+ let tmp = tempfile::tempdir().expect("tempdir");
468
+ let dir = tmp.path();
469
+ for (rel, src) in modules {
470
+ let p = dir.join(rel);
471
+ if let Some(parent) = p.parent() {
472
+ std::fs::create_dir_all(parent).unwrap();
473
+ }
474
+ let mut f = std::fs::File::create(&p).unwrap();
475
+ f.write_all(src.as_bytes()).unwrap();
476
+ f.sync_all().unwrap();
477
+ }
478
+ compile_project_esm(&dir.join(entry), Some(dir), false).expect("compile_project_esm failed")
479
+ }
480
+
481
+ fn module_js<'a>(mods: &'a [EmittedJsModule], rel: &str) -> &'a str {
482
+ mods.iter()
483
+ .find(|m| m.relative_path.to_string_lossy() == rel)
484
+ .map(|m| m.js.as_str())
485
+ .unwrap_or_else(|| panic!("module {rel} not emitted; got {:?}", mods.iter().map(|m| m.relative_path.display().to_string()).collect::<Vec<_>>()))
486
+ }
487
+
488
+ #[test]
489
+ fn esm_emits_named_and_default_exports() {
490
+ let mods = build_esm(
491
+ "main.tish",
492
+ &[(
493
+ "main.tish",
494
+ "export const VERSION = \"1.0\"\nexport fn greet(n) { return \"hi \" + n }\nexport default greet\n",
495
+ )],
496
+ );
497
+ let js = module_js(&mods, "main.js");
498
+ assert!(js.contains("export const VERSION = \"1.0\";"), "named const export:\n{js}");
499
+ assert!(js.contains("export function greet "), "named fn export:\n{js}");
500
+ assert!(js.contains("export default greet;"), "default export:\n{js}");
501
+ }
502
+
503
+ #[test]
504
+ fn esm_rewrites_import_specifier_to_js_with_alias() {
505
+ let mods = build_esm(
506
+ "main.tish",
507
+ &[
508
+ ("dep.tish", "export const ssrH = 42\nexport fn greet(n) { return n }\n"),
509
+ (
510
+ "main.tish",
511
+ "import { ssrH as h, greet } from \"./dep.tish\"\nimport * as M from \"./dep.tish\"\nconsole.log(h)\nconsole.log(greet(M.ssrH))\n",
512
+ ),
513
+ ],
514
+ );
515
+ let js = module_js(&mods, "main.js");
516
+ assert!(
517
+ js.contains("import { ssrH as h, greet } from \"./dep.js\";"),
518
+ "named import with alias, .tish->.js:\n{js}"
519
+ );
520
+ assert!(js.contains("import * as M from \"./dep.js\";"), "namespace import:\n{js}");
521
+ }
522
+
523
+ #[test]
524
+ fn esm_one_file_per_module_preserves_tree() {
525
+ let mods = build_esm(
526
+ "main.tish",
527
+ &[
528
+ ("lib/util.tish", "export fn id(x) { return x }\n"),
529
+ ("main.tish", "import { id } from \"./lib/util.tish\"\nconsole.log(id(1))\n"),
530
+ ],
531
+ );
532
+ // Nested module keeps its relative path; importer points at the nested `.js`.
533
+ let _ = module_js(&mods, "lib/util.js");
534
+ let main = module_js(&mods, "main.js");
535
+ assert!(
536
+ main.contains("from \"./lib/util.js\";"),
537
+ "nested relative import preserved:\n{main}"
538
+ );
539
+ }
540
+
541
+ #[test]
542
+ fn esm_module_outside_project_root_is_emitted() {
543
+ // #282 follow-up: a dependency in a *sibling* package (outside the entry's project root)
544
+ // must still be emitted — the output tree is rooted at the directory common to all modules.
545
+ let tmp = tempfile::tempdir().expect("tempdir");
546
+ let base = tmp.path();
547
+ std::fs::create_dir_all(base.join("app/src")).unwrap();
548
+ std::fs::create_dir_all(base.join("lib")).unwrap();
549
+ std::fs::write(base.join("lib/util.tish"), "export fn id(x) { return x }\n").unwrap();
550
+ std::fs::write(
551
+ base.join("app/src/main.tish"),
552
+ "import { id } from \"../../lib/util.tish\"\nconsole.log(id(1))\n",
553
+ )
554
+ .unwrap();
555
+ // Project root is the entry's package (`app`); `lib/util.tish` lives outside it.
556
+ let mods = compile_project_esm(
557
+ &base.join("app/src/main.tish"),
558
+ Some(&base.join("app")),
559
+ false,
560
+ )
561
+ .expect("compile_project_esm failed for sibling dep");
562
+ let rels: Vec<String> = mods
563
+ .iter()
564
+ .map(|m| m.relative_path.to_string_lossy().replace('\\', "/"))
565
+ .collect();
566
+ let main_js = mods
567
+ .iter()
568
+ .find(|m| m.relative_path.to_string_lossy().replace('\\', "/").ends_with("app/src/main.js"))
569
+ .map(|m| m.js.clone());
570
+ assert!(
571
+ rels.iter().any(|r| r.ends_with("lib/util.js")),
572
+ "sibling dep emitted under common base: {:?}",
573
+ rels
574
+ );
575
+ assert!(
576
+ rels.iter().any(|r| r.ends_with("app/src/main.js")),
577
+ "entry emitted under its own subtree: {:?}",
578
+ rels
579
+ );
580
+ let main_js = main_js.expect("entry module present");
581
+ assert!(
582
+ main_js.contains("from \"../../lib/util.js\";"),
583
+ "relative import to sibling rewritten to .js:\n{main_js}"
584
+ );
585
+ }
586
+
587
+ #[test]
588
+ fn esm_bare_node_modules_dep_is_emitted() {
589
+ // #282 follow-up: a bare specifier resolved from `node_modules` (like `lattish`) is emitted
590
+ // into the output tree and the importer points at it with a relative `.js` specifier.
591
+ let tmp = tempfile::tempdir().expect("tempdir");
592
+ let base = tmp.path();
593
+ std::fs::create_dir_all(base.join("src")).unwrap();
594
+ std::fs::create_dir_all(base.join("node_modules/pkg")).unwrap();
595
+ std::fs::write(
596
+ base.join("node_modules/pkg/package.json"),
597
+ "{\"name\":\"pkg\",\"main\":\"index.tish\"}\n",
598
+ )
599
+ .unwrap();
600
+ std::fs::write(
601
+ base.join("node_modules/pkg/index.tish"),
602
+ "export fn ping() { return \"pong\" }\n",
603
+ )
604
+ .unwrap();
605
+ std::fs::write(
606
+ base.join("src/main.tish"),
607
+ "import { ping } from \"pkg\"\nconsole.log(ping())\n",
608
+ )
609
+ .unwrap();
610
+ let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false)
611
+ .expect("compile_project_esm failed for node_modules dep");
612
+ let rels: Vec<String> = mods
613
+ .iter()
614
+ .map(|m| m.relative_path.to_string_lossy().replace('\\', "/"))
615
+ .collect();
616
+ let main_js = mods
617
+ .iter()
618
+ .find(|m| m.relative_path.to_string_lossy().replace('\\', "/").ends_with("src/main.js"))
619
+ .map(|m| m.js.clone());
620
+ assert!(
621
+ rels.iter().any(|r| r.ends_with("node_modules/pkg/index.js")),
622
+ "node_modules dep emitted: {:?}",
623
+ rels
624
+ );
625
+ let main_js = main_js.expect("entry module present");
626
+ assert!(
627
+ main_js.contains("from \"../node_modules/pkg/index.js\";"),
628
+ "bare specifier rewritten to relative .js path:\n{main_js}"
629
+ );
630
+ }
631
+
632
+ #[test]
633
+ fn esm_rejects_native_imports() {
634
+ let tmp = tempfile::tempdir().expect("tempdir");
635
+ let dir = tmp.path();
636
+ let p = dir.join("main.tish");
637
+ 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();
639
+ assert!(
640
+ err.message.contains("Native module import") && err.message.contains("esm"),
641
+ "expected a native-import rejection for ESM, got: {}",
642
+ err.message
643
+ );
644
+ }
645
+
646
+ // ── #284: single-module compile for Vite dev (in-graph HMR) ───────────────────────────────
647
+
648
+ /// Write `modules` into a fresh temp dir and compile just `entry` (no graph resolution),
649
+ /// in Vite-dev import-rewrite mode without a source map.
650
+ fn build_module_vite(entry: &str, modules: &[(&str, &str)]) -> String {
651
+ let tmp = tempfile::tempdir().expect("tempdir");
652
+ let dir = tmp.path();
653
+ for (rel, src) in modules {
654
+ let p = dir.join(rel);
655
+ if let Some(parent) = p.parent() {
656
+ std::fs::create_dir_all(parent).unwrap();
657
+ }
658
+ let mut f = std::fs::File::create(&p).unwrap();
659
+ f.write_all(src.as_bytes()).unwrap();
660
+ f.sync_all().unwrap();
661
+ }
662
+ compile_module_esm(
663
+ &dir.join(entry),
664
+ Some(dir),
665
+ false,
666
+ ImportRewrite::ViteDev,
667
+ false,
668
+ )
669
+ .expect("compile_module_esm failed")
670
+ .js
671
+ }
672
+
673
+ #[test]
674
+ fn compile_module_esm_vite_preserves_relative_tish_imports() {
675
+ let js = build_module_vite(
676
+ "main.tish",
677
+ &[(
678
+ "main.tish",
679
+ "import { id } from \"./dep.tish\"\nconsole.log(id(1))\n",
680
+ )],
681
+ );
682
+ assert!(
683
+ js.contains("import { id } from \"./dep.tish\";"),
684
+ "Vite dev keeps the .tish specifier so resolveId re-enters the plugin:\n{js}"
685
+ );
686
+ assert!(
687
+ !js.contains("./dep.js"),
688
+ "Vite dev must NOT rewrite .tish to .js:\n{js}"
689
+ );
690
+ }
691
+
692
+ #[test]
693
+ fn compile_module_esm_vite_leaves_bare_specifiers_unchanged() {
694
+ let js = build_module_vite(
695
+ "main.tish",
696
+ &[(
697
+ "main.tish",
698
+ "import { h } from \"lattish\"\nconsole.log(h)\n",
699
+ )],
700
+ );
701
+ assert!(
702
+ js.contains("import { h } from \"lattish\";"),
703
+ "bare specifier passes through for normal Node/Vite resolution:\n{js}"
704
+ );
705
+ }
706
+
707
+ #[test]
708
+ fn compile_module_esm_vite_emits_exports() {
709
+ let js = build_module_vite(
710
+ "main.tish",
711
+ &[(
712
+ "main.tish",
713
+ "export const VERSION = \"1.0\"\nexport fn greet(n) { return \"hi \" + n }\nexport default greet\n",
714
+ )],
715
+ );
716
+ assert!(js.contains("export const VERSION = \"1.0\";"), "named const export:\n{js}");
717
+ assert!(js.contains("export function greet "), "named fn export:\n{js}");
718
+ assert!(js.contains("export default greet;"), "default export:\n{js}");
719
+ }
720
+
721
+ #[test]
722
+ fn compile_module_esm_does_not_resolve_graph() {
723
+ // Compiling a single module must not read or require its dependencies to exist on disk:
724
+ // only the entry file is created, yet the import to a missing `./dep.tish` compiles fine.
725
+ let js = build_module_vite(
726
+ "main.tish",
727
+ &[(
728
+ "main.tish",
729
+ "import { id } from \"./dep.tish\"\nconsole.log(id(1))\n",
730
+ )],
731
+ );
732
+ assert!(
733
+ js.contains("import { id } from \"./dep.tish\";"),
734
+ "single-module compile emits the import without loading the dependency:\n{js}"
735
+ );
736
+ }
737
+
738
+ #[test]
739
+ fn compile_module_esm_rejects_native_imports() {
740
+ let tmp = tempfile::tempdir().expect("tempdir");
741
+ let dir = tmp.path();
742
+ let p = dir.join("main.tish");
743
+ 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)
745
+ .unwrap_err();
746
+ assert!(
747
+ err.message.contains("Native module import"),
748
+ "expected a native-import rejection, got: {}",
749
+ err.message
750
+ );
751
+ }
752
+
753
+ #[test]
754
+ fn compile_module_esm_source_map_maps_statement_to_tish_file() {
755
+ let tmp = tempfile::tempdir().expect("tempdir");
756
+ let dir = tmp.path();
757
+ let p = dir.join("main.tish");
758
+ std::fs::write(&p, "export fn greet(n) { return \"hi \" + n }\nconsole.log(greet(\"x\"))\n")
759
+ .unwrap();
760
+ let bundle =
761
+ compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true).unwrap();
762
+ let map = bundle
763
+ .source_map_json
764
+ .expect("source map requested → must be present");
765
+ assert!(
766
+ map.contains("\"version\":3") || map.contains("\"version\": 3"),
767
+ "v3 source map:\n{map}"
768
+ );
769
+ assert!(
770
+ map.contains("main.tish"),
771
+ "map sources reference the .tish file:\n{map}"
772
+ );
773
+ }
774
+
775
+ #[test]
776
+ fn compile_module_esm_source_map_embeds_sources_content() {
777
+ // The map must carry sourcesContent so Vite/devtools never resolve `sources` from disk
778
+ // (otherwise Vite warns "Sourcemap ... points to missing source files").
779
+ let tmp = tempfile::tempdir().expect("tempdir");
780
+ let dir = tmp.path();
781
+ let p = dir.join("main.tish");
782
+ let src = "export fn greet(n) { return \"hi \" + n }\n";
783
+ std::fs::write(&p, src).unwrap();
784
+ let bundle =
785
+ compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true).unwrap();
786
+ let map = bundle.source_map_json.expect("source map requested");
787
+ assert!(
788
+ map.contains("\"sourcesContent\""),
789
+ "map must include a sourcesContent field:\n{map}"
790
+ );
791
+ assert!(
792
+ map.contains("export fn greet"),
793
+ "sourcesContent must embed the original .tish source:\n{map}"
794
+ );
795
+ }
796
+
797
+ #[test]
798
+ fn compile_module_esm_source_map_requires_no_optimize() {
799
+ let tmp = tempfile::tempdir().expect("tempdir");
800
+ let dir = tmp.path();
801
+ let p = dir.join("main.tish");
802
+ std::fs::write(&p, "console.log(1)\n").unwrap();
803
+ let err = compile_module_esm(&p, Some(dir), true, ImportRewrite::ViteDev, true)
804
+ .unwrap_err();
805
+ assert!(
806
+ err.message.to_lowercase().contains("optimiz"),
807
+ "source map + optimize must be rejected, got: {}",
808
+ err.message
809
+ );
810
+ }
458
811
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "2.8.0",
3
+ "version": "2.10.1",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, build to native or other targets.",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file