@monimesl/dot-cli 0.1.0-alpha.8 → 0.1.0-alpha.9

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 (57) hide show
  1. package/README.md +17 -9
  2. package/dist/cli.js +156 -67
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-DFOUQnLA.js → server-BfPAnsSf.js} +34 -81
  5. package/dist/{server-DFOUQnLA.js.map → server-BfPAnsSf.js.map} +1 -1
  6. package/dist/templates/showcase/.env +1 -1
  7. package/dist/templates/showcase/dot.ts +2 -0
  8. package/dist/templates/showcase/pages/cache/controller.ts +45 -0
  9. package/dist/templates/showcase/pages/cache/state.ts +17 -0
  10. package/dist/templates/showcase/pages/cache/view.dml +36 -0
  11. package/dist/templates/showcase/pages/carousel/controller.ts +33 -3
  12. package/dist/templates/showcase/pages/carousel/state.ts +61 -24
  13. package/dist/templates/showcase/pages/carousel/view.dml +18 -15
  14. package/dist/templates/showcase/pages/device/controller.ts +22 -12
  15. package/dist/templates/showcase/pages/device/state.ts +1 -1
  16. package/dist/templates/showcase/pages/device/view.dml +13 -8
  17. package/dist/templates/showcase/pages/feedback/controller.ts +6 -1
  18. package/dist/templates/showcase/pages/feedback/state.ts +1 -0
  19. package/dist/templates/showcase/pages/feedback/view.dml +23 -23
  20. package/dist/templates/showcase/pages/files/controller.ts +33 -14
  21. package/dist/templates/showcase/pages/files/state.ts +10 -2
  22. package/dist/templates/showcase/pages/files/view.dml +29 -14
  23. package/dist/templates/showcase/pages/filters/controller.ts +11 -12
  24. package/dist/templates/showcase/pages/filters/state.ts +107 -61
  25. package/dist/templates/showcase/pages/filters/view.dml +18 -17
  26. package/dist/templates/showcase/pages/forms/controller.ts +36 -12
  27. package/dist/templates/showcase/pages/forms/state.ts +25 -6
  28. package/dist/templates/showcase/pages/forms/view.dml +28 -18
  29. package/dist/templates/showcase/pages/images/controller.ts +26 -0
  30. package/dist/templates/showcase/pages/images/state.ts +3 -0
  31. package/dist/templates/showcase/pages/images/view.dml +18 -3
  32. package/dist/templates/showcase/pages/index/state.ts +26 -14
  33. package/dist/templates/showcase/pages/index/view.dml +2 -2
  34. package/dist/templates/showcase/pages/layout/controller.ts +36 -2
  35. package/dist/templates/showcase/pages/layout/state.ts +22 -2
  36. package/dist/templates/showcase/pages/layout/view.dml +45 -22
  37. package/dist/templates/showcase/pages/lists/controller.ts +12 -11
  38. package/dist/templates/showcase/pages/lists/state.ts +87 -66
  39. package/dist/templates/showcase/pages/lists/view.dml +15 -15
  40. package/dist/templates/showcase/pages/navigation/controller.ts +2 -1
  41. package/dist/templates/showcase/pages/navigation/view.dml +5 -5
  42. package/dist/templates/showcase/pages/network/controller.ts +68 -24
  43. package/dist/templates/showcase/pages/network/state.ts +13 -6
  44. package/dist/templates/showcase/pages/network/view.dml +29 -18
  45. package/dist/templates/showcase/pages/payments/controller.ts +16 -12
  46. package/dist/templates/showcase/pages/payments/state.ts +3 -0
  47. package/dist/templates/showcase/pages/payments/view.dml +22 -12
  48. package/dist/templates/showcase/pages/secrets/controller.ts +43 -0
  49. package/dist/templates/showcase/pages/secrets/state.ts +5 -0
  50. package/dist/templates/showcase/pages/secrets/view.dml +24 -0
  51. package/dist/templates/showcase/pages/state/state.ts +1 -1
  52. package/dist/templates/showcase/pages/state/view.dml +9 -9
  53. package/dist/templates/showcase/pages/text/controller.ts +3 -3
  54. package/dist/templates/showcase/pages/text/state.ts +6 -5
  55. package/dist/templates/showcase/pages/text/view.dml +22 -13
  56. package/dist/templates/showcase/statics/logo.png +0 -0
  57. package/package.json +6 -6
