@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 CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "2.9.0"
3
+ version = "2.12.0"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -59,6 +59,7 @@ tishlang_runtime = { path = "../tish_runtime", version = ">=0.1" }
59
59
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
60
60
  tishlang_js_to_tish = { path = "../js_to_tish", version = ">=0.1" }
61
61
  clap = { version = "4.6.0", features = ["derive", "color"] }
62
+ serde_json = "1"
62
63
  tishlang_pg = { path = "../tish_pg", version = ">=0.1", optional = true }
63
64
  mimalloc = { version = "0.1", optional = true }
64
65
 
@@ -563,6 +563,14 @@ pub(crate) struct BuildArgs {
563
563
  /// For `--target js`: `bundle` (default) merges all modules into one file; `esm` emits one `.js` per `.tish` module with real `import`/`export` (so `-o` is a directory) for Vite/Rollup tree-shaking.
564
564
  #[arg(long, default_value = "bundle", value_name = "FORMAT", help_heading = "Options")]
565
565
  pub format: String,
566
+ /// For `--target js --format esm`: package the JSX runtime (`h`/`Fragment`) is auto-imported from when a module uses JSX without importing them (default `lattish`).
567
+ #[arg(
568
+ long = "jsx-import-source",
569
+ default_value = "lattish",
570
+ value_name = "PKG",
571
+ help_heading = "Options"
572
+ )]
573
+ pub jsx_import_source: String,
566
574
  /// Run the gradual type checker: `warn` prints `line:col` type diagnostics; `error` also fails the build on them. (Equivalent to setting `TISH_CHECK`.)
567
575
  #[arg(long, value_name = "MODE", help_heading = "Options")]
568
576
  pub check: Option<String>,
@@ -571,6 +579,42 @@ pub(crate) struct BuildArgs {
571
579
  pub file: String,
572
580
  }
573
581
 
582
+ #[derive(Parser)]
583
+ pub(crate) struct CompileModuleArgs {
584
+ /// `--target js` only (the single-module ESM path is JS-specific).
585
+ #[arg(long, default_value = "js", value_name = "TARGET", help_heading = "Options")]
586
+ pub target: String,
587
+ /// `--format esm` only (one ES module per file).
588
+ #[arg(long, default_value = "esm", value_name = "FORMAT", help_heading = "Options")]
589
+ pub format: String,
590
+ /// Keep relative `.tish` specifiers and bare packages verbatim so Vite's resolveId/load re-enters the plugin per module (in-graph HMR). Omit for disk-style `.tish`->`.js` rewriting.
591
+ #[arg(long, help_heading = "Options")]
592
+ pub vite_dev: bool,
593
+ /// Emit a v3 source map back to the `.tish` source as a `{ "js", "map" }` JSON envelope on stdout (on by default; implies no optimization).
594
+ #[arg(long, help_heading = "Options")]
595
+ pub source_map: bool,
596
+ /// Disable the source map; print raw ES module JS to stdout instead of the JSON envelope.
597
+ #[arg(long = "no-source-map", help_heading = "Options")]
598
+ pub no_source_map: bool,
599
+ /// Project root for resolving bare specifiers / `node_modules` (defaults to the file's parent).
600
+ #[arg(long, value_name = "DIR", help_heading = "Options")]
601
+ pub project_root: Option<String>,
602
+ /// Disable AST optimizations (forced on when a source map is emitted).
603
+ #[arg(long, help_heading = "Options")]
604
+ pub no_optimize: bool,
605
+ /// Package the JSX runtime (`h`/`Fragment`) is auto-imported from when a module uses JSX without importing them (default `lattish`).
606
+ #[arg(
607
+ long = "jsx-import-source",
608
+ default_value = "lattish",
609
+ value_name = "PKG",
610
+ help_heading = "Options"
611
+ )]
612
+ pub jsx_import_source: String,
613
+ /// Entry `.tish` file to compile (only this file is read; dependencies are left to the bundler).
614
+ #[arg(required = true, value_name = "FILE", help_heading = "Arguments")]
615
+ pub file: String,
616
+ }
617
+
574
618
  #[derive(Subcommand)]
