@monimesl/dot-cli 0.1.0-alpha.3 → 0.1.0-alpha.4

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 (61) hide show
  1. package/README.md +34 -15
  2. package/dist/cli.js +252 -39
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-z7M4FLjf.js → server-DFOUQnLA.js} +437 -60
  5. package/dist/server-DFOUQnLA.js.map +1 -0
  6. package/dist/{template → templates/showcase}/dot.ts +3 -0
  7. package/dist/templates/showcase/pages/carousel/controller.ts +10 -0
  8. package/dist/templates/showcase/pages/carousel/state.ts +40 -0
  9. package/dist/templates/showcase/pages/carousel/view.dml +37 -0
  10. package/dist/templates/showcase/pages/filters/controller.ts +28 -0
  11. package/dist/templates/showcase/pages/filters/state.ts +72 -0
  12. package/dist/templates/showcase/pages/filters/view.dml +43 -0
  13. package/dist/{template → templates/showcase}/pages/index/state.ts +19 -1
  14. package/dist/{template → templates/showcase}/pages/layout/controller.ts +3 -0
  15. package/dist/{template → templates/showcase}/pages/layout/state.ts +2 -0
  16. package/dist/{template → templates/showcase}/pages/layout/view.dml +17 -0
  17. package/dist/{template → templates/showcase}/pages/navigation/controller.ts +22 -0
  18. package/dist/{template → templates/showcase}/pages/navigation/state.ts +4 -0
  19. package/dist/{template → templates/showcase}/pages/navigation/view.dml +10 -0
  20. package/dist/templates/showcase/pages/payments/controller.ts +42 -0
  21. package/dist/templates/showcase/pages/payments/state.ts +10 -0
  22. package/dist/templates/showcase/pages/payments/view.dml +35 -0
  23. package/dist/templates/starter/.env +3 -0
  24. package/dist/templates/starter/dot.ts +3 -0
  25. package/dist/templates/starter/pages/index/controller.ts +9 -0
  26. package/dist/templates/starter/pages/index/state.ts +4 -0
  27. package/dist/templates/starter/pages/index/view.dml +16 -0
  28. package/dist/templates/starter/statics/logo.png +0 -0
  29. package/package.json +8 -7
  30. package/dist/server-z7M4FLjf.js.map +0 -1
  31. /package/dist/{template → templates/showcase}/.env +0 -0
  32. /package/dist/{template → templates/showcase}/pages/device/controller.ts +0 -0
  33. /package/dist/{template → templates/showcase}/pages/device/state.ts +0 -0
  34. /package/dist/{template → templates/showcase}/pages/device/view.dml +0 -0
  35. /package/dist/{template → templates/showcase}/pages/feedback/controller.ts +0 -0
  36. /package/dist/{template → templates/showcase}/pages/feedback/state.ts +0 -0
  37. /package/dist/{template → templates/showcase}/pages/feedback/view.dml +0 -0
  38. /package/dist/{template → templates/showcase}/pages/files/controller.ts +0 -0
  39. /package/dist/{template → templates/showcase}/pages/files/state.ts +0 -0
  40. /package/dist/{template → templates/showcase}/pages/files/view.dml +0 -0
  41. /package/dist/{template → templates/showcase}/pages/forms/controller.ts +0 -0
  42. /package/dist/{template → templates/showcase}/pages/forms/state.ts +0 -0
  43. /package/dist/{template → templates/showcase}/pages/forms/view.dml +0 -0
  44. /package/dist/{template → templates/showcase}/pages/images/controller.ts +0 -0
  45. /package/dist/{template → templates/showcase}/pages/images/state.ts +0 -0
  46. /package/dist/{template → templates/showcase}/pages/images/view.dml +0 -0
  47. /package/dist/{template → templates/showcase}/pages/index/controller.ts +0 -0
  48. /package/dist/{template → templates/showcase}/pages/index/view.dml +0 -0
  49. /package/dist/{template → templates/showcase}/pages/lists/controller.ts +0 -0
  50. /package/dist/{template → templates/showcase}/pages/lists/state.ts +0 -0
  51. /package/dist/{template → templates/showcase}/pages/lists/view.dml +0 -0
  52. /package/dist/{template → templates/showcase}/pages/network/controller.ts +0 -0
  53. /package/dist/{template → templates/showcase}/pages/network/state.ts +0 -0
  54. /package/dist/{template → templates/showcase}/pages/network/view.dml +0 -0
  55. /package/dist/{template → templates/showcase}/pages/state/controller.ts +0 -0
  56. /package/dist/{template → templates/showcase}/pages/state/state.ts +0 -0
  57. /package/dist/{template → templates/showcase}/pages/state/view.dml +0 -0
  58. /package/dist/{template → templates/showcase}/pages/text/controller.ts +0 -0
  59. /package/dist/{template → templates/showcase}/pages/text/state.ts +0 -0
  60. /package/dist/{template → templates/showcase}/pages/text/view.dml +0 -0
  61. /package/dist/{template → templates/showcase}/statics/logo.png +0 -0