package/README.md CHANGED
@@ -8,19 +8,20 @@ dotctl create [dir] # scaffold a dot (asks for its name and template)
8
8
  dotctl create page <path> # add a page to the dot you are in
9
9
  dotctl build [root] # compile the dot into dist/ui and dist/logic, minified
10
10
  dotctl dev [root] # build, run it in the browser simulator, and reload it on each change
11
+ dotctl preview [root] # build it as `build` does, and run that build in the browser simulator
11
12
  dotctl keys show # print this computer's developer key: its public key and key ID
12
13
  dotctl lsp # the .dml language server, which editors start
13
14
  ```
14
15
 
15
- | Option | Applies to | What it does |
16
- | --------------------- | ---------- | ------------------------------------------------------------------------ |
17
- | `--out-dir <dir>` | both | Output directory (default: `<root>/dist`) |
18
- | `--conditions <list>` | both | Extra export conditions, such as `@monimesl/dot-source` |
19
- | `--port <port>` | `dev` | Simulator port (default: `3680`, or the next free one) |
20
- | `--device` | `dev` | Also serve the dot to the Dot example apps on phones, through a QR code |
21
- | `--name <name>` | `create` | The dot's name, instead of asking |
22
- | `--template <name>` | `create` | `starter` (one page to build on, the default) or `showcase` (every page) |
23
- | `--pm <name>` | `create` | The package manager to install with: npm, pnpm, yarn or bun |
16
+ | Option | Applies to | What it does |
17
+ | --------------------- | ------------------------- | ------------------------------------------------------------------------ |
18
+ | `--out-dir <dir>` | `build`, `dev`, `preview` | Output directory (default: `<root>/dist`) |
19
+ | `--conditions <list>` | `build`, `dev`, `preview` | Extra export conditions, such as `@monimesl/dot-source` |
20
+ | `--port <port>` | `dev`, `preview` | Simulator port (default: `3680`, or the next free one) |
21
+ | `--device` | `dev` | Also serve the dot to the Dot example apps on phones, through a QR code |
22
+ | `--name <name>` | `create` | The dot's name, instead of asking |
23
+ | `--template <name>` | `create` | `starter` (one page to build on, the default) or `showcase` (every page) |
24
+ | `--pm <name>` | `create` | The package manager to install with: npm, pnpm, yarn or bun |
24
25
 
25
26
  ## `dotctl create`
26
27
 
@@ -45,6 +46,13 @@ With `--device`, it also serves each build on the local network to the Dot examp
45
46
  QR code to scan with one, which then follows the dot in the same way: see
46
47
  [the example apps](../hosts/native/README.md#the-example-apps).
47
48
 
49
+ ## `dotctl preview`
50
+
51
+ Builds the dot as `dotctl build` does, and runs that build in the browser simulator: minified, and
52
+ with only the components its pages use, as the host app gets it. Use it to check a build before
53
+ publishing it. It doesn't rebuild on change: run it again after editing. The dot keeps the files it
54
+ saved under `dotctl dev`.
55
+
48
56
  ## Developer keys
49
57
 
50
58
  Dash opens only links that one of the signed-in developer's keys signed. `dotctl dev --device`
package/dist/cli.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
- import { createInterface } from "node:readline/promises";
5
- import { parseArgs } from "node:util";
4
+ import { parseArgs, styleText } from "node:util";
6
5
  import { CompileError, PAGES_DIR, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
7
6
  import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
8
7
  import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
@@ -14,6 +13,8 @@ import { renderUnicodeCompact } from "uqr";
14
13
  import { createHash, createHmac, createPrivateKey, createPublicKey, randomBytes, sign, timingSafeEqual } from "node:crypto";
15
14
  import { homedir, networkInterfaces } from "node:os";
16
15
  import { createServer } from "node:http";
16
+ import { emitKeypressEvents } from "node:readline";
17
+ import { createInterface } from "node:readline/promises";
17
18
  //#region src/report.ts
18
19
  /** A mistake in how a command was used, which its message alone explains. */
19
20
  var CommandError = class extends Error {
@@ -36,6 +37,10 @@ function printBuildTime(startedAt) {
36
37
  //#endregion
37
38
  //#region src/create/template.ts
38
39
  const TEMPLATES = ["starter", "showcase"];
40
+ const TEMPLATE_HINTS = {
41
+ starter: "one page to build on",
42
+ showcase: "a page for each feature, to read"
43
+ };
39
44
  function isTemplate(name) {
40
45
  return TEMPLATES.includes(name);
41
46
  }
@@ -75,6 +80,7 @@ function packageJson({ packageName, dotVersion, workspace }) {
75
80
  scripts: {
76
81
  dev: `dotctl dev${conditions}`,
77
82
  build: `dotctl build${conditions}`,
83
+ preview: `dotctl preview${conditions}`,
78
84
  typecheck: "tsc --noEmit"
79
85
  },
80
86
  dependencies: { "@monimesl/dot-runtime": dotVersion },
@@ -115,8 +121,8 @@ function dotJson({ name, packageName, template }) {
115
121
  version: "0.1.0"
116
122
  };
117
123
  if (template === "showcase") dot.network = { hosts: [
118
- "api.github.com",
119
- "avatars.githubusercontent.com",
124
+ "itunes.apple.com",
125
+ "*.mzstatic.com",
120
126
  "httpbin.org"
121
127
  ] };
122
128
  return json(dot);
@@ -218,11 +224,17 @@ function cliRoot() {
218
224
  return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
219
225
  }
220
226
  //#endregion
227
+ //#region src/dot-root.ts
228
+ function findDotRoot(dir) {
229
+ while (true) {
230
+ if (existsSync(join(dir, "dot.json"))) return dir;
231
+ const parent = dirname(dir);
232
+ if (parent === dir) return;
233
+ dir = parent;
234
+ }
235
+ }
236
+ //#endregion
221
237
  //#region src/create/page.ts
222
- /**
223
- * Adds a page to the dot around `cwd`: its folder under pages/, with its three files, listed last in
224
- * dot.ts.
225
- */
226
238
  async function createPage(path, cwd) {
227
239
  if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path: use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
228
240
  const root = findDotRoot(resolve(cwd));
@@ -277,14 +289,6 @@ function listsPage(source, dotFile, path) {
277
289
  return false;
278
290
  }
279
291
  }
280
- function findDotRoot(dir) {
281
- while (true) {
282
- if (existsSync(join(dir, "dot.json"))) return dir;
283
- const parent = dirname(dir);
284
- if (parent === dir) return;
285
- dir = parent;
286
- }
287
- }
288
292
  //#endregion
289
293
  //#region src/create/package-manager.ts
290
294
  const PACKAGE_MANAGERS = [
@@ -520,6 +524,17 @@ async function readText(request) {
520
524
  return Buffer.concat(chunks).toString("utf8");
521
525
  }
522
526
  //#endregion
527
+ //#region src/simulator.ts
528
+ function startDotSimulator(project, outDir, port) {
529
+ return startSimulator({
530
+ dotDir: outDir,
531
+ reactHostDir: installedRuntime(project.root).react.dir,
532
+ name: project.dotJson.name,
533
+ port,
534
+ storageDir: join(project.root, ".dot", "files")
535
+ });
536
+ }
537
+ //#endregion
523
538
  //#region src/dev.ts
524
539
  const CHANGE_DEBOUNCE_MS = 50;
525
540
  async function dev({ port, device, ...options }) {
@@ -543,13 +558,7 @@ async function dev({ port, device, ...options }) {
543
558
  }
544
559
  };
545
560
  const firstError = await buildApp();
546
- const simulator = await startSimulator({
547
- dotDir: outDir,
548
- reactHostDir: installedRuntime(project.root).react.dir,
549
- name: project.dotJson.name,
550
- port,
551
- storageDir: join(project.root, ".dot", "files")
552
- });
561
+ const simulator = await startDotSimulator(project, outDir, port);
553
562
  if (firstError) simulator.error(firstError);
554
563
  console.log(`\n Simulator running at: ${simulator.url}\n`);
555
564
  let devices;
@@ -637,6 +646,99 @@ function isIgnored(file) {
637
646
  });
638
647
  }
639
648
  //#endregion
649
+ //#region src/preview.ts
650
+ async function preview({ port, ...options }) {
651
+ const project = await loadProject(options.root);
652
+ const outDir = resolve(project.root, options.outDir ?? "dist");
653
+ const started = performance.now();
654
+ printWarnings(await build({
655
+ ...options,
656
+ outDir,
657
+ mode: "production"
658
+ }));
659
+ printBuildTime(started);
660
+ const simulator = await startDotSimulator(project, outDir, port);
661
+ console.log(`\n Simulator running at: ${simulator.url}\n`);
662
+ return simulator;
663
+ }
664
+ //#endregion
665
+ //#region src/prompts.ts
666
+ const TERMINAL = {
667
+ input: process.stdin,
668
+ output: process.stdout
669
+ };
670
+ const ERASE_BELOW = "\x1B[0J";
671
+ const HIDE_CURSOR = "\x1B[?25l";
672
+ const SHOW_CURSOR = "\x1B[?25h";
673
+ const cursorUp = (lines) => `\x1b[${lines}A`;
674
+ async function select(question, choices, fallback, { input, output } = TERMINAL) {
675
+ if (!input.isTTY) return fallback;
676
+ let current = Math.max(0, choices.findIndex((choice) => choice.value === fallback));
677
+ const width = choices.some((choice) => choice.hint) ? Math.max(...choices.map((choice) => choice.value.length)) : 0;
678
+ const row = (choice, index) => {
679
+ const name = choice.value.padEnd(width);
680
+ const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
681
+ const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
682
+ if (index === current) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], name)}${hint}${defaultMark}`;
683
+ return ` ${name}${hint}${defaultMark}`;
684
+ };
685
+ let drawnLines = 0;
686
+ const draw = (lines) => {
687
+ output.write(`${drawnLines ? cursorUp(drawnLines) : ""}${ERASE_BELOW}${lines.join("\n")}\n`);
688
+ drawnLines = lines.length;
689
+ };
690
+ const drawList = () => draw([`${styleText("bold", question)} ${styleText("dim", "↑/↓ to move, Enter to choose")}`, ...choices.map(row)]);
691
+ emitKeypressEvents(input);
692
+ input.setRawMode(true);
693
+ input.resume();
694
+ output.write(HIDE_CURSOR);
695
+ drawList();
696
+ return new Promise((resolve) => {
697
+ const onKeypress = (_text, key) => {
698
+ if (key.ctrl && key.name === "c") {
699
+ output.write(`${SHOW_CURSOR}\n`);
700
+ process.exit(130);
701
+ }
702
+ if (key.name === "up" || key.name === "k") {
703
+ current = (current - 1 + choices.length) % choices.length;
704
+ drawList();
705
+ } else if (key.name === "down" || key.name === "j") {
706
+ current = (current + 1) % choices.length;
707
+ drawList();
708
+ } else if (key.name === "return" || key.name === "enter") {
709
+ const { value } = choices[current];
710
+ input.off("keypress", onKeypress);
711
+ input.setRawMode(false);
712
+ input.pause();
713
+ draw([`${styleText("bold", question)} ${styleText("cyan", value)}`]);
714
+ output.write(SHOW_CURSOR);
715
+ resolve(value);
716
+ }
717
+ };
718
+ input.on("keypress", onKeypress);
719
+ });
720
+ }
721
+ async function confirm(question, fallback, terminal = TERMINAL) {
722
+ if (!terminal.input.isTTY) return false;
723
+ return await select(question, [{ value: "Yes" }, { value: "No" }], fallback ? "Yes" : "No", terminal) === "Yes";
724
+ }
725
+ async function ask(question, fallback, { input, output } = TERMINAL) {
726
+ if (!input.isTTY) return fallback;
727
+ const hint = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
728
+ const readline = createInterface({
729
+ input,
730
+ output
731
+ });
732
+ try {
733
+ return (await readline.question(`${styleText("bold", question)}${hint}: `)).trim() || fallback;
734
+ } catch (error) {
735
+ if (error.name === "AbortError") return;
736
+ throw error;
737
+ } finally {
738
+ readline.close();
739
+ }
740
+ }
741
+ //#endregion
640
742
  //#region src/cli.ts
