@monimesl/dot-cli 0.1.0-alpha.10

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 (68) hide show
  1. package/README.md +96 -0
  2. package/bin/dotctl.js +2 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +942 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/server-Dbrgkelo.js +11215 -0
  7. package/dist/server-Dbrgkelo.js.map +1 -0
  8. package/dist/templates/showcase/.env +3 -0
  9. package/dist/templates/showcase/dot.ts +24 -0
  10. package/dist/templates/showcase/pages/cache/controller.ts +45 -0
  11. package/dist/templates/showcase/pages/cache/state.ts +17 -0
  12. package/dist/templates/showcase/pages/cache/view.dml +36 -0
  13. package/dist/templates/showcase/pages/carousel/controller.ts +40 -0
  14. package/dist/templates/showcase/pages/carousel/state.ts +77 -0
  15. package/dist/templates/showcase/pages/carousel/view.dml +40 -0
  16. package/dist/templates/showcase/pages/device/controller.ts +80 -0
  17. package/dist/templates/showcase/pages/device/state.ts +7 -0
  18. package/dist/templates/showcase/pages/device/view.dml +49 -0
  19. package/dist/templates/showcase/pages/feedback/controller.ts +21 -0
  20. package/dist/templates/showcase/pages/feedback/state.ts +8 -0
  21. package/dist/templates/showcase/pages/feedback/view.dml +87 -0
  22. package/dist/templates/showcase/pages/files/controller.ts +66 -0
  23. package/dist/templates/showcase/pages/files/state.ts +19 -0
  24. package/dist/templates/showcase/pages/files/view.dml +50 -0
  25. package/dist/templates/showcase/pages/filters/controller.ts +27 -0
  26. package/dist/templates/showcase/pages/filters/state.ts +118 -0
  27. package/dist/templates/showcase/pages/filters/view.dml +44 -0
  28. package/dist/templates/showcase/pages/forms/controller.ts +68 -0
  29. package/dist/templates/showcase/pages/forms/state.ts +34 -0
  30. package/dist/templates/showcase/pages/forms/view.dml +49 -0
  31. package/dist/templates/showcase/pages/images/controller.ts +41 -0
  32. package/dist/templates/showcase/pages/images/state.ts +10 -0
  33. package/dist/templates/showcase/pages/images/view.dml +53 -0
  34. package/dist/templates/showcase/pages/index/controller.ts +10 -0
  35. package/dist/templates/showcase/pages/index/state.ts +128 -0
  36. package/dist/templates/showcase/pages/index/view.dml +20 -0
  37. package/dist/templates/showcase/pages/layout/controller.ts +60 -0
  38. package/dist/templates/showcase/pages/layout/state.ts +34 -0
  39. package/dist/templates/showcase/pages/layout/view.dml +103 -0
  40. package/dist/templates/showcase/pages/lists/controller.ts +37 -0
  41. package/dist/templates/showcase/pages/lists/state.ts +117 -0
  42. package/dist/templates/showcase/pages/lists/view.dml +59 -0
  43. package/dist/templates/showcase/pages/navigation/controller.ts +65 -0
  44. package/dist/templates/showcase/pages/navigation/state.ts +17 -0
  45. package/dist/templates/showcase/pages/navigation/view.dml +34 -0
  46. package/dist/templates/showcase/pages/network/controller.ts +151 -0
  47. package/dist/templates/showcase/pages/network/state.ts +22 -0
  48. package/dist/templates/showcase/pages/network/view.dml +67 -0
  49. package/dist/templates/showcase/pages/payments/controller.ts +46 -0
  50. package/dist/templates/showcase/pages/payments/state.ts +13 -0
  51. package/dist/templates/showcase/pages/payments/view.dml +45 -0
  52. package/dist/templates/showcase/pages/secrets/controller.ts +43 -0
  53. package/dist/templates/showcase/pages/secrets/state.ts +5 -0
  54. package/dist/templates/showcase/pages/secrets/view.dml +24 -0
  55. package/dist/templates/showcase/pages/state/controller.ts +22 -0
  56. package/dist/templates/showcase/pages/state/state.ts +10 -0
  57. package/dist/templates/showcase/pages/state/view.dml +33 -0
  58. package/dist/templates/showcase/pages/text/controller.ts +10 -0
  59. package/dist/templates/showcase/pages/text/state.ts +8 -0
  60. package/dist/templates/showcase/pages/text/view.dml +58 -0
  61. package/dist/templates/showcase/statics/logo.png +0 -0
  62. package/dist/templates/starter/.env +3 -0
  63. package/dist/templates/starter/dot.ts +3 -0
  64. package/dist/templates/starter/pages/index/controller.ts +9 -0
  65. package/dist/templates/starter/pages/index/state.ts +4 -0
  66. package/dist/templates/starter/pages/index/view.dml +16 -0
  67. package/dist/templates/starter/statics/logo.png +0 -0
  68. package/package.json +53 -0
