visaljs 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/README.md +17 -1
  2. package/cli.js +194 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,9 +9,24 @@ visaljs build app.js -o app
9
9
  ./app
10
10
  ```
11
11
 
12
- Needs Node 22 or later and a C compiler (`cc`, `gcc` or `clang`). Linux and macOS for now.
12
+ Needs Node 22 or later and a C compiler (`cc`, `gcc` or `clang`). Linux and macOS for now; on Windows, run it under WSL.
13
13
  The C runtime is built on first use and cached in `~/.cache/visaljs`.
14
14
 
15
+ `visaljs build` reports each step as it goes:
16
+
17
+ ```
18
+ ✓ 🔍 Detected GCC 13.3.0 10ms
19
+ ✓ 📦 Rolled 6 modules into a single file (63.7 kB) 37ms
20
+ ✓ 🌳 Parsed 1,988 lines of JavaScript 60ms
21
+ ✓ 🧩 Compiled 1 regular expression 6ms
22
+ ✓ 🔧 Converted 272 functions to IR and optimized them 46ms
23
+ ✓ 📝 Emitted 21,951 lines of C code (1.2 MB) 39ms
24
+ ✓ 📚 Runtime libraries ready (cached ⚡) 39ms
25
+ ✓ 🔨 Compiled the binary (-O2, stripped) 6.57s
26
+
27
+ 🎉 Built srv in 6.85s: 1.2 MB, fits on a floppy disk 💾
28
+ ```
29
+
15
30
  ```
16
31
  usage: visaljs build <entry.js> [options]
17
32
 
@@ -25,6 +40,7 @@ options:
25
40
  --all-builtins install every built-in, not only those the program names
26
41
  --no-bundle skip the bundler: the input is one ES module (a bundle)
27
42
  --script compile as a classic script (sloppy mode, no bundler)
43
+ -q, --quiet don't report each step of the build
28
44
  --version print the version
29
45
  --help print this help