641
743
  const USAGE = `Usage:
642
744
  dotctl create [dir] [--name <name>] [--template <starter|showcase>]
@@ -648,6 +750,8 @@ const USAGE = `Usage:
648
750
  view.dml, state.ts and controller.ts
649
751
  dotctl build [root] [options] Build the minified bundles the host app runs
650
752
  dotctl dev [root] [options] Run in the browser simulator, rebuilding on change
753
+ dotctl preview [root] [options] Build as \`build\` does, and run that build in the browser
754
+ simulator, as the host app gets it
651
755
  dotctl keys show Show the public key and key ID of this computer's
652
756
  developer key, which signs the links \`dev --device\`
653
757
  shows, for Dash to open
@@ -657,8 +761,8 @@ const USAGE = `Usage:
657
761
  Options:
658
762
  --out-dir <dir> Output directory (default: <root>/dist)
659
763
  --conditions <list> Extra export conditions, comma separated
660
- --port <port> Simulator port for \`dev\`; the next free one is used if taken
661
- (default: ${DEFAULT_PORT})
764
+ --port <port> Simulator port for \`dev\` and \`preview\`; the next free one is used
765
+ if taken (default: ${DEFAULT_PORT})
662
766
  --device With \`dev\`, also serve the dot to the Dot example apps on phones on
663
767
  the local network, through a link and QR code; they reload on each build
664
768
  -h, --help Show this message`;