package/dist/cli.js ADDED
@@ -0,0 +1,942 @@
1
+ import { createRequire } from "node:module";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
+ import { parseArgs, styleText } from "node:util";
5
+ import { CompileError, PAGES_DIR, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
6
+ import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
7
+ import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
8
+ import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
9
+ import { DmlParseError } from "@monimesl/dot-dml-parser";
10
+ import { ScriptParseError, parseDotModule } from "@monimesl/dot-script-parser";
11
+ import { isPageId } from "@monimesl/dot-core";
12
+ import { renderUnicodeCompact } from "uqr";
13
+ import { createHash, createHmac, createPrivateKey, createPublicKey, randomBytes, sign, timingSafeEqual } from "node:crypto";
14
+ import { homedir, networkInterfaces } from "node:os";
15
+ import { createServer } from "node:http";
16
+ import { emitKeypressEvents } from "node:readline";
17
+ import { createInterface } from "node:readline/promises";
18
+ //#region src/report.ts
19
+ var CommandError = class extends Error {
20
+ name = "CommandError";
21
+ };
22
+ function describeError(error) {
23
+ if (error instanceof CommandError || error instanceof CompileError || error instanceof DmlParseError || error instanceof ScriptParseError) return error.message;
24
+ if (error.code?.startsWith("ERR_PARSE_ARGS")) {
25
+ const [firstSentence] = error.message.split(". ");
26
+ return `${firstSentence}. See dotctl --help.`;
27
+ }
28
+ return error instanceof Error ? error.stack ?? error.message : String(error);
29
+ }
30
+ function printWarnings({ warnings }) {
31
+ for (const warning of warnings) console.warn(`warning: ${warning.message}`);
32
+ }
33
+ function printBuildTime(startedAt) {
34
+ console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
35
+ }
36
+ //#endregion
37
+ //#region src/create/template.ts
38
+ const TEMPLATES = ["starter", "showcase"];
39
+ const TEMPLATE_HINTS = {
40
+ starter: "one page to build on",
41
+ showcase: "a page for each feature, to learn from"
42
+ };
43
+ function isTemplate(name) {
44
+ return TEMPLATES.includes(name);
45
+ }
46
+ const PAGE_FILES = {
47
+ "view.dml": "<View>\n <Text>My new page</Text>\n</View>\n",
48
+ "state.ts": "export default {};\n",
49
+ "controller.ts": `import state from './state';
50
+
51
+ export default definePage({
52
+ initialState: state,
53
+ });
54
+ `
55
+ };
56
+ const TEMPLATE_FILES = [
57
+ "dot.ts",
58
+ "pages",
59
+ "statics",
60
+ ".env"
61
+ ];
62
+ const DOT_JSON_SCHEMA = "./node_modules/@monimesl/dot-runtime/dot.schema.json";
63
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
64
+ function projectFiles(options) {
65
+ return {
66
+ "package.json": packageJson(options),
67
+ "tsconfig.json": tsconfig(options),
68
+ ".gitignore": "node_modules/\ndist/\n# Types generated by dotctl\n.dot/\n",
69
+ "dot.json": dotJson(options),
70
+ "README.md": readme(options)
71
+ };
72
+ }
73
+ function packageJson({ packageName, dotVersion, workspace }) {
74
+ const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
75
+ return json({
76
+ name: packageName,
77
+ private: true,
78
+ type: "module",
79
+ scripts: {
80
+ dev: `dotctl dev${conditions}`,
81
+ build: `dotctl build${conditions}`,
82
+ preview: `dotctl preview${conditions}`,
83
+ typecheck: "tsc --noEmit"
84
+ },
85
+ dependencies: { "@monimesl/dot-runtime": dotVersion },
86
+ devDependencies: {
87
+ "@monimesl/dot-cli": dotVersion,
88
+ typescript: "^5.9.3"
89
+ }
90
+ });
91
+ }
92
+ function tsconfig({ workspace }) {
93
+ const compilerOptions = {
94
+ target: "ES2022",
95
+ module: "ESNext",
96
+ moduleResolution: "bundler",
97
+ lib: ["ES2022"],
98
+ types: ["@monimesl/dot-runtime/globals"],
99
+ strict: true,
100
+ noEmit: true,
101
+ skipLibCheck: true,
102
+ verbatimModuleSyntax: true
103
+ };
104
+ if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
105
+ return json({
106
+ compilerOptions,
107
+ include: [
108
+ "dot.ts",
109
+ "pages",
110
+ ".dot/pages.d.ts",
111
+ ".dot/env.d.ts"
112
+ ]
113
+ });
114
+ }
115
+ function dotJson({ name, packageName, template }) {
116
+ const dot = {
117
+ $schema: DOT_JSON_SCHEMA,
118
+ id: `com.example.${idWord(packageName)}`,
119
+ name,
120
+ version: "0.1.0"
121
+ };
122
+ if (template === "showcase") dot.network = { hosts: [
123
+ "itunes.apple.com",
124
+ "*.mzstatic.com",
125
+ "httpbin.org"
126
+ ] };
127
+ return json(dot);
128
+ }
129
+ const TEMPLATE_READMES = {
130
+ starter: `It starts with one page, \`index\`. It shows a greeting from \`.env\` and a counter you
131
+ can tap. Change it, and add pages as the dot grows. To see how each feature works, make a showcase
132
+ dot with \`npx dotctl create my-tour --template showcase\`.`,
133
+ showcase: `It starts as a showcase. The \`index\` page links to a page for each feature: layout,
134
+ text, images and inputs, then state, navigation, and Dash's alerts, network and files. Read a page
135
+ to see how it works. Then delete the pages you don't need, both from \`pages/\` and from
136
+ \`dot.ts\`.`
137
+ };
138
+ function readme({ name, template }) {
139
+ return `# ${name}
140
+
141
+ A dot: a mini app that runs inside Dash.
142
+
143
+ \`\`\`sh
144
+ npm install
145
+ npm run dev # run it in the simulator; it reloads on every change
146
+ npm run build # build it for Dash
147
+ \`\`\`
148
+
149
+ ${TEMPLATE_READMES[template]}
150
+
151
+ ## Pages
152
+
153
+ Each page is a folder under \`pages/\` with three files:
154
+
155
+ - \`view.dml\`: what the page shows. It has one root \`<View>\`, with everything else inside it.
156
+ - \`state.ts\`: the data the page starts with.
157
+ - \`controller.ts\`: how the data changes.
158
+
159
+ \`npx dotctl create page <path>\` adds a page, such as \`cart/checkout\`, with these three files. It
160
+ also lists the page in \`dot.ts\`. The dot opens on the first page in that list.
161
+
162
+ ## dot.json
163
+
164
+ \`dot.json\` describes the dot to Dash. Before you publish, change its \`id\` from
165
+ \`com.example.…\` to a reverse domain name you own. You can't change the id after publishing.
166
+ \`network.hosts\` lists every host the dot sends requests to with \`dash.network\`. Requests to any
167
+ other host fail. Your editor can complete the other fields from the schema the file points to.
168
+
169
+ ## .env
170
+
171
+ \`.env\` holds public configuration. Code in the dot reads each variable from \`env\`, such as
172
+ \`env.GREETING\`. Every variable is built into the dot, so anyone who has the dot can read it.
173
+ Never put a secret there. \`.env.development\` and \`.env.production\` override it for \`dev\` and
174
+ \`build\`.
175
+ `;
176
+ }
177
+ function idWord(packageName) {
178
+ return packageName.slice(0, 63).replace(/-+$/, "");
179
+ }
180
+ //#endregion
181
+ //#region src/create/create.ts
182
+ async function create({ name, dir, template = "starter" }) {
183
+ const root = resolve(dir);
184
+ if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty. Choose another folder`);
185
+ const packageName = toPackageName(name);
186
+ if (!packageName) throw new CommandError(`The name "${name}" needs at least one letter or digit`);
187
+ const workspace = isInDotWorkspace(root);
188
+ const source = templateDir(template);
189
+ await mkdir(root, { recursive: true });
190
+ for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
191
+ const generated = projectFiles({
192
+ name,
193
+ packageName,
194
+ dotVersion: workspace ? "workspace:*" : `^${cliVersion()}`,
195
+ workspace,
196
+ template
197
+ });
198
+ for (const [path, content] of Object.entries(generated)) await writeFile(join(root, path), content);
199
+ return {
200
+ root,
201
+ workspace
202
+ };
203
+ }
204
+ function toPackageName(name) {
205
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
206
+ }
207
+ function toDisplayName(dir) {
208
+ return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
209
+ }
210
+ function isInDotWorkspace(dir) {
211
+ let current = dirname(dir);
212
+ while (true) {
213
+ if (existsSync(join(current, "pnpm-workspace.yaml"))) {
214
+ const runtimeManifest = join(current, "packages/runtime/package.json");
215
+ if (!existsSync(runtimeManifest)) return false;
216
+ return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
217
+ }
218
+ const parent = dirname(current);
219
+ if (parent === current) return false;
220
+ current = parent;
221
+ }
222
+ }
223
+ function templateDir(template) {
224
+ const example = join(cliRoot(), "../../examples", template);
225
+ return existsSync(example) ? example : join(cliRoot(), "dist/templates", template);
226
+ }
227
+ function cliVersion() {
228
+ return readPackageJson(join(cliRoot(), "package.json")).version;
229
+ }
230
+ function readPackageJson(path) {
231
+ return JSON.parse(readFileSync(path, "utf8"));
232
+ }
233
+ function cliRoot() {
234
+ return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
235
+ }
236
+ //#endregion
237
+ //#region src/dot-root.ts
238
+ function findDotRoot(dir) {
239
+ while (true) {
240
+ if (existsSync(join(dir, "dot.json"))) return dir;
241
+ const parent = dirname(dir);
242
+ if (parent === dir) return;
243
+ dir = parent;
244
+ }
245
+ }
246
+ //#endregion
247
+ //#region src/create/page.ts
248
+ async function createPage(path, cwd) {
249
+ if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path. Use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
250
+ const root = findDotRoot(resolve(cwd));
251
+ if (root === void 0) throw new CommandError("There is no dot.json in this folder or any folder above it. Run this command inside a dot");
252
+ const dir = join(root, "pages", path);
253
+ if (existsSync(dir)) throw new CommandError(`pages/${path} already exists. Choose another path`);
254
+ await mkdir(dir, { recursive: true });
255
+ const files = [];
256
+ for (const [name, content] of Object.entries(PAGE_FILES)) {
257
+ const file = join(dir, name);
258
+ await writeFile(file, content);
259
+ files.push(file);
260
+ }
261
+ return {
262
+ dir,
263
+ files,
264
+ listing: await listPage(join(root, "dot.ts"), path)
265
+ };
266
+ }
267
+ async function listPage(dotFile, path) {
268
+ if (!existsSync(dotFile)) return "manual";
269
+ const source = await readFile(dotFile, "utf8");
270
+ let dot;
271
+ try {
272
+ dot = parseDotModule(source, dotFile);
273
+ } catch {
274
+ return "manual";
275
+ }
276
+ if (dot.pages.includes(path)) return "already";
277
+ const updated = withPage(source, dot.lastPage, path);
278
+ if (!listsPage(updated, dotFile, path)) return "manual";
279
+ await writeFile(dotFile, updated);
280
+ return "added";
281
+ }
282
+ function withPage(source, lastPage, path) {
283
+ const quote = source[lastPage.start] === "\"" ? "\"" : "'";
284
+ const page = `${quote}${path}${quote}`;
285
+ const lineStart = source.lastIndexOf("\n", lastPage.start - 1) + 1;
286
+ const indent = source.slice(lineStart, lastPage.start);
287
+ if (!/^\s*$/.test(indent)) return `${source.slice(0, lastPage.end)}, ${page}${source.slice(lastPage.end)}`;
288
+ const trailingComma = /^\s*,/.exec(source.slice(lastPage.end));
289
+ if (trailingComma) {
290
+ const end = lastPage.end + trailingComma[0].length;
291
+ return `${source.slice(0, end)}\n${indent}${page},${source.slice(end)}`;
292
+ }
293
+ return `${source.slice(0, lastPage.end)},\n${indent}${page}${source.slice(lastPage.end)}`;
294
+ }
295
+ function listsPage(source, dotFile, path) {
296
+ try {
297
+ return parseDotModule(source, dotFile).pages.includes(path);
298
+ } catch {
299
+ return false;
300
+ }
301
+ }
302
+ //#endregion
303
+ //#region src/create/package-manager.ts
304
+ const PACKAGE_MANAGERS = [
305
+ "npm",
306
+ "pnpm",
307
+ "yarn",
308
+ "bun"
309
+ ];
310
+ function isPackageManager(name) {
311
+ return PACKAGE_MANAGERS.includes(name);
312
+ }
313
+ function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
314
+ return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
315
+ }
316
+ function installedManagers(isInstalled = onPath) {
317
+ return PACKAGE_MANAGERS.filter(isInstalled);
318
+ }
319
+ function onPath(name) {
320
+ return spawnSync(name, ["--version"], {
321
+ stdio: "ignore",
322
+ shell: process.platform === "win32"
323
+ }).status === 0;
324
+ }
325
+ //#endregion
326
+ //#region src/device/developer-key.ts
327
+ function defaultDeveloperKeyPath() {
328
+ return process.env.DOT_DEVELOPER_KEY ?? join(homedir(), ".dot", "developer-key.pem");
329
+ }
330
+ function missingDeveloperKeyHint(path = defaultDeveloperKeyPath()) {
331
+ return ` There is no developer key at ${path}. Dash only opens signed links.\n Make one with OpenSSL 3. macOS's built-in openssl can't, so first run brew install openssl:
332
+
333
+ mkdir -p ${dirname(path)} && openssl genpkey -algorithm ed25519 -out ${path}\n\n Then run \`dotctl keys show\` and add the public key it prints to your Monime account.`;
334
+ }
335
+ async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
336
+ let pem;
337
+ try {
338
+ pem = await readFile(path, "utf8");
339
+ } catch (error) {
340
+ if (error.code === "ENOENT") return;
341
+ throw error;
342
+ }
343
+ const privateKey = createPrivateKey(pem);
344
+ if (privateKey.asymmetricKeyType !== "ed25519") throw new Error(`${path} is not an Ed25519 key. Make one with: openssl genpkey -algorithm ed25519 -out ${path}`);
345
+ return toDeveloperKey(privateKey);
346
+ }
347
+ function toDeveloperKey(privateKey) {
348
+ const raw = Buffer.from(createPublicKey(privateKey).export({ format: "jwk" }).x, "base64url");
349
+ return {
350
+ publicKey: raw.toString("base64"),
351
+ keyId: raw.subarray(0, 8).toString("hex"),
352
+ sign: (bytes) => sign(null, bytes, privateKey).toString("base64")
353
+ };
354
+ }
355
+ //#endregion
356
+ //#region src/device/server.ts
357
+ const DEVICE_SCHEME = "dotdev";
358
+ const WAIT_MS = 25e3;
359
+ const MAX_LOG_BYTES = 1048576;
360
+ const LINK_LIFETIME_SECONDS = 43200;
361
+ const BUILD_MAC_HEADER = "dot-build-mac";
362
+ function signedLinkMessage(server, secret, expires, keyId) {
363
+ return [
364
+ "dotdev-connect",
365
+ server,
366
+ secret,
367
+ String(expires),
368
+ keyId
369
+ ].join("\n");
370
+ }
371
+ function deviceToken(secret) {
372
+ return createHmac("sha256", Buffer.from(secret, "base64url")).update("dotdev-token", "utf8").digest("base64url");
373
+ }
374
+ function buildMac(secret, body) {
375
+ return createHmac("sha256", Buffer.from(secret, "base64url")).update(`dotdev-build\n${body}`, "utf8").digest("base64url");
376
+ }
377
+ async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {}, developerKey } = {}) {
378
+ if (address === void 0) throw new Error("This computer has no local network address, so devices can't reach it. Connect to a network and try again");
379
+ const secret = randomBytes(16).toString("base64url");
380
+ const token = deviceToken(secret);
381
+ let signature = null;
382
+ let build = { files: /* @__PURE__ */ new Map() };
383
+ let generation = 0;
384
+ let hotSince = 0;
385
+ let error = null;
386
+ const waiting = /* @__PURE__ */ new Set();
387
+ const connected = /* @__PURE__ */ new Set();
388
+ const announce = () => {
389
+ generation += 1;
390
+ for (const wake of waiting) wake();
391
+ };
392
+ const server = createServer((request, response) => {
393
+ const from = request.socket.remoteAddress ?? "a device";
394
+ if (request.method === "GET" && request.url === "/dev/link") {
395
+ if (signature) sendJson(response, signature);
396
+ else response.writeHead(404).end();
397
+ return;
398
+ }
399
+ if (!isAuthorized(request, token)) {
400
+ response.writeHead(401).end();
401
+ return;
402
+ }
403
+ const url = new URL(request.url ?? "/", "http://device");
404
+ const path = url.pathname;
405
+ if (request.method === "GET" && path === "/dev/build") {
406
+ if (!connected.has(from)) {
407
+ connected.add(from);
408
+ onConnect(from);
409
+ }
410
+ const files = Object.fromEntries([...build.files].map(([name, file]) => [name, file.hash]));
411
+ const body = JSON.stringify({
412
+ generation,
413
+ hotSince,
414
+ files,
415
+ error
416
+ });
417
+ response.writeHead(200, {
418
+ "content-type": "application/json",
419
+ [BUILD_MAC_HEADER]: buildMac(secret, body)
420
+ }).end(body);
421
+ } else if (request.method === "GET" && path.startsWith("/dev/files/")) {
422
+ if (url.searchParams.get("generation") !== String(generation)) {
423
+ response.writeHead(409).end();
424
+ return;
425
+ }
426
+ const name = path.slice(11).split("/").map(decodeURIComponent).join("/");
427
+ const file = build.files.get(name);
428
+ if (!file) {
429
+ response.writeHead(404).end();
430
+ return;
431
+ }
432
+ response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
433
+ } else if (request.method === "GET" && path === "/dev/changes") waitForChange(response, Number(url.searchParams.get("after")));
434
+ else if (request.method === "POST" && path === "/dev/log") readText(request).then((text) => {
435
+ for (const line of text.split("\n")) onLog(line, from);
436
+ response.writeHead(204).end();
437
+ }).catch(() => response.writeHead(413).end());
438
+ else response.writeHead(404).end();
439
+ });
440
+ const waitForChange = (response, after) => {
441
+ if (generation > after) {
442
+ sendJson(response, { generation });
443
+ return;
444
+ }
445
+ const wake = () => {
446
+ clearTimeout(timer);
447
+ waiting.delete(wake);
448
+ sendJson(response, { generation });
449
+ };
450
+ const timer = setTimeout(() => {
451
+ waiting.delete(wake);
452
+ response.writeHead(204).end();
453
+ }, WAIT_MS);
454
+ waiting.add(wake);
455
+ response.on("close", () => {
456
+ clearTimeout(timer);
457
+ waiting.delete(wake);
458
+ });
459
+ };
460
+ await new Promise((resolve, reject) => {
461
+ server.once("error", reject);
462
+ server.listen(port, address, () => resolve());
463
+ });
464
+ const { port: listening } = server.address();
465
+ const origin = `http://${address}:${listening}`;
466
+ if (developerKey) {
467
+ const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
468
+ const keyId = developerKey.keyId;
469
+ const message = signedLinkMessage(origin, secret, expires, keyId);
470
+ signature = {
471
+ expires,
472
+ keyId,
473
+ sig: developerKey.sign(Buffer.from(message, "utf8"))
474
+ };
475
+ }
476
+ return {
477
+ link: `${DEVICE_SCHEME}://connect?server=${encodeURIComponent(origin)}&secret=${secret}`,
478
+ async publish(dir, { hot = false } = {}) {
479
+ build = await readBuild(dir);
480
+ error = null;
481
+ announce();
482
+ if (!hot) hotSince = generation;
483
+ },
484
+ fail(message) {
485
+ error = message;
486
+ announce();
487
+ },
488
+ close() {
489
+ for (const wake of waiting) wake();
490
+ return new Promise((resolve) => {
491
+ server.close(() => resolve());
492
+ server.closeAllConnections();
493
+ });
494
+ }
495
+ };
496
+ }
497
+ function localNetworkAddress() {
498
+ for (const addresses of Object.values(networkInterfaces())) for (const candidate of addresses ?? []) if (candidate.family === "IPv4" && !candidate.internal) return candidate.address;
499
+ }
500
+ async function readBuild(dir) {
501
+ const files = /* @__PURE__ */ new Map();
502
+ const entries = await readdir(dir, {
503
+ recursive: true,
504
+ withFileTypes: true
505
+ });
506
+ for (const entry of entries) {
507
+ if (!entry.isFile() || entry.name.endsWith(".map") || entry.name.endsWith(".zip")) continue;
508
+ const path = join(entry.parentPath, entry.name);
509
+ const bytes = await readFile(path);
510
+ const hash = createHash("sha256").update(bytes).digest("base64url");
511
+ files.set(relative(dir, path).split(sep).join("/"), {
512
+ bytes,
513
+ hash
514
+ });
515
+ }
516
+ return { files };
517
+ }
518
+ function isAuthorized(request, token) {
519
+ const given = Buffer.from(request.headers.authorization ?? "");
520
+ const expected = Buffer.from(`Bearer ${token}`);
521
+ return given.length === expected.length && timingSafeEqual(given, expected);
522
+ }
523
+ function sendJson(response, body) {
524
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
525
+ }
526
+ async function readText(request) {
527
+ const chunks = [];
528
+ let size = 0;
529
+ for await (const chunk of request) {
530
+ size += chunk.length;
531
+ if (size > MAX_LOG_BYTES) throw new Error(`A device sent more than ${MAX_LOG_BYTES} bytes of log at once`);
532
+ chunks.push(chunk);
533
+ }
534
+ return Buffer.concat(chunks).toString("utf8");
535
+ }
536
+ //#endregion
537
+ //#region src/simulator.ts
538
+ function startDotSimulator(project, outDir, port) {
539
+ return startSimulator({
540
+ dotDir: outDir,
541
+ reactHostDir: installedRuntime(project.root).react.dir,
542
+ name: project.dotJson.name,
543
+ port,
544
+ storageDir: join(project.root, ".dot", "files")
545
+ });
546
+ }
547
+ //#endregion
548
+ //#region src/dev.ts
549
+ const CHANGE_DEBOUNCE_MS = 50;
550
+ async function dev({ port, device, ...options }) {
551
+ const project = await loadProject(options.root);
552
+ const outDir = resolve(project.root, options.outDir ?? "dist");
553
+ const buildOptions = {
554
+ mode: "development",
555
+ ...options,
556
+ outDir
557
+ };
558
+ const buildApp = async () => {
559
+ const started = performance.now();
560
+ try {
561
+ printWarnings(await build(buildOptions));
562
+ printBuildTime(started);
563
+ return null;
564
+ } catch (error) {
565
+ const message = describeError(error).replaceAll(project.root + sep, "");
566
+ console.error(`error: ${message}`);
567
+ return message;
568
+ }
569
+ };
570
+ const firstError = await buildApp();
571
+ const simulator = await startDotSimulator(project, outDir, port);
572
+ if (firstError) simulator.error(firstError);
573
+ console.log(`\n Simulator running at: ${simulator.url}\n`);
574
+ let devices;
575
+ if (device) {
576
+ const developerKey = await loadDeveloperKey();
577
+ devices = await startDeviceServer({
578
+ developerKey,
579
+ onConnect: (from) => console.log(`A device connected from ${from}`),
580
+ onLog: (line) => console.log(`device ${line}`)
581
+ });
582
+ if (firstError) devices.fail(firstError);
583
+ else await devices.publish(outDir);
584
+ printDeviceLink(devices.link);
585
+ if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
586
+ else console.log(`${missingDeveloperKeyHint()}\n`);
587
+ }
588
+ const changed = /* @__PURE__ */ new Set();
589
+ const pagesDir = join(project.root, PAGES_DIR) + sep;
590
+ const rebuilds = new RebuildQueue(async () => {
591
+ const building = [...changed];
592
+ const error = await buildApp();
593
+ if (error) {
594
+ simulator.error(error);
595
+ devices?.fail(error);
596
+ return;
597
+ }
598
+ for (const path of building) changed.delete(path);
599
+ const hot = building.every((path) => path.startsWith(pagesDir));
600
+ if (hot) simulator.hotReload();
601
+ else simulator.reload();
602
+ await devices?.publish(outDir, { hot });
603
+ });
604
+ let debounceTimer;
605
+ const watcher = watch(project.root, { recursive: true }, (_event, file) => {
606
+ if (!file || isIgnored(file)) return;
607
+ const path = resolve(project.root, file);
608
+ if (path === outDir || path.startsWith(outDir + sep)) return;
609
+ changed.add(path);
610
+ clearTimeout(debounceTimer);
611
+ debounceTimer = setTimeout(() => {
612
+ console.log(`Changed ${relative(project.root, path)}`);
613
+ rebuilds.request();
614
+ }, CHANGE_DEBOUNCE_MS);
615
+ });
616
+ return async () => {
617
+ clearTimeout(debounceTimer);
618
+ watcher.close();
619
+ await Promise.all([simulator.close(), devices?.close()]);
620
+ };
621
+ }
622
+ function printDeviceLink(link) {
623
+ console.log(" To open it in Dash, turn on Enable Dot Debugging in Developer Settings.\n Then tap Live Test on the Dots tab and scan this code.\n");
624
+ console.log(renderUnicodeCompact(link, { border: 1 }).replace(/^/gm, " "));
625
+ console.log(`\n ${link}\n`);
626
+ }
627
+ var RebuildQueue = class {
628
+ #rebuild;
629
+ #building = false;
630
+ #changedWhileBuilding = false;
631
+ constructor(rebuild) {
632
+ this.#rebuild = rebuild;
633
+ }
634
+ request() {
635
+ if (this.#building) {
636
+ this.#changedWhileBuilding = true;
637
+ return;
638
+ }
639
+ this.#build();
640
+ }
641
+ async #build() {
642
+ this.#building = true;
643
+ this.#changedWhileBuilding = false;
644
+ try {
645
+ await this.#rebuild();
646
+ } finally {
647
+ this.#building = false;
648
+ }
649
+ if (this.#changedWhileBuilding) this.request();
650
+ }
651
+ };
652
+ function isIgnored(file) {
653
+ return file.split(/[\\/]/).some((segment) => {
654
+ if (segment === ".env" || segment.startsWith(".env.")) return false;
655
+ return segment === "node_modules" || segment.startsWith(".");
656
+ });
657
+ }
658
+ //#endregion
659
+ //#region src/preview.ts
660
+ async function preview({ port, ...options }) {
661
+ const project = await loadProject(options.root);
662
+ const outDir = resolve(project.root, options.outDir ?? "dist");
663
+ const started = performance.now();
664
+ printWarnings(await build({
665
+ ...options,
666
+ outDir,
667
+ mode: "production"
668
+ }));
669
+ printBuildTime(started);
670
+ const simulator = await startDotSimulator(project, outDir, port);
671
+ console.log(`\n Simulator running at: ${simulator.url}\n`);
672
+ return simulator;
673
+ }
674
+ //#endregion
675
+ //#region src/prompts.ts
676
+ const TERMINAL = {
677
+ input: process.stdin,
678
+ output: process.stdout
679
+ };
680
+ const ERASE_BELOW = "\x1B[0J";
681
+ const HIDE_CURSOR = "\x1B[?25l";
682
+ const SHOW_CURSOR = "\x1B[?25h";
683
+ const cursorUp = (lines) => `\x1b[${lines}A`;
684
+ async function select(question, choices, fallback, { input, output } = TERMINAL) {
685
+ if (!input.isTTY) return fallback;
686
+ let current = Math.max(0, choices.findIndex((choice) => choice.value === fallback));
687
+ const width = choices.some((choice) => choice.hint) ? Math.max(...choices.map((choice) => choice.value.length)) : 0;
688
+ const row = (choice, index) => {
689
+ const name = choice.value.padEnd(width);
690
+ const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
691
+ const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
692
+ if (index === current) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], name)}${hint}${defaultMark}`;
693
+ return ` ${name}${hint}${defaultMark}`;
694
+ };
695
+ let drawnLines = 0;
696
+ const draw = (lines) => {
697
+ output.write(`${drawnLines ? cursorUp(drawnLines) : ""}${ERASE_BELOW}${lines.join("\n")}\n`);
698
+ drawnLines = lines.length;
699
+ };
700
+ const drawList = () => draw([`${styleText("bold", question)} ${styleText("dim", "↑/↓ to move, Enter to choose")}`, ...choices.map(row)]);
701
+ emitKeypressEvents(input);
702
+ input.setRawMode(true);
703
+ input.resume();
704
+ output.write(HIDE_CURSOR);
705
+ drawList();
706
+ return new Promise((resolve) => {
707
+ const onKeypress = (_text, key) => {
708
+ if (key.ctrl && key.name === "c") {
709
+ output.write(`${SHOW_CURSOR}\n`);
710
+ process.exit(130);
711
+ }
712
+ if (key.name === "up" || key.name === "k") {
713
+ current = (current - 1 + choices.length) % choices.length;
714
+ drawList();
715
+ } else if (key.name === "down" || key.name === "j") {
716
+ current = (current + 1) % choices.length;
717
+ drawList();
718
+ } else if (key.name === "return" || key.name === "enter") {
719
+ const { value } = choices[current];
720
+ input.off("keypress", onKeypress);
721
+ input.setRawMode(false);
722
+ input.pause();
723
+ draw([`${styleText("bold", question)} ${styleText("cyan", value)}`]);
724
+ output.write(SHOW_CURSOR);
725
+ resolve(value);
726
+ }
727
+ };
728
+ input.on("keypress", onKeypress);
729
+ });
730
+ }
731
+ async function confirm(question, fallback, terminal = TERMINAL) {
732
+ if (!terminal.input.isTTY) return false;
733
+ return await select(question, [{ value: "Yes" }, { value: "No" }], fallback ? "Yes" : "No", terminal) === "Yes";
734
+ }
735
+ async function ask(question, fallback, { input, output } = TERMINAL) {
736
+ if (!input.isTTY) return fallback;
737
+ const hint = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
738
+ const readline = createInterface({
739
+ input,
740
+ output
741
+ });
742
+ try {
743
+ return (await readline.question(`${styleText("bold", question)}${hint}: `)).trim() || fallback;
744
+ } catch (error) {
745
+ if (error.name === "AbortError") return;
746
+ throw error;
747
+ } finally {
748
+ readline.close();
749
+ }
750
+ }
751
+ //#endregion
752
+ //#region src/cli.ts
753
+ const USAGE = `Usage:
754
+ dotctl create [dir] [--name <name>] [--template <starter|showcase>]
755
+ [--pm <npm|pnpm|yarn|bun>]
756
+ Create a dot. Asks for its name and template:
757
+ starter is one page to build on, showcase has a
758
+ page for each feature
759
+ dotctl create page <path> Add a page to the dot you are in. Writes
760
+ pages/<path>/ with view.dml, state.ts and controller.ts
761
+ dotctl build [root] [options] Build the minified bundles the host app runs
762
+ dotctl dev [root] [options] Run the dot in the browser simulator and rebuild on
763
+ every change
764
+ dotctl preview [root] [options] Build as \`build\` does, then run that build in the
765
+ browser simulator, exactly as the host app gets it
766
+ dotctl keys show Show this computer's developer key: its public key
767
+ and key ID. The key signs the links that
768
+ \`dev --device\` shows, so Dash will open them
769
+ dotctl lsp [--stdio] Start the .dml language server for editors. It talks
770
+ over stdin and stdout
771
+
772
+ Options:
773
+ --out-dir <dir> Output directory (default: <root>/dist)
774
+ --conditions <list> Extra export conditions, separated by commas
775
+ --port <port> Simulator port for \`dev\` and \`preview\`. If it is taken, the
776
+ next free port is used (default: ${DEFAULT_PORT})
777
+ --device With \`dev\`, also serve the dot to the Dot example apps on phones
778
+ on your local network, through a link and QR code. The apps
779
+ reload after each build
780
+ -h, --help Show this message`;
781
+ async function main(argv) {
782
+ const { values, positionals } = parseArgs({
783
+ args: argv,
784
+ allowPositionals: true,
785
+ options: {
786
+ "out-dir": { type: "string" },
787
+ conditions: { type: "string" },
788
+ port: { type: "string" },
789
+ device: { type: "boolean" },
790
+ name: { type: "string" },
791
+ template: { type: "string" },
792
+ pm: { type: "string" },
793
+ help: {
794
+ type: "boolean",
795
+ short: "h"
796
+ },
797
+ stdio: { type: "boolean" }
798
+ }
799
+ });
800
+ const [command, target, pagePath] = positionals;
801
+ if (command === "lsp" && !values.help) {
802
+ const { serveStdio } = await import("./server-Dbrgkelo.js");
803
+ serveStdio();
804
+ return new Promise(() => {});
805
+ }
806
+ if (command === "keys" && !values.help) return keys(target);
807
+ if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
808
+ if (command === "create" && !values.help) return createApp(target, values.name, values.template, values.pm);
809
+ if (values.help || command !== "build" && command !== "dev" && command !== "preview") {
810
+ console.log(USAGE);
811
+ return values.help ? 0 : 1;
812
+ }
813
+ const port = values.port === void 0 ? void 0 : Number(values.port);
814
+ if (port !== void 0 && !Number.isInteger(port)) {
815
+ console.error(`--port must be a whole number, not "${values.port}"`);
816
+ return 1;
817
+ }
818
+ const options = { root: target ?? "." };
819
+ if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
820
+ if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
821
+ if (command === "dev") {
822
+ const stop = await dev({
823
+ ...options,
824
+ port,
825
+ device: values.device
826
+ });
827
+ await new Promise((resolve) => process.once("SIGINT", resolve));
828
+ await stop();
829
+ return 0;
830
+ }
831
+ if (command === "preview") {
832
+ if (values.device) {
833
+ console.error("--device only works with dotctl dev");
834
+ return 1;
835
+ }
836
+ const simulator = await preview({
837
+ ...options,
838
+ port
839
+ });
840
+ await new Promise((resolve) => process.once("SIGINT", resolve));
841
+ await simulator.close();
842
+ return 0;
843
+ }
844
+ const started = performance.now();
845
+ const result = await build(options);
846
+ for (const file of result.files.filter((file) => !file.endsWith(".map"))) console.log(` ${relative(process.cwd(), file)}`);
847
+ printWarnings(result);
848
+ printBuildTime(started);
849
+ return 0;
850
+ }
851
+ async function keys(action) {
852
+ if (action !== "show") {
853
+ console.error("Usage: dotctl keys show");
854
+ return 1;
855
+ }
856
+ const path = defaultDeveloperKeyPath();
857
+ const key = await loadDeveloperKey(path);
858
+ if (!key) {
859
+ console.error(missingDeveloperKeyHint(path));
860
+ return 1;
861
+ }
862
+ console.log(`\n Public key: ${key.publicKey}\n Key ID: ${key.keyId}\n`);
863
+ return 0;
864
+ }
865
+ async function addPage(path) {
866
+ if (path === void 0) {
867
+ console.error("Give the page a path, such as: dotctl create page cart/checkout");
868
+ return 1;
869
+ }
870
+ if (path.startsWith("/")) path = path.substring(1);
871
+ if (path.endsWith("/")) path = path.substring(0, path.length - 1);
872
+ const { files, listing } = await createPage(path, process.cwd());
873
+ for (const file of files) console.log(` ${relative(process.cwd(), file)}`);
874
+ if (listing === "added") console.log(`\nListed '${path}' in dot.ts.`);
875
+ else if (listing === "manual") console.log(`\nAdd it to the pages in dot.ts so it gets built: defineDot({ pages: [..., '${path}'] })`);
876
+ return 0;
877
+ }
878
+ async function createApp(dir, name, templateFlag, packageManagerFlag) {
879
+ if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
880
+ console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
881
+ return 1;
882
+ }
883
+ if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
884
+ console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
885
+ return 1;
886
+ }
887
+ name ??= await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
888
+ if (!name) {
889
+ console.error("The dot needs a name. Pass one with: dotctl create [dir] --name \"My Dot\"");
890
+ return 1;
891
+ }
892
+ const template = templateFlag ?? await chooseTemplate();
893
+ const { root, workspace } = await create({
894
+ name,
895
+ dir: dir ?? toPackageName(name),
896
+ template
897
+ });
898
+ const path = relative(process.cwd(), root) || ".";
899
+ console.log(`\nCreated ${name} in ${path}`);
900
+ if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
901
+ let packageManager = workspace ? "pnpm" : packageManagerFlag ?? launchedBy();
902
+ console.log();
903
+ if (await confirm("Install and start it now?", true)) {
904
+ if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
905
+ if (await run(packageManager, ["install"], root) === 0) return run(packageManager, ["run", "dev"], root);
906
+ console.error(`\n${packageManager} install failed. Fix the problem above, then run the steps below.`);
907
+ }
908
+ console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
909
+ return 0;
910
+ }
911
+ function run(command, args, cwd) {
912
+ return new Promise((resolvePromise, reject) => {
913
+ const child = spawn(command, args, {
914
+ cwd,
915
+ stdio: "inherit",
916
+ shell: process.platform === "win32"
917
+ });
918
+ child.on("error", reject);
919
+ child.on("exit", (code) => resolvePromise(code ?? 1));
920
+ });
921
+ }
922
+ function chooseTemplate() {
923
+ console.log();
924
+ return select("Template?", TEMPLATES.map((template) => ({
925
+ value: template,
926
+ hint: TEMPLATE_HINTS[template]
927
+ })), "starter");
928
+ }
929
+ async function choosePackageManager(suggested) {
930
+ const installed = installedManagers();
931
+ if (installed.length < 2) return installed[0] ?? suggested;
932
+ const fallback = installed.includes(suggested) ? suggested : installed[0];
933
+ return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
934
+ }
935
+ main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
936
+ console.error(`error: ${describeError(error)}`);
937
+ process.exit(1);
938
+ });
939
+ //#endregion
940
+ export { findDotRoot as t };
941
+
942
+ //# sourceMappingURL=cli.js.map