575
619
  pub(crate) enum Commands {
576
620
  /// Run a Tish file (interpret)
@@ -579,6 +623,9 @@ pub(crate) enum Commands {
579
623
  Repl(ReplArgs),
580
624
  /// Build native binary, wasm, wasi, or JavaScript output
581
625
  Build(BuildArgs),
626
+ /// Compile a single `.tish` module to one ES module (for Vite dev / HMR); prints to stdout
627
+ #[command(name = "compile-module")]
628
+ CompileModule(CompileModuleArgs),
582
629
  /// Parse and dump AST
583
630
  #[command(name = "dump-ast")]
584
631
  DumpAst {
@@ -22,7 +22,7 @@ use tishlang_core::VmRef;
22
22
  use clap::FromArgMatches;
23
23
  use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
24
24
 
25
- use cli_help::{Cli, Commands};
25
+ use cli_help::{Cli, Commands, CompileModuleArgs};
26
26
 
27
27
  /// Normalize `--feature` / `--feature http,timers,fs` / `--feature full` for VM runs and native builds.
28
28
  fn normalize_capability_flags(features: &[String]) -> HashSet<String> {
@@ -73,7 +73,7 @@ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
73
73
  fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
74
74
  if argv.len() >= 2 {
75
75
  let first = argv[1].as_str();
76
- const SUBCOMMANDS: &[&str] = &["run", "repl", "build", "dump-ast"];
76
+ const SUBCOMMANDS: &[&str] = &["run", "repl", "build", "compile-module", "dump-ast"];
77
77
  let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.contains(&first);
78
78
  if looks_like_file {
79
79
  argv.insert(1, "run".to_string());
@@ -137,8 +137,10 @@ fn main() {
137
137
  a.ios_triple.as_deref(),
138
138
  &a.crate_type,
139
139
  &a.format,
140
+ &a.jsx_import_source,
140
141
  )
141
142
  }
143
+ Some(Commands::CompileModule(a)) => compile_module(&a),
142
144
  Some(Commands::DumpAst {
143
145
  file,
144
146
  ignore_indent,
@@ -543,6 +545,7 @@ fn compile_to_js(
543
545
  optimize: bool,
544
546
  source_map: bool,
545
547
  format: &str,
548
+ jsx_import_source: &str,
546
549
  ) -> Result<(), String> {
547
550
  if format != "bundle" && format != "esm" {
548
551
  return Err(format!(
@@ -573,7 +576,14 @@ fn compile_to_js(
573
576
  }
574
577
  });
575
578
  if format == "esm" {
576
- return compile_to_js_esm(input_path, output_path, optimize, source_map, project_root);
579
+ return compile_to_js_esm(
580
+ input_path,
581
+ output_path,
582
+ optimize,
583
+ source_map,
584
+ project_root,
585
+ jsx_import_source,
586
+ );
577
587
  }
578
588
  let out_path = Path::new(output_path);
579
589
  let out_path = if out_path.extension().is_none()
@@ -655,6 +665,7 @@ fn compile_to_js_esm(
655
665
  optimize: bool,
656
666
  source_map: bool,
657
667
  project_root: Option<&Path>,
668
+ jsx_import_source: &str,
658
669
  ) -> Result<(), String> {
659
670
  if source_map {
660
671
  return Err(
@@ -670,8 +681,9 @@ fn compile_to_js_esm(
670
681
  .into(),
671
682
  );
672
683
  }
673
- let modules = tishlang_compile_js::compile_project_esm(input_path, project_root, optimize)
674
- .map_err(|e| format!("{}", e))?;
684
+ let modules =
685
+ tishlang_compile_js::compile_project_esm(input_path, project_root, optimize, jsx_import_source)
686
+ .map_err(|e| format!("{}", e))?;
675
687
  let out_dir = Path::new(output_path);
676
688
  fs::create_dir_all(out_dir)
677
689
  .map_err(|e| format!("Cannot create output directory {}: {}", out_dir.display(), e))?;
@@ -703,6 +715,72 @@ fn compile_to_js_esm(
703
715
  Ok(())
704
716
  }
705
717
 
718
+ /// `tish compile-module FILE` (#284): compile a single `.tish` file to one ES module and print it
719
+ /// to stdout. This is the fast, in-graph path the Vite plugin shells out to in `load()` — only the
720
+ /// one file is read (no dependency graph resolution), so Vite owns the module graph and can HMR a
721
+ /// leaf module without a full reload.
722
+ ///
723
+ /// With a source map (default), stdout is a JSON envelope `{"js":…,"map":…}` so the plugin gets
724
+ /// both artifacts from one spawn; with `--no-source-map`, stdout is raw ES module JS.
725
+ fn compile_module(args: &CompileModuleArgs) -> Result<(), String> {
726
+ if args.target != "js" {
727
+ return Err(format!(
728
+ "tish compile-module only supports --target js (got '{}').",
729
+ args.target
730
+ ));
731
+ }
732
+ if args.format != "esm" {
733
+ return Err(format!(
734
+ "tish compile-module only supports --format esm (got '{}').",
735
+ args.format
736
+ ));
737
+ }
738
+ let input_path = Path::new(&args.file)
739
+ .canonicalize()
740
+ .map_err(|e| format!("Cannot resolve {}: {}", args.file, e))?;
741
+ if input_path.extension().map(|e| e != "tish").unwrap_or(true) {
742
+ return Err(format!(
743
+ "tish compile-module expects a .tish file (got '{}').",
744
+ args.file
745
+ ));
746
+ }
747
+ let want_map = !args.no_source_map;
748
+ // A source map needs unmerged statement order, so it forces no optimization (same rule as
749
+ // `build --target js --source-map`).
750
+ let optimize = !args.no_optimize && !want_map;
751
+ let import_rewrite = if args.vite_dev {
752
+ tishlang_compile_js::ImportRewrite::ViteDev
753
+ } else {
754
+ tishlang_compile_js::ImportRewrite::Disk
755
+ };
756
+ let project_root = args
757
+ .project_root
758
+ .as_ref()
759
+ .map(PathBuf::from)
760
+ .or_else(|| input_path.parent().map(Path::to_path_buf));
761
+ let bundle = tishlang_compile_js::compile_module_esm(
762
+ &input_path,
763
+ project_root.as_deref(),
764
+ optimize,
765
+ import_rewrite,
766
+ want_map,
767
+ &args.jsx_import_source,
768
+ )
769
+ .map_err(|e| format!("{}", e))?;
770
+
771
+ if let Some(map_json) = bundle.source_map_json {
772
+ let map: serde_json::Value = serde_json::from_str(&map_json)
773
+ .map_err(|e| format!("Internal: source map is not valid JSON: {}", e))?;
774
+ let envelope = serde_json::json!({ "js": bundle.js, "map": map });
775
+ let line = serde_json::to_string(&envelope)
776
+ .map_err(|e| format!("Cannot serialize compile-module output: {}", e))?;
777
+ println!("{}", line);
778
+ } else {
779
+ print!("{}", bundle.js);
780
+ }
781
+ Ok(())
782
+ }
783
+
706
784
  #[allow(clippy::vec_init_then_push, clippy::too_many_arguments)] // build_file maps CLI build flags 1:1
707
785
  fn build_file(
708
786
  input_path: &str,
@@ -715,6 +793,7 @@ fn build_file(
715
793
  ios_triple: Option<&str>,
716
794
  crate_type: &str,
717
795
  format: &str,
796
+ jsx_import_source: &str,
718
797
  ) -> Result<(), String> {
719
798
  let optimize = !no_optimize;
720
799
  let input_path = Path::new(input_path)
@@ -724,7 +803,14 @@ fn build_file(
724
803
  let is_js = input_path.extension().map(|e| e == "js") == Some(true);
725
804
 
726
805
  if target == "js" {
727
- return compile_to_js(&input_path, output_path, optimize, source_map, format);
806
+ return compile_to_js(
807
+ &input_path,
808
+ output_path,
809
+ optimize,
810
+ source_map,
811
+ format,
812
+ jsx_import_source,
813
+ );
728
814
  }
729
815
 
730
816
  // `wasm-gpu` (#277): same pipeline as `wasm` but builds the `--features gpu` WebGPU runtime and
@@ -738,6 +738,183 @@ fn test_js_esm_export_collision() {
738
738
  }
739
739
  }
740
740
 
741
+ /// #284: `tish compile-module --vite-dev --source-map` compiles a single `.tish` file (no graph
742
+ /// resolution) to one ES module, keeps relative `.tish` specifiers, and emits a `{ js, map }` JSON
743
+ /// envelope whose map references the `.tish` source. This is the in-graph path the Vite plugin uses.
744
+ #[test]
745
+ fn test_compile_module_vite_dev_with_source_map() {
746
+ let bin = tish_bin();
747
+ if !bin.exists() {
748
+ return;
749
+ }
750
+ let main = workspace_root()
751
+ .join("tests")
752
+ .join("modules")
753
+ .join("esm")
754
+ .join("vite_hmr")
755
+ .join("main.tish");
756
+ if !main.exists() {
757
+ return;
758
+ }
759
+ let out = Command::new(&bin)
760
+ .args(["compile-module"])
761
+ .arg(&main)
762
+ .args([
763
+ "--target",
764
+ "js",
765
+ "--format",
766
+ "esm",
767
+ "--vite-dev",
768
+ "--source-map",
769
+ ])
770
+ .current_dir(workspace_root())
771
+ .output()
772
+ .expect("run compile-module");
773
+ assert!(
774
+ out.status.success(),
775
+ "compile-module failed: stderr={}",
776
+ String::from_utf8_lossy(&out.stderr)
777
+ );
778
+ let stdout = String::from_utf8_lossy(&out.stdout);
779
+ let parsed: serde_json::Value =
780
+ serde_json::from_str(&stdout).unwrap_or_else(|e| panic!("stdout is not JSON: {e}\n{stdout}"));
781
+ let js = parsed["js"].as_str().expect("envelope has string `js`");
782
+ assert!(
783
+ js.contains("import { makeCounter } from \"./counter.tish\";"),
784
+ "Vite-dev keeps the .tish specifier:\n{js}"
785
+ );
786
+ assert!(
787
+ !js.contains("./counter.js"),
788
+ "Vite-dev must not rewrite .tish to .js:\n{js}"
789
+ );
790
+ let map = &parsed["map"];
791
+ assert!(map.is_object(), "envelope has object `map`: {map}");
792
+ assert_eq!(map["version"], serde_json::json!(3), "v3 source map");
793
+ let sources = map["sources"].as_array().expect("map has sources");
794
+ assert!(
795
+ sources
796
+ .iter()
797
+ .any(|s| s.as_str().map(|s| s.contains("main.tish")).unwrap_or(false)),
798
+ "map sources reference the .tish file: {sources:?}"
799
+ );
800
+ }
801
+
802
+ /// #284: without `--source-map`, `compile-module` prints raw ES module JS to stdout (no envelope),
803
+ /// so the output can be consumed directly.
804
+ #[test]
805
+ fn test_compile_module_no_source_map_prints_raw_js() {
806
+ let bin = tish_bin();
807
+ if !bin.exists() {
808
+ return;
809
+ }
810
+ let counter = workspace_root()
811
+ .join("tests")
812
+ .join("modules")
813
+ .join("esm")
814
+ .join("vite_hmr")
815
+ .join("counter.tish");
816
+ if !counter.exists() {
817
+ return;
818
+ }
819
+ let out = Command::new(&bin)
820
+ .args(["compile-module"])
821
+ .arg(&counter)
822
+ .args([
823
+ "--target",
824
+ "js",
825
+ "--format",
826
+ "esm",
827
+ "--vite-dev",
828
+ "--no-source-map",
829
+ ])
830
+ .current_dir(workspace_root())
831
+ .output()
832
+ .expect("run compile-module");
833
+ assert!(
834
+ out.status.success(),
835
+ "compile-module failed: stderr={}",
836
+ String::from_utf8_lossy(&out.stderr)
837
+ );
838
+ let stdout = String::from_utf8_lossy(&out.stdout);
839
+ assert!(
840
+ stdout.contains("export function makeCounter"),
841
+ "raw JS output includes the export:\n{stdout}"
842
+ );
843
+ assert!(
844
+ !stdout.trim_start().starts_with('{'),
845
+ "no-source-map output is raw JS, not a JSON envelope:\n{stdout}"
846
+ );
847
+ }
848
+
849
+ /// #291: `compile-module` auto-imports the JSX runtime (`h` / `Fragment`) when a module uses JSX
850
+ /// but doesn't import them itself, so per-module ESM output doesn't throw `ReferenceError` at load.
851
+ /// `--jsx-import-source` overrides the runtime package.
852
+ #[test]
853
+ fn test_compile_module_jsx_auto_imports_runtime() {
854
+ let bin = tish_bin();
855
+ if !bin.exists() {
856
+ return;
857
+ }
858
+ let widget = workspace_root()
859
+ .join("tests")
860
+ .join("modules")
861
+ .join("esm")
862
+ .join("vite_hmr")
863
+ .join("widget.tish");
864
+ if !widget.exists() {
865
+ return;
866
+ }
867
+ // Default runtime source (`lattish`), Vite-dev so the specifier stays bare.
868
+ let out = Command::new(&bin)
869
+ .args(["compile-module"])
870
+ .arg(&widget)
871
+ .args([
872
+ "--target",
873
+ "js",
874
+ "--format",
875
+ "esm",
876
+ "--vite-dev",
877
+ "--no-source-map",
878
+ ])
879
+ .current_dir(workspace_root())
880
+ .output()
881
+ .expect("run compile-module");
882
+ assert!(
883
+ out.status.success(),
884
+ "compile-module failed: stderr={}",
885
+ String::from_utf8_lossy(&out.stderr)
886
+ );
887
+ let stdout = String::from_utf8_lossy(&out.stdout);
888
+ assert!(
889
+ stdout.contains("import { h, Fragment } from \"lattish\";"),
890
+ "JSX module with a fragment auto-imports both h and Fragment:\n{stdout}"
891
+ );
892
+
893
+ // Custom runtime source via --jsx-import-source.
894
+ let out2 = Command::new(&bin)
895
+ .args(["compile-module"])
896
+ .arg(&widget)
897
+ .args([
898
+ "--target",
899
+ "js",
900
+ "--format",
901
+ "esm",
902
+ "--vite-dev",
903
+ "--no-source-map",
904
+ "--jsx-import-source",
905
+ "@tishlang/lattish",
906
+ ])
907
+ .current_dir(workspace_root())
908
+ .output()
909
+ .expect("run compile-module");
910
+ assert!(out2.status.success());
911
+ let stdout2 = String::from_utf8_lossy(&out2.stdout);
912
+ assert!(
913
+ stdout2.contains("from \"@tishlang/lattish\";"),
914
+ "--jsx-import-source overrides the runtime package:\n{stdout2}"
915
+ );
916
+ }
917
+
741
918
  /// Ignored: tishlang_eval::run() does not run the event loop.
742
919
  #[test]
743
920
  #[cfg(feature = "http")]