package/README.md CHANGED
@@ -4,23 +4,42 @@ The `dotctl` command: build mini apps and run them in the simulator. Platform ta
4
4
  packaging, signing and publishing mini apps will live here too.
5
5
 
6
6
  ```sh
7
- dotctl create [dir] # scaffold a dot (asks for its name), with a sample home page
8
- dotctl build [root] # compile the dot into dist/ui and dist/logic, minified
9
- dotctl dev [root] # build, open it in the browser simulator, rebuild and reload on change
7
+ dotctl create [dir] # scaffold a dot (asks for its name and template)
8
+ dotctl create page <path> # add a page to the dot you are in
9
+ dotctl build [root] # compile the dot into dist/ui and dist/logic, minified
10
+ dotctl dev [root] # build, run it in the browser simulator, and reload it on each change
11
+ dotctl lsp # the .dml language server, which editors start
10
12
  ```
11
13
 
12
- | Option | Applies to | What it does |
13
- | --------------------- | ---------- | ------------------------------------------------------- |
14
- | `--out-dir <dir>` | both | Output directory (default: `<root>/dist`) |
15
- | `--conditions <list>` | both | Extra export conditions, such as `@monimesl/dot-source` |
16
- | `--port <port>` | `dev` | Simulator port (default: `3680`, or the next free one) |
14
+ | Option | Applies to | What it does |
15
+ | --------------------- | ---------- | ------------------------------------------------------------------------ |
16
+ | `--out-dir <dir>` | both | Output directory (default: `<root>/dist`) |
17
+ | `--conditions <list>` | both | Extra export conditions, such as `@monimesl/dot-source` |
18
+ | `--port <port>` | `dev` | Simulator port (default: `3680`, or the next free one) |
19
+ | `--device` | `dev` | Also serve the dot to the Dot example apps on phones, through a QR code |
20
+ | `--name <name>` | `create` | The dot's name, instead of asking |
21
+ | `--template <name>` | `create` | `starter` (one page to build on, the default) or `showcase` (every page) |
22
+ | `--pm <name>` | `create` | The package manager to install with: npm, pnpm, yarn or bun |
17
23
 
18
24
  ## `dotctl create`
19
25
 