30
46
  ```
package/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { isBuiltin } from "node:module";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  import ts from "typescript";
7
7
  import { rolldown } from "rolldown";
8
- import { execFileSync, spawnSync } from "node:child_process";
8
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
9
9
  import { createHash } from "node:crypto";
10
10
  import { availableParallelism, homedir, tmpdir } from "node:os";
11
11
  //#region \0rolldown/runtime.js
@@ -7470,7 +7470,8 @@ async function bundle(entry, hosts, warn) {
7470
7470
  const chunk = output[0];
7471
7471
  return {
7472
7472
  code: chunk.code,
7473
- map: chunk.map?.toString() ?? ""
7473
+ map: chunk.map?.toString() ?? "",
7474
+ modules: chunk.moduleIds.filter((id) => !id.startsWith("\0")).length
7474
7475
  };
7475
7476
  } finally {
7476
7477
  await build.close();
@@ -7531,7 +7532,10 @@ function formatLog(log, kind) {
7531
7532
  //#region cli/src/cc.ts
7532
7533
  /** $CC, else the first of the usual C compilers on the PATH. */
7533
7534
  function findCompiler() {
7534
- if (process.env.CC) return process.env.CC;
7535
+ if (process.env.CC) {
7536
+ if (!compilerVersion(process.env.CC)) throw new Error(`$CC (${process.env.CC}) doesn't run`);
7537
+ return process.env.CC;
7538
+ }
7535
7539
  for (const cc of [
7536
7540
  "cc",
7537
7541
  "clang",
@@ -7552,6 +7556,16 @@ const COMMON = [
7552
7556
  ];
7553
7557
  const LINK = process.platform === "darwin" ? ["-Wl,-dead_strip"] : ["-Wl,--gc-sections"];
7554
7558
  const STRIP = process.platform === "darwin" ? ["-Wl,-S,-x"] : ["-s"];
7559
+ /** "GCC 13.3.0", "clang 18.1.3": the compiler's name and version, else its command. */
7560
+ function describeCompiler(cc) {
7561
+ const text = compilerVersion(cc);
7562
+ const first = text.split("\n")[0] ?? "";
7563
+ const version = (/version (\d+\.\d+(?:\.\d+)?)/.exec(first) ?? /(\d+\.\d+(?:\.\d+)?)\S*\s*$/.exec(first))?.[1];
7564
+ if (!version) return cc;
7565
+ if (/clang/i.test(text)) return `${/apple/i.test(text) ? "Apple clang" : "clang"} ${version}`;
7566
+ if (/gcc|free software foundation/i.test(text)) return `GCC ${version}`;
7567
+ return `${cc} ${version}`;
7568
+ }
7555
7569
  function compilerVersion(cc) {
7556
7570
  try {
7557
7571
  return execFileSync(cc, ["--version"], { encoding: "utf8" });
@@ -7559,8 +7573,9 @@ function compilerVersion(cc) {
7559
7573
  return "";
7560
7574
  }
7561
7575
  }
7562
- /** Builds (or reuses) lib<name>.a for this toolchain; returns its path. */
7563
- function libraryArchive(tc, lib) {
7576
+ /** Builds (or reuses) lib<name>.a for this toolchain; returns its path. `building` hears
7577
+ * about a library that isn't cached, before it's built. */
7578
+ function libraryArchive(tc, lib, building) {
7564
7579
  const hash = createHash("sha256");
7565
7580
  hash.update(`${tc.cc}\n${compilerVersion(tc.cc)}\n${[
7566
7581
  ...COMMON,
@@ -7573,6 +7588,7 @@ function libraryArchive(tc, lib) {
7573
7588
  const dir = join(root, `${lib.name}-${key}`);
7574
7589
  const archive = join(dir, `lib${lib.name}.a`);
7575
7590
  if (existsSync(archive)) return archive;
7591
+ building?.(lib.name);
7576
7592
  const work = join(root, `tmp-${process.pid}-${Date.now()}`);
7577
7593
  mkdirSync(work, { recursive: true });
7578
7594
  try {
@@ -7636,11 +7652,6 @@ function compileAll(commands) {
7636
7652
  ]
7637
7653
  });
7638
7654
  }
7639
- /** The runtime's archives for this toolchain, in link order: libjsrt.a, then libbearssl.a
7640
- * (the test262 host links them too). */
7641
- function runtimeArchives(tc) {
7642
- return runtimeLibraries().map((l) => libraryArchive(tc, l));
7643
- }
7644
7655
  function cacheRoot() {
7645
7656
  return process.env.VISALJS_CACHE ?? join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "visaljs");
7646
7657
  }
@@ -7680,12 +7691,17 @@ function regexpCompiler(cc) {
7680
7691
  renameSync(tmp, tool);
7681
7692
  return tool;
7682
7693
  }
7683
- /** Compiles program.c and links it with the host packages' libraries, then libjsrt (which they
7684
- * use). The linker takes only what the program reaches: a program without I/O links no libuv.
7685
- * The libraries keep their debug info (-g); stripping, when asked, happens at this link. */
7686
- function compileProgram(tc, cFile, output, hosts, extraFlags = []) {
7687
- const archives = [...hosts.flatMap((h) => h.libraries(RUNTIME_INCLUDE)).filter((l) => l.sources.length).map((l) => libraryArchive(tc, l)), ...runtimeArchives(tc)];
7688
- execFileSync(tc.cc, [
7694
+ /** A program's archives, in link order: the host packages' libraries, then libjsrt (which they
7695
+ * use). Each is built the first time; `building` hears about it. */
7696
+ function programArchives(tc, hosts, building) {
7697
+ return [...hosts.flatMap((h) => h.libraries(RUNTIME_INCLUDE)).filter((l) => l.sources.length), ...runtimeLibraries()].map((l) => libraryArchive(tc, l, building));
7698
+ }
7699
+ /** Compiles program.c and links it with `archives` (programArchives). The linker takes only
7700
+ * what the program reaches: a program without I/O links no libuv. The libraries keep their
7701
+ * debug info (-g); stripping, when asked, happens at this link. Asynchronous: the build's
7702
+ * report keeps drawing while the compiler runs. */
7703
+ function compileProgram(tc, cFile, output, archives, extraFlags = []) {
7704
+ const args = [
7689
7705
  ...COMMON,
7690
7706
  ...tc.opt,
7691
7707
  ...extraFlags,
@@ -7697,11 +7713,14 @@ function compileProgram(tc, cFile, output, hosts, extraFlags = []) {
7697
7713
  ...tc.strip ? STRIP : [],
7698
7714
  "-o",
7699
7715
  output
7700
- ], { stdio: [
7701
- "ignore",
7702
- "inherit",
7703
- "inherit"
7704
- ] });
7716
+ ];
7717
+ return new Promise((resolve, reject) => {
7718
+ spawn(tc.cc, args, { stdio: [
7719
+ "ignore",
7720
+ "inherit",
7721
+ "inherit"
7722
+ ] }).on("error", reject).on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`${tc.cc} exited with code ${code}`)));
7723
+ });
7705
7724
  }
7706
7725
  function scratchDir() {
7707
7726
  const dir = join(tmpdir(), `visaljs-${process.pid}-${Date.now()}`);
@@ -7758,8 +7777,115 @@ function compileRegExps(cc, literals) {
7758
7777
  return bytecode;
7759
7778
  }
7760
7779
  //#endregion
7780
+ //#region cli/src/report.ts
7781
+ const CLEAR = "\r\x1B[2K";
7782
+ const FRAMES = [
7783
+ "⠋",
7784
+ "⠙",
7785
+ "⠹",
7786
+ "⠸",
7787
+ "⠼",
7788
+ "⠴",
7789
+ "⠦",
7790
+ "⠧",
7791
+ "⠇",
7792
+ "⠏"
7793
+ ];
7794
+ var Report = class {
7795
+ #write;
7796
+ #style;
7797
+ #start = performance.now();
7798
+ #running;
7799
+ #timer;
7800
+ #frame = 0;
7801
+ /** No style: reports nothing (--quiet, and callers that capture the CLI's output). */
7802
+ constructor(write, style) {
7803
+ this.#write = write;
7804
+ this.#style = style;
7805
+ }
7806
+ /** Runs one step: `label` while it runs, then `done(result)`. A failed step is crossed out
7807
+ * and its error rethrown, for the caller to print. The spinner turns only while the event
7808
+ * loop is free: an asynchronous step. */
7809
+ async step(icon, label, run, done) {
7810
+ this.#running = {
7811
+ icon,
7812
+ label,
7813
+ since: performance.now()
7814
+ };
7815
+ if (this.#live) {
7816
+ this.#draw();
7817
+ this.#timer = setInterval(() => (this.#frame++, this.#draw()), 80);
7818
+ this.#timer.unref();
7819
+ }
7820
+ let result;
7821
+ try {
7822
+ result = await run();
7823
+ } catch (e) {
7824
+ this.#end(this.#paint("31", `✗ ${label}`));
7825
+ throw e;
7826
+ }
7827
+ this.#end(`${this.#paint("32", "✓")} ${icon} ${done(result)}`);
7828
+ return result;
7829
+ }
7830
+ /** Renames the running step (what it turned out to be doing). */
7831
+ relabel(label) {
7832
+ if (!this.#running) return;
7833
+ this.#running.label = label;
7834
+ if (this.#live) this.#draw();
7835
+ }
7836
+ /** A message from inside a step (a bundler warning): printed even when quiet. */
7837
+ note(text) {
7838
+ if (this.#live && this.#running) this.#write(CLEAR);
7839
+ this.#write(`${text}\n`);
7840
+ if (this.#live && this.#running) this.#draw();
7841
+ }
7842
+ /** The last line, given the whole build's time. */
7843
+ finish(icon, text) {
7844
+ if (this.#style) this.#write(`\n ${icon} ${this.#paint("1", text(since(this.#start)))}\n`);
7845
+ }
7846
+ get #live() {
7847
+ return this.#style?.live === true;
7848
+ }
7849
+ #paint(code, text) {
7850
+ return this.#style?.color ? `\x1b[${code}m${text}\x1b[0m` : text;
7851
+ }
7852
+ #draw() {
7853
+ const { icon, label } = this.#running;
7854
+ this.#write(`${CLEAR} ${this.#paint("36", FRAMES[this.#frame % FRAMES.length])} ${icon} ${this.#paint("2", `${label}…`)}`);
7855
+ }
7856
+ #end(line) {
7857
+ clearInterval(this.#timer);
7858
+ const start = this.#running.since;
7859
+ this.#running = void 0;
7860
+ if (this.#style) this.#write(`${this.#live ? CLEAR : ""} ${line} ${this.#paint("2", since(start))}\n`);
7861
+ }
7862
+ };
7863
+ function since(start) {
7864
+ const ms = performance.now() - start;
7865
+ return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(2)}s`;
7866
+ }
7867
+ /** 383.2 kB, 1.2 MB (decimal units). */
7868
+ function size(bytes) {
7869
+ if (bytes < 1e3) return `${bytes} B`;
7870
+ return bytes < 1e6 ? `${(bytes / 1e3).toFixed(1)} kB` : `${(bytes / 1e6).toFixed(1)} MB`;
7871
+ }
7872
+ /** The binary's size, put in scale. */
7873
+ function sizeQuip(bytes) {
7874
+ const FLOPPY = 1474560;
7875
+ if (bytes <= 65536) return "small enough for a Commodore 64 🕹️";
7876
+ if (bytes <= FLOPPY) return "fits on a floppy disk 💾";
7877
+ return `${Math.ceil(bytes / FLOPPY)} floppy disks' worth 💾`;
7878
+ }
7879
+ /** "1 module", "1,204 lines". */
7880
+ const count = (n, one, many = `${one}s`) => `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
7881
+ function lineCount(text) {
7882
+ let n = text.length > 0 && !text.endsWith("\n") ? 1 : 0;
7883
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) n++;
7884
+ return n;
7885
+ }
7886
+ //#endregion
7761
7887
  //#region cli/src/main.ts
7762
- const VERSION = "0.0.1";
7888
+ const VERSION = "0.0.2";
7763
7889
  const USAGE = `usage: visaljs build <entry.js> [options]
7764
7890
 
7765
7891
  options:
@@ -7772,6 +7898,7 @@ options:
7772
7898
  --all-builtins install every built-in, not only those the program names
7773
7899
  --no-bundle skip the bundler: the input is one ES module (a bundle)
7774
7900
  --script compile as a classic script (sloppy mode, no bundler)
7901
+ -q, --quiet don't report each step of the build
7775
7902
  --version print the version
7776
7903
  --help print this help
7777
7904
  `;
@@ -7795,6 +7922,10 @@ async function main(argv, io) {
7795
7922
  "all-builtins": { type: "boolean" },
7796
7923
  "no-bundle": { type: "boolean" },
7797
7924
  script: { type: "boolean" },
7925
+ quiet: {
7926
+ type: "boolean",
7927
+ short: "q"
7928
+ },
7798
7929
  version: { type: "boolean" },
7799
7930
  help: {
7800
7931
  type: "boolean",
@@ -7815,6 +7946,10 @@ async function main(argv, io) {
7815
7946
  io.stdout(USAGE);
7816
7947
  return values.help ? 0 : 2;
7817
7948
  }
7949
+ if ((io.platform ?? process.platform) === "win32") {
7950
+ io.stderr("visaljs: Windows is not supported yet; use Linux or macOS (on Windows, WSL works)\n");
7951
+ return 1;
7952
+ }
7818
7953
  const [command, ...rest] = positionals;
7819
7954
  switch (command) {
7820
7955
  case "build":
@@ -7836,13 +7971,23 @@ async function build(entry, options, io) {
7836
7971
  io.stderr(`visaljs: cannot read ${entry}: ${e.message}\n`);
7837
7972
  return 1;
7838
7973
  }
7974
+ const dump = options["dump-ast"] === true || options["dump-ir"] === true;
7975
+ const report = new Report(io.stderr, options.quiet || dump ? void 0 : io.report);
7976
+ const binary = !dump && !options["emit-c"];
7839
7977
  const hosts = DEFAULT_HOSTS;
7840
7978
  const primitives = hostPrimitives(hosts);
7979
+ let cc;
7980
+ if (binary) try {
7981
+ cc = await report.step("🔍", "Detecting a C compiler", findCompiler, (cc) => `Detected ${describeCompiler(cc)}${process.env.CC ? " (from $CC)" : ""}`);
7982
+ } catch (e) {
7983
+ io.stderr(`visaljs: ${e.message}\n`);
7984
+ return 1;
7985
+ }
7841
7986
  const script = options.script === true;
7842
7987
  const bundled = !script && options["no-bundle"] !== true;
7843
7988
  let map;
7844
7989
  if (bundled) try {
7845
- const b = await bundle(entry, hosts, (message) => io.stderr(`${message}\n`));
7990
+ const b = await report.step("📦", "Rolling the modules into a single file", () => bundle(entry, hosts, (message) => report.note(message)), (b) => `Rolled ${count(b.modules, "module")} into a single file (${size(Buffer.byteLength(b.code))})`);
7846
7991
  source = b.code;
7847
7992
  if (b.map) map = new SourceMap(b.map, displayPath);
7848
7993
  } catch (e) {
@@ -7852,30 +7997,30 @@ async function build(entry, options, io) {
7852
7997
  }
7853
7998
  let c;
7854
7999
  try {
7855
- const ast = script ? parseScript(source, entry) : parseModule(source, map ? "bundle.js" : entry, map, resolve(entry), primitives);
8000
+ const ast = await report.step("🌳", "Parsing the JavaScript", () => script ? parseScript(source, entry) : parseModule(source, map ? "bundle.js" : entry, map, resolve(entry), primitives), () => `Parsed ${count(lineCount(source), "line")} of JavaScript`);
7856
8001
  if (options["dump-ast"]) {
7857
8002
  io.stdout(JSON.stringify(ast, null, 2) + "\n");
7858
8003
  return 0;
7859
8004
  }
7860
8005
  const info = analyze(ast);
7861
8006
  const literals = regexpLiterals(ast);
7862
- const regexps = literals.length ? compileRegExps(findCompiler(), literals) : /* @__PURE__ */ new Map();
8007
+ const regexps = literals.length ? await report.step("🧩", "Compiling the regular expressions", () => compileRegExps(cc ??= findCompiler(), literals), () => `Compiled ${count(literals.length, "regular expression")}`) : /* @__PURE__ */ new Map();
7863
8008
  const unsupported = firstUnsupported(ast);
7864
8009
  if (unsupported) throw unsupported;
7865
- const ir = optimize(lower(ast, info));
8010
+ const ir = await report.step("🔧", "Converting to IR and optimizing", () => optimize(lower(ast, info)), (ir) => `Converted ${count(ir.functions.length + 1, "function")} to IR and optimized them`);
7866
8011
  if (options["dump-ir"]) {
7867
8012
  io.stdout(printModule(ir));
7868
8013
  return 0;
7869
8014
  }
7870
8015
  const names = programNames(ast);
7871
8016
  const all = options["all-builtins"] === true;
7872
- c = emitProgram(ir, {
8017
+ c = await report.step("📝", "Emitting C code", () => emitProgram(ir, {
7873
8018
  embedSource: options["embed-source"] === true,
7874
8019
  components: (all ? COMPONENTS : reachableComponents(names)).map((c) => c.symbol),
7875
8020
  core: all ? void 0 : namedCore(names),
7876
8021
  regexps,
7877
8022
  hostPrimitives: primitives
7878
- });
8023
+ }), (c) => `Emitted ${count(lineCount(c), "line")} of C code (${size(Buffer.byteLength(c))})`);
7879
8024
  } catch (e) {
7880
8025
  if (!(e instanceof CompileError)) throw e;
7881
8026
  io.stderr(`${e.message}\n`);
@@ -7885,18 +8030,25 @@ async function build(entry, options, io) {
7885
8030
  mkdirSync(options["emit-c"], { recursive: true });
7886
8031
  writeFileSync(join(options["emit-c"], "program.c"), c);
7887
8032
  if (bundled) writeFileSync(join(options["emit-c"], "bundle.js"), source);
8033
+ report.finish("📂", (time) => `Wrote ${displayPath(resolve(options["emit-c"], "program.c"))} in ${time}`);
7888
8034
  return 0;
7889
8035
  }
7890
8036
  const output = options.output ?? basename(entry).replace(/\.[cm]?js$/, "");
8037
+ const tc = {
8038
+ cc,
8039
+ opt: options.debug ? ["-O0"] : ["-O2"],
8040
+ strip: !options.debug
8041
+ };
7891
8042
  const dir = scratchDir();
7892
8043
  try {
7893
8044
  const file = join(dir, "program.c");
7894
8045
  writeFileSync(file, c);
7895
- compileProgram({
7896
- cc: findCompiler(),
7897
- opt: options.debug ? ["-O0"] : ["-O2"],
7898
- strip: !options.debug
7899
- }, file, output, hosts);
8046
+ const built = [];
8047
+ const archives = await report.step("📚", "Preparing the runtime libraries", () => programArchives(tc, hosts, (name) => {
8048
+ built.push(`lib${name}`);
8049
+ report.relabel(`Building lib${name} (first build only: it's cached after)`);
8050
+ }), () => built.length ? `Built ${built.join(", ")} (cached for next time)` : "Runtime libraries ready (cached ⚡)");
8051
+ await report.step("🔨", `Compiling with ${describeCompiler(tc.cc)}`, () => compileProgram(tc, file, output, archives), () => `Compiled the binary (${tc.opt.join(" ")}, ${tc.strip ? "stripped" : "with debug info"})`);
7900
8052
  } catch (e) {
7901
8053
  io.stderr(`visaljs: C compilation failed: ${e.message}\n`);
7902
8054
  return 1;
@@ -7906,6 +8058,8 @@ async function build(entry, options, io) {
7906
8058
  force: true
7907
8059
  });
7908
8060
  }
8061
+ const bytes = statSync(output).size;
8062
+ report.finish("🎉", (time) => `Built ${displayPath(resolve(output))} in ${time}: ${size(bytes)}, ${sizeQuip(bytes)}`);
7909
8063
  return 0;
7910
8064
  }
7911
8065
  /** How diagnostics and #line name an original file: relative to the working directory when
@@ -7917,9 +8071,14 @@ function displayPath(source) {
7917
8071
  }
7918
8072
  //#endregion
7919
8073
  //#region cli/src/cli.ts
8074
+ const tty = process.stderr.isTTY === true;
7920
8075
  process.exitCode = await main(process.argv.slice(2), {
7921
8076
  stdout: (text) => process.stdout.write(text),
7922
- stderr: (text) => process.stderr.write(text)
8077
+ stderr: (text) => process.stderr.write(text),
8078
+ report: {
8079
+ live: tty,
8080
+ color: tty && !process.env.NO_COLOR && process.env.TERM !== "dumb"
8081
+ }
7923
8082
  });
7924
8083
  //#endregion
7925
8084
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visaljs",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Ahead-of-time JavaScript to C compiler: the command line",
5
5
  "license": "MIT",
6
6
  "repository": {