@@ -683,14 +787,14 @@ async function main(argv) {
683
787
  });
684
788
  const [command, target, pagePath] = positionals;
685
789
  if (command === "lsp" && !values.help) {
686
- const { serveStdio } = await import("./server-DFOUQnLA.js");
790
+ const { serveStdio } = await import("./server-BfPAnsSf.js");
687
791
  serveStdio();
688
792
  return new Promise(() => {});
689
793
  }
690
794
  if (command === "keys" && !values.help) return keys(target);
691
795
  if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
692
796
  if (command === "create" && !values.help) return createApp(target, values.name, values.template, values.pm);
693
- if (values.help || command !== "build" && command !== "dev") {
797
+ if (values.help || command !== "build" && command !== "dev" && command !== "preview") {
694
798
  console.log(USAGE);
695
799
  return values.help ? 0 : 1;
696
800
  }
@@ -712,6 +816,19 @@ async function main(argv) {
712
816
  await stop();
713
817
  return 0;
714
818
  }
819
+ if (command === "preview") {
820
+ if (values.device) {
821
+ console.error("--device works only with dotctl dev");
822
+ return 1;
823
+ }
824
+ const simulator = await preview({
825
+ ...options,
826
+ port
827
+ });
828
+ await new Promise((resolve) => process.once("SIGINT", resolve));
829
+ await simulator.close();
830
+ return 0;
831
+ }
715
832
  const started = performance.now();
716
833
  const result = await build(options);
717
834
  for (const file of result.files.filter((file) => !file.endsWith(".map"))) console.log(` ${relative(process.cwd(), file)}`);
@@ -755,7 +872,7 @@ async function createApp(dir, name, templateFlag, packageManagerFlag) {
755
872
  console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
756
873
  return 1;
757
874
  }
758
- name ??= await ask("Dot name:", dir ? toDisplayName(dir) : "Dot");
875
+ name ??= await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
759
876
  if (!name) {
760
877
  console.error("A dot needs a name: dotctl create [dir] --name \"My Dot\"");
761
878
  return 1;
@@ -770,7 +887,8 @@ async function createApp(dir, name, templateFlag, packageManagerFlag) {
770
887
  console.log(`\nCreated ${name} in ${path}`);
771
888
  if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
772
889
  let packageManager = workspace ? "pnpm" : packageManagerFlag ?? launchedBy();
773
- if (await confirm("\nInstall and start it now?")) {
890
+ console.log();
891
+ if (await confirm("Install and start it now?", true)) {
774
892
  if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
775
893
  if (await run(packageManager, ["install"], root) === 0) return run(packageManager, ["run", "dev"], root);
776
894
  console.error(`\n${packageManager} install failed.`);
@@ -789,53 +907,24 @@ function run(command, args, cwd) {
789
907
  child.on("exit", (code) => resolvePromise(code ?? 1));
790
908
  });
791
909
  }
792
- async function confirm(question) {
793
- if (!process.stdin.isTTY) return false;
794
- const answer = await readAnswer(`${question} (Y/n)`, "y");
795
- return answer !== void 0 && answer.toLowerCase().startsWith("y");
796
- }
797
- async function ask(question, fallback) {
798
- return readAnswer(`${question}${fallback ? ` (${fallback})` : ""}`, fallback);
799
- }
800
- async function readAnswer(prompt, fallback) {
801
- if (!process.stdin.isTTY) return fallback;
802
- const readline = createInterface({
803
- input: process.stdin,
804
- output: process.stdout
805
- });
806
- try {
807
- return (await readline.question(`${prompt} `)).trim() || fallback;
808
- } catch (error) {
809
- if (error.name === "AbortError") return;
810
- throw error;
811
- } finally {
812
- readline.close();
813
- }
814
- }
815
- async function chooseTemplate() {
816
- if (!process.stdin.isTTY) return "starter";
817
- console.log("\nTemplates:\n starter one page to build on\n showcase a page for each feature, to read");
818
- return chooseOne("Template?", TEMPLATES, "starter");
910
+ function chooseTemplate() {
911
+ console.log();
912
+ return select("Template?", TEMPLATES.map((template) => ({
913
+ value: template,
914
+ hint: TEMPLATE_HINTS[template]
915
+ })), "starter");
819
916
  }
820
917
  async function choosePackageManager(suggested) {
821
918
  const installed = installedManagers();
822
919
  if (installed.length < 2) return installed[0] ?? suggested;
823
- return chooseOne("Package manager?", installed, installed.includes(suggested) ? suggested : installed[0]);
824
- }
825
- async function chooseOne(question, options, fallback) {
826
- while (true) {
827
- const answer = await ask(`${question} ${options.join(" / ")}`, fallback);
828
- if (answer === void 0) return fallback;
829
- const chosen = options.find((option) => option === answer);
830
- if (chosen) return chosen;
831
- console.log(`Choose one of ${options.join(", ")}.`);
832
- }
920
+ const fallback = installed.includes(suggested) ? suggested : installed[0];
921
+ return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
833
922
  }
834
923
  main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
835
924
  console.error(`error: ${describeError(error)}`);
836
925
  process.exit(1);
837
926
  });
838
927
  //#endregion
839
- export {};
928
+ export { findDotRoot as t };
840
929
 
841
930
  //# sourceMappingURL=cli.js.map