@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.
- package/README.md +34 -15
- package/dist/cli.js +252 -39
- package/dist/cli.js.map +1 -1
- package/dist/{server-z7M4FLjf.js → server-DFOUQnLA.js} +437 -60
- package/dist/server-DFOUQnLA.js.map +1 -0
- package/dist/{template → templates/showcase}/dot.ts +3 -0
- package/dist/templates/showcase/pages/carousel/controller.ts +10 -0
- package/dist/templates/showcase/pages/carousel/state.ts +40 -0
- package/dist/templates/showcase/pages/carousel/view.dml +37 -0
- package/dist/templates/showcase/pages/filters/controller.ts +28 -0
- package/dist/templates/showcase/pages/filters/state.ts +72 -0
- package/dist/templates/showcase/pages/filters/view.dml +43 -0
- package/dist/{template → templates/showcase}/pages/index/state.ts +19 -1
- package/dist/{template → templates/showcase}/pages/layout/controller.ts +3 -0
- package/dist/{template → templates/showcase}/pages/layout/state.ts +2 -0
- package/dist/{template → templates/showcase}/pages/layout/view.dml +17 -0
- package/dist/{template → templates/showcase}/pages/navigation/controller.ts +22 -0
- package/dist/{template → templates/showcase}/pages/navigation/state.ts +4 -0
- package/dist/{template → templates/showcase}/pages/navigation/view.dml +10 -0
- package/dist/templates/showcase/pages/payments/controller.ts +42 -0
- package/dist/templates/showcase/pages/payments/state.ts +10 -0
- package/dist/templates/showcase/pages/payments/view.dml +35 -0
- package/dist/templates/starter/.env +3 -0
- package/dist/templates/starter/dot.ts +3 -0
- package/dist/templates/starter/pages/index/controller.ts +9 -0
- package/dist/templates/starter/pages/index/state.ts +4 -0
- package/dist/templates/starter/pages/index/view.dml +16 -0
- package/dist/templates/starter/statics/logo.png +0 -0
- package/package.json +8 -7
- package/dist/server-z7M4FLjf.js.map +0 -1
- /package/dist/{template → templates/showcase}/.env +0 -0
- /package/dist/{template → templates/showcase}/pages/device/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/device/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/device/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/feedback/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/feedback/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/feedback/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/files/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/files/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/files/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/forms/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/forms/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/forms/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/images/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/images/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/images/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/index/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/index/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/lists/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/lists/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/lists/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/network/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/network/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/network/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/state/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/state/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/state/view.dml +0 -0
- /package/dist/{template → templates/showcase}/pages/text/controller.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/text/state.ts +0 -0
- /package/dist/{template → templates/showcase}/pages/text/view.dml +0 -0
- /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]
|
|
8
|
-
dotctl
|
|
9
|
-
dotctl
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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":
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
172
|
+
const source = templateDir(template);
|
|
155
173
|
await mkdir(root, { recursive: true });
|
|
156
|
-
for (const path of TEMPLATE_FILES) await cp(join(
|
|
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
|
|
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)
|
|
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>] [--
|
|
382
|
-
|
|
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-
|
|
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
|
-
|
|
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(
|
|
739
|
+
const answer = await ask(`${question} ${options.join(" / ")}`, fallback);
|
|
528
740
|
if (answer === void 0) return fallback;
|
|
529
|
-
|
|
530
|
-
|
|
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) => {
|