20
- Asks for the dot's name (`Dot` by default, or pass `--name`) and creates a self-contained project
21
- in `dir` (by default, the name in kebab case) with one page, `home`: a short welcome, localized for
22
- Sierra Leone, to a dot running inside Dash. The dot depends on `@monimesl/dot-runtime` and `@monimesl/dot-cli`; inside
23
- the Dot repository it uses the workspace packages instead. It then offers to install the
24
- dependencies and start the simulator inside the new dot, asking which of the installed package
25
- managers to use (npm, pnpm, yarn or bun; the one that launched `dotctl`, or npm, by default).
26
- `--pm <name>` answers that up front.
26
+ Asks for the dot's name (`Dot` by default) and template, and creates a self-contained project in
27
+ `dir` (by default, the name in kebab case): `starter` is one page to build on, and `showcase` a page
28
+ for each feature. The dot depends on `@monimesl/dot-runtime` and `@monimesl/dot-cli`; inside the Dot
29
+ repository it uses the workspace packages instead. It then offers to install the dependencies and
30
+ start the simulator inside the new dot, asking which of the installed package managers to use (npm,
31
+ pnpm, yarn or bun; the one that launched `dotctl`, or npm, by default).
32
+
33
+ ## `dotctl dev`
34
+
35
+ Builds the dot, opens it in the browser simulator, and builds it again on each save:
36
+
37
+ - A change to its pages (`pages/**`) runs in place: each open page keeps its state, and runs the new
38
+ code. A page whose initial state changed starts over from the new one.
39
+ - Any other change, such as to `dot.ts`, `dot.json`, `.env` or `statics/`, starts the dot again, on
40
+ the pages it had open.
41
+ - A failed build shows its error, and the dot keeps its last good build.
42
+
43
+ With `--device`, it also serves each build on the local network to the Dot example apps. It prints a
44
+ QR code to scan with one, which then follows the dot in the same way: see
45
+ [the example apps](../hosts/native/README.md#the-example-apps).
package/dist/cli.js CHANGED
@@ -3,13 +3,17 @@ import { spawn, spawnSync } from "node:child_process";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { parseArgs } from "node:util";
6
- import { CompileError, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
6
+ import { CompileError, PAGES_DIR, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
7
7
  import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
8
8
  import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
9
- import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
10
10
  import { DmlParseError } from "@monimesl/dot-dml-parser";
11
11
  import { ScriptParseError, parseDotModule } from "@monimesl/dot-script-parser";
12
12
  import { isPageId } from "@monimesl/dot-core";
13
+ import { renderUnicodeCompact } from "uqr";
14
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
15
+ import { createServer } from "node:http";
16
+ import { networkInterfaces } from "node:os";
13
17
  //#region src/report.ts
14
18
  /** A mistake in how a command was used, which its message alone explains. */
15
19
  var CommandError = class extends Error {
@@ -31,6 +35,10 @@ function printBuildTime(startedAt) {
31
35
  }
32
36
  //#endregion
33
37
  //#region src/create/template.ts
38
+ const TEMPLATES = ["starter", "showcase"];
39
+ function isTemplate(name) {
40
+ return TEMPLATES.includes(name);
41
+ }
34
42
  const PAGE_FILES = {
35
43
  "view.dml": "<View>\n <Text>My new page</Text>\n</View>\n",
36
44
  "state.ts": "export default {};\n",
@@ -54,18 +62,8 @@ function projectFiles(options) {
54
62
  "package.json": packageJson(options),
55
63
  "tsconfig.json": tsconfig(options),
56
64
  ".gitignore": "node_modules/\ndist/\n# Types generated by dotctl\n.dot/\n",
57
- "dot.json": json({
58
- $schema: DOT_JSON_SCHEMA,
59
- id: `com.example.${idWord(options.packageName)}`,
60
- name: options.name,
61
- version: "0.1.0",
62
- network: { hosts: [
63
- "api.github.com",
64
- "avatars.githubusercontent.com",
65
- "httpbin.org"
66
- ] }
67
- }),
68
- "README.md": readme(options.name)
65
+ "dot.json": dotJson(options),
66
+ "README.md": readme(options)
69
67
  };
70
68
  }
71
69
  function packageJson({ packageName, dotVersion, workspace }) {
@@ -109,7 +107,29 @@ function tsconfig({ workspace }) {
109
107
  ]
110
108
  });
111
109
  }
112
- function readme(name) {
110
+ function dotJson({ name, packageName, template }) {
111
+ const dot = {
112
+ $schema: DOT_JSON_SCHEMA,
113
+ id: `com.example.${idWord(packageName)}`,
114
+ name,
115
+ version: "0.1.0"
116
+ };
117
+ if (template === "showcase") dot.network = { hosts: [
118
+ "api.github.com",
119
+ "avatars.githubusercontent.com",
120
+ "httpbin.org"
121
+ ] };
122
+ return json(dot);
123
+ }
124
+ const TEMPLATE_READMES = {
125
+ starter: `It starts with one page, \`index\`: a greeting from \`.env\` and a count to tap. Change it,
126
+ and add pages as the dot grows. \`npx dotctl create my-tour --template showcase\` makes a dot with a
127
+ page for each feature, to read how each is done.`,
128
+ showcase: `It starts as a showcase: \`index\` lists a page for each feature, from layout, text, images and
129
+ inputs to state, navigation, and Dash's alerts, network and files. Read a page to see how it's
130
+ done, then delete the ones you don't need, from \`pages/\` and from \`dot.ts\`.`
131
+ };
132
+ function readme({ name, template }) {
113
133
  return `# ${name}
114
134
 
115
135
  A dot: a mini app that runs inside Dash.
@@ -135,9 +155,7 @@ the other fields from the schema it points to.
135
155
  there is built into the dot, so anyone who has it can read them: never put a secret there.
136
156
  \`.env.development\` and \`.env.production\` override it for \`dev\` and \`build\`.
137
157
 
138
- It starts as a showcase: \`index\` lists a page for each feature, from layout, text, images and
139
- inputs to state, navigation, and Dash's alerts, network and files. Read a page to see how it's
140
- done, then delete the ones you don't need, from \`pages/\` and from \`dot.ts\`.
158
+ ${TEMPLATE_READMES[template]}
141
159
  `;
142
160
  }
143
161
  function idWord(packageName) {
@@ -145,20 +163,21 @@ function idWord(packageName) {
145
163
  }
146
164
  //#endregion
147
165
  //#region src/create/create.ts
148
- async function create({ name, dir }) {
166
+ async function create({ name, dir, template = "starter" }) {
149
167
  const root = resolve(dir);
150
168
  if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty`);
151
169
  const packageName = toPackageName(name);
152
170
  if (!packageName) throw new CommandError(`"${name}" needs at least one letter or digit`);
153
171
  const workspace = isInDotWorkspace(root);
154
- const template = templateDir();
172
+ const source = templateDir(template);
155
173
  await mkdir(root, { recursive: true });
156
- for (const path of TEMPLATE_FILES) await cp(join(template, path), join(root, path), { recursive: true });
174
+ for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
157
175
  const generated = projectFiles({
158
176
  name,
159
177
  packageName,
160
178
  dotVersion: workspace ? "workspace:*" : `^${cliVersion()}`,
161
- workspace
179
+ workspace,
180
+ template
162
181
  });
163
182
  for (const [path, content] of Object.entries(generated)) await writeFile(join(root, path), content);
164
183
  return {
@@ -185,9 +204,9 @@ function isInDotWorkspace(dir) {
185
204
  current = parent;
186
205
  }
187
206
  }
188
- function templateDir() {
189
- const example = join(cliRoot(), "../../examples/showcase");
190
- return existsSync(example) ? example : join(cliRoot(), "dist/template");
207
+ function templateDir(template) {
208
+ const example = join(cliRoot(), "../../examples", template);
209
+ return existsSync(example) ? example : join(cliRoot(), "dist/templates", template);
191
210
  }
192
211
  function cliVersion() {
193
212
  return readPackageJson(join(cliRoot(), "package.json")).version;
@@ -290,9 +309,152 @@ function onPath(name) {
290
309
  }).status === 0;
291
310
  }
292
311
  //#endregion
312
+ //#region src/device/server.ts
313
+ const DEVICE_SCHEME = "dotdev";
314
+ const WAIT_MS = 25e3;
315
+ const MAX_LOG_BYTES = 1048576;
316
+ async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {} } = {}) {
317
+ if (address === void 0) throw new Error("This computer has no local network address for devices to reach it on");
318
+ const token = randomBytes(18).toString("base64url");
319
+ let build = { files: /* @__PURE__ */ new Map() };
320
+ let generation = 0;
321
+ let hotSince = 0;
322
+ let error = null;
323
+ const waiting = /* @__PURE__ */ new Set();
324
+ const connected = /* @__PURE__ */ new Set();
325
+ const announce = () => {
326
+ generation += 1;
327
+ for (const wake of waiting) wake();
328
+ };
329
+ const server = createServer((request, response) => {
330
+ const from = request.socket.remoteAddress ?? "a device";
331
+ if (!isAuthorized(request, token)) {
332
+ response.writeHead(401).end();
333
+ return;
334
+ }
335
+ const url = new URL(request.url ?? "/", "http://device");
336
+ const path = url.pathname;
337
+ if (request.method === "GET" && path === "/dev/build") {
338
+ if (!connected.has(from)) {
339
+ connected.add(from);
340
+ onConnect(from);
341
+ }
342
+ const files = Object.fromEntries([...build.files].map(([name, file]) => [name, file.hash]));
343
+ sendJson(response, {
344
+ generation,
345
+ hotSince,
346
+ files,
347
+ error
348
+ });
349
+ } else if (request.method === "GET" && path.startsWith("/dev/files/")) {
350
+ if (url.searchParams.get("generation") !== String(generation)) {
351
+ response.writeHead(409).end();
352
+ return;
353
+ }
354
+ const name = path.slice(11).split("/").map(decodeURIComponent).join("/");
355
+ const file = build.files.get(name);
356
+ if (!file) {
357
+ response.writeHead(404).end();
358
+ return;
359
+ }
360
+ response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
361
+ } else if (request.method === "GET" && path === "/dev/changes") waitForChange(response, Number(url.searchParams.get("after")));
362
+ else if (request.method === "POST" && path === "/dev/log") readText(request).then((text) => {
363
+ for (const line of text.split("\n")) onLog(line, from);
364
+ response.writeHead(204).end();
365
+ }).catch(() => response.writeHead(413).end());
366
+ else response.writeHead(404).end();
367
+ });
368
+ const waitForChange = (response, after) => {
369
+ if (generation > after) {
370
+ sendJson(response, { generation });
371
+ return;
372
+ }
373
+ const wake = () => {
374
+ clearTimeout(timer);
375
+ waiting.delete(wake);
376
+ sendJson(response, { generation });
377
+ };
378
+ const timer = setTimeout(() => {
379
+ waiting.delete(wake);
380
+ response.writeHead(204).end();
381
+ }, WAIT_MS);
382
+ waiting.add(wake);
383
+ response.on("close", () => {
384
+ clearTimeout(timer);
385
+ waiting.delete(wake);
386
+ });
387
+ };
388
+ await new Promise((resolve, reject) => {
389
+ server.once("error", reject);
390
+ server.listen(port, address, () => resolve());
391
+ });
392
+ const { port: listening } = server.address();
393
+ const origin = `http://${address}:${listening}`;
394
+ return {
395
+ link: `${DEVICE_SCHEME}://connect?server=${encodeURIComponent(origin)}&token=${token}`,
396
+ async publish(dir, { hot = false } = {}) {
397
+ build = await readBuild(dir);
398
+ error = null;
399
+ announce();
400
+ if (!hot) hotSince = generation;
401
+ },
402
+ fail(message) {
403
+ error = message;
404
+ announce();
405
+ },
406
+ close() {
407
+ for (const wake of waiting) wake();
408
+ return new Promise((resolve) => {
409
+ server.close(() => resolve());
410
+ server.closeAllConnections();
411
+ });
412
+ }
413
+ };
414
+ }
415
+ function localNetworkAddress() {
416
+ for (const addresses of Object.values(networkInterfaces())) for (const candidate of addresses ?? []) if (candidate.family === "IPv4" && !candidate.internal) return candidate.address;
417
+ }
418
+ async function readBuild(dir) {
419
+ const files = /* @__PURE__ */ new Map();
420
+ const entries = await readdir(dir, {
421
+ recursive: true,
422
+ withFileTypes: true
423
+ });
424
+ for (const entry of entries) {
425
+ if (!entry.isFile() || entry.name.endsWith(".map") || entry.name.endsWith(".zip")) continue;
426
+ const path = join(entry.parentPath, entry.name);
427
+ const bytes = await readFile(path);
428
+ const hash = createHash("sha256").update(bytes).digest("base64url");
429
+ files.set(relative(dir, path).split(sep).join("/"), {
430
+ bytes,
431
+ hash
432
+ });
433
+ }
434
+ return { files };
435
+ }
436
+ function isAuthorized(request, token) {
437
+ const given = Buffer.from(request.headers.authorization ?? "");
438
+ const expected = Buffer.from(`Bearer ${token}`);
439
+ return given.length === expected.length && timingSafeEqual(given, expected);
440
+ }
441
+ function sendJson(response, body) {
442
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
443
+ }
444
+ async function readText(request) {
445
+ const chunks = [];
446
+ let size = 0;
447
+ for await (const chunk of request) {
448
+ size += chunk.length;
449
+ if (size > MAX_LOG_BYTES) throw new Error("Too much log at once");
450
+ chunks.push(chunk);
451
+ }
452
+ return Buffer.concat(chunks).toString("utf8");
453
+ }
454
+ //#endregion
293
455
  //#region src/dev.ts
294
456
  const CHANGE_DEBOUNCE_MS = 50;
295
- async function dev({ port, ...options }) {
457
+ async function dev({ port, device, ...options }) {
296
458
  const project = await loadProject(options.root);
297
459
  const outDir = resolve(project.root, options.outDir ?? "dist");
298
460
  const buildOptions = {
@@ -322,16 +484,38 @@ async function dev({ port, ...options }) {
322
484
  });
323
485
  if (firstError) simulator.error(firstError);
324
486
  console.log(`\n Simulator running at: ${simulator.url}\n`);
487
+ let devices;
488
+ if (device) {
489
+ devices = await startDeviceServer({
490
+ onConnect: (from) => console.log(`A device connected from ${from}`),
491
+ onLog: (line) => console.log(`device ${line}`)
492
+ });
493
+ if (firstError) devices.fail(firstError);
494
+ else await devices.publish(outDir);
495
+ printDeviceLink(devices.link);
496
+ }
497
+ const changed = /* @__PURE__ */ new Set();
498
+ const pagesDir = join(project.root, PAGES_DIR) + sep;
325
499
  const rebuilds = new RebuildQueue(async () => {
500
+ const building = [...changed];
326
501
  const error = await buildApp();
327
- if (error) simulator.error(error);
502
+ if (error) {
503
+ simulator.error(error);
504
+ devices?.fail(error);
505
+ return;
506
+ }
507
+ for (const path of building) changed.delete(path);
508
+ const hot = building.every((path) => path.startsWith(pagesDir));
509
+ if (hot) simulator.hotReload();
328
510
  else simulator.reload();
511
+ await devices?.publish(outDir, { hot });
329
512
  });
330
513
  let debounceTimer;
331
514
  const watcher = watch(project.root, { recursive: true }, (_event, file) => {
332
515
  if (!file || isIgnored(file)) return;
333
516
  const path = resolve(project.root, file);
334
517
  if (path === outDir || path.startsWith(outDir + sep)) return;
518
+ changed.add(path);
335
519
  clearTimeout(debounceTimer);
336
520
  debounceTimer = setTimeout(() => {
337
521
  console.log(`Changed ${relative(project.root, path)}`);
@@ -341,9 +525,16 @@ async function dev({ port, ...options }) {
341
525
  return async () => {
342
526
  clearTimeout(debounceTimer);
343
527
  watcher.close();
344
- await simulator.close();
528
+ await Promise.all([simulator.close(), devices?.close()]);
345
529
  };
346
530
  }
531
+ function printDeviceLink(link) {
532
+ console.log(" Scan this in a Dot example app on a phone:\n");
533
+ console.log(renderUnicodeCompact(link, { border: 2 }).replace(/^/gm, " "));
534
+ console.log(`\n Or open the link on a device, an emulator or a simulator:\n\n ${link}\n`);
535
+ console.log(` Android emulator: adb shell am start -a android.intent.action.VIEW -d '${link}'`);
536
+ console.log(` iOS Simulator: xcrun simctl openurl booted '${link}'\n`);
537
+ }
347
538
  var RebuildQueue = class {
348
539
  #rebuild;
349
540
  #building = false;
@@ -378,8 +569,11 @@ function isIgnored(file) {
378
569
  //#endregion
379
570
  //#region src/cli.ts
380
571
  const USAGE = `Usage:
381
- dotctl create [dir] [--name <name>] [--pm <npm|pnpm|yarn|bun>]
382
- Create a dot (asks for its name)
572
+ dotctl create [dir] [--name <name>] [--template <starter|showcase>]
573
+ [--pm <npm|pnpm|yarn|bun>]
574
+ Create a dot (asks for its name and template):
575
+ starter is one page to build on, showcase a page
576
+ for each feature
383
577
  dotctl create page <path> Add a page to the dot you are in: pages/<path>/ with
384
578
  view.dml, state.ts and controller.ts
385
579
  dotctl build [root] [options] Build the minified bundles the host app runs
@@ -392,6 +586,8 @@ Options:
392
586
  --conditions <list> Extra export conditions, comma separated
393
587
  --port <port> Simulator port for \`dev\`; the next free one is used if taken
394
588
  (default: ${DEFAULT_PORT})
589
+ --device With \`dev\`, also serve the dot to the Dot example apps on phones on
590
+ the local network, through a link and QR code; they reload on each build
395
591
  -h, --help Show this message`;
396
592
  async function main(argv) {
397
593
  const { values, positionals } = parseArgs({
@@ -401,7 +597,9 @@ async function main(argv) {
401
597
  "out-dir": { type: "string" },
402
598
  conditions: { type: "string" },
403
599
  port: { type: "string" },
600
+ device: { type: "boolean" },
404
601
  name: { type: "string" },
602
+ template: { type: "string" },
405
603
  pm: { type: "string" },
406
604
  help: {
407
605
  type: "boolean",
@@ -412,12 +610,12 @@ async function main(argv) {
412
610
  });
413
611
  const [command, target, pagePath] = positionals;
414
612
  if (command === "lsp" && !values.help) {
415
- const { serveStdio } = await import("./server-z7M4FLjf.js");
613
+ const { serveStdio } = await import("./server-DFOUQnLA.js");
416
614
  serveStdio();
417
615
  return new Promise(() => {});
418
616
  }
419
617
  if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
420
- if (command === "create" && !values.help) return createApp(target, values.name, values.pm);
618
+ if (command === "create" && !values.help) return createApp(target, values.name, values.template, values.pm);
421
619
  if (values.help || command !== "build" && command !== "dev") {
422
620
  console.log(USAGE);
423
621
  return values.help ? 0 : 1;
@@ -433,7 +631,8 @@ async function main(argv) {
433
631
  if (command === "dev") {
434
632
  const stop = await dev({
435
633
  ...options,
436
- port
634
+ port,
635
+ device: values.device
437
636
  });
438
637
  await new Promise((resolve) => process.once("SIGINT", resolve));
439
638
  await stop();
@@ -459,7 +658,11 @@ async function addPage(path) {
459
658
  else if (listing === "manual") console.log(`\nList it in dot.ts to build it: defineDot({ pages: [..., '${path}'] })`);
460
659
  return 0;
461
660
  }
462
- async function createApp(dir, name, packageManagerFlag) {
661
+ async function createApp(dir, name, templateFlag, packageManagerFlag) {
662
+ if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
663
+ console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
664
+ return 1;
665
+ }
463
666
  if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
464
667
  console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
465
668
  return 1;
@@ -469,9 +672,11 @@ async function createApp(dir, name, packageManagerFlag) {
469
672
  console.error("A dot needs a name: dotctl create [dir] --name \"My Dot\"");
470
673
  return 1;
471
674
  }
675
+ const template = templateFlag ?? await chooseTemplate();
472
676
  const { root, workspace } = await create({
473
677
  name,
474
- dir: dir ?? toPackageName(name)
678
+ dir: dir ?? toPackageName(name),
679
+ template
475
680
  });
476
681
  const path = relative(process.cwd(), root) || ".";
477
682
  console.log(`\nCreated ${name} in ${path}`);
@@ -519,15 +724,23 @@ async function readAnswer(prompt, fallback) {
519
724
  readline.close();
520
725
  }
521
726
  }
727
+ async function chooseTemplate() {
728
+ if (!process.stdin.isTTY) return "starter";
729
+ console.log("\nTemplates:\n starter one page to build on\n showcase a page for each feature, to read");
730
+ return chooseOne("Template?", TEMPLATES, "starter");
731
+ }
522
732
  async function choosePackageManager(suggested) {
523
733
  const installed = installedManagers();
524
734
  if (installed.length < 2) return installed[0] ?? suggested;
525
- const fallback = installed.includes(suggested) ? suggested : installed[0];
735
+ return chooseOne("Package manager?", installed, installed.includes(suggested) ? suggested : installed[0]);
736
+ }
737
+ async function chooseOne(question, options, fallback) {
526
738
  while (true) {
527
- const answer = await ask(`Package manager? ${installed.join(" / ")}`, fallback);
739
+ const answer = await ask(`${question} ${options.join(" / ")}`, fallback);
528
740
  if (answer === void 0) return fallback;
529
- if (isPackageManager(answer) && installed.includes(answer)) return answer;
530
- console.log(`Choose one of ${installed.join(", ")}.`);
741
+ const chosen = options.find((option) => option === answer);
742
+ if (chosen) return chosen;
743
+ console.log(`Choose one of ${options.join(", ")}.`);
531
744
  }
532
745
  }
533
746
  main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {