@monimesl/dot-cli 0.1.0-alpha.19 → 0.1.0-alpha.21
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/dist/cli.js +381 -261
- package/dist/cli.js.map +1 -1
- package/dist/{server-uIyYmYnp.js → server-B9-F93mS.js} +539 -464
- package/dist/server-B9-F93mS.js.map +1 -0
- package/dist/templates/showcase/dot.ts +2 -0
- package/dist/templates/showcase/pages/basics/bars/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/events/controller.ts +4 -4
- package/dist/templates/showcase/pages/basics/events/view.dml +2 -2
- package/dist/templates/showcase/pages/basics/hooks/controller.ts +6 -0
- package/dist/templates/showcase/pages/basics/hooks/view.dml +5 -0
- package/dist/templates/showcase/pages/basics/navigation/controller.ts +7 -0
- package/dist/templates/showcase/pages/basics/navigation/state.ts +2 -0
- package/dist/templates/showcase/pages/basics/navigation/view.dml +11 -0
- package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +12 -0
- package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
- package/dist/templates/showcase/pages/basics/pick-genre/view.dml +9 -0
- package/dist/templates/showcase/pages/components/carousel/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/feedback/controller.ts +12 -2
- package/dist/templates/showcase/pages/components/feedback/state.ts +33 -0
- package/dist/templates/showcase/pages/components/feedback/view.dml +21 -1
- package/dist/templates/showcase/pages/components/forms/controller.ts +22 -22
- package/dist/templates/showcase/pages/components/forms/state.ts +3 -3
- package/dist/templates/showcase/pages/components/forms/view.dml +9 -2
- package/dist/templates/showcase/pages/components/icons/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/images/controller.ts +3 -1
- package/dist/templates/showcase/pages/components/images/view.dml +13 -0
- package/dist/templates/showcase/pages/components/layout/controller.ts +2 -2
- package/dist/templates/showcase/pages/components/lists/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/lists/view.dml +18 -0
- package/dist/templates/showcase/pages/components/money/controller.ts +6 -8
- package/dist/templates/showcase/pages/components/sheets/controller.ts +2 -2
- package/dist/templates/showcase/pages/components/tabs/controller.ts +22 -10
- package/dist/templates/showcase/pages/components/tabs/state.ts +1 -0
- package/dist/templates/showcase/pages/components/tabs/view.dml +4 -3
- package/dist/templates/showcase/pages/dash/cache/controller.ts +2 -2
- package/dist/templates/showcase/pages/dash/calls/controller.ts +7 -6
- package/dist/templates/showcase/pages/dash/calls/state.ts +1 -1
- package/dist/templates/showcase/pages/dash/calls/view.dml +3 -2
- package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
- package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
- package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
- package/dist/templates/showcase/pages/dash/files/controller.ts +15 -13
- package/dist/templates/showcase/pages/dash/files/state.ts +3 -6
- package/dist/templates/showcase/pages/dash/media/controller.ts +16 -13
- package/dist/templates/showcase/pages/dash/network/controller.ts +71 -16
- package/dist/templates/showcase/pages/dash/network/state.ts +10 -0
- package/dist/templates/showcase/pages/dash/network/view.dml +32 -4
- package/dist/templates/showcase/pages/dash/secrets/controller.ts +1 -1
- package/dist/templates/showcase/pages/dash/system/controller.ts +32 -5
- package/dist/templates/showcase/pages/dash/system/state.ts +6 -2
- package/dist/templates/showcase/pages/dash/system/view.dml +39 -1
- package/dist/templates/showcase/pages/dash/ui/controller.ts +23 -23
- package/dist/templates/showcase/pages/dash/ui/view.dml +1 -1
- package/dist/templates/showcase/pages/index/controller.ts +1 -1
- package/dist/templates/showcase/pages/index/state.ts +17 -10
- package/package.json +6 -6
- package/dist/server-uIyYmYnp.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parseArgs, styleText } from "node:util";
|
|
5
|
-
import { CompileError, PAGES_DIR, build, installedRuntime, isDotPackage, loadProject } from "@monimesl/dot-compiler";
|
|
5
|
+
import { CompileError, DOT_FILE, DOT_JSON_FILE, PAGES_DIR, build, installedRuntime, isDotPackage, loadProject } from "@monimesl/dot-compiler";
|
|
6
6
|
import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
8
8
|
import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
@@ -21,11 +21,19 @@ var CommandError = class extends Error {
|
|
|
21
21
|
};
|
|
22
22
|
function describeError(error) {
|
|
23
23
|
if (error instanceof CommandError || error instanceof CompileError || error instanceof DmlParseError || error instanceof ScriptParseError) return error.message;
|
|
24
|
-
if (error
|
|
24
|
+
if (isArgumentError(error)) {
|
|
25
25
|
const [firstSentence] = error.message.split(". ");
|
|
26
26
|
return `${firstSentence}. See dotctl --help.`;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
29
|
+
return String(error);
|
|
30
|
+
}
|
|
31
|
+
function isArgumentError(error) {
|
|
32
|
+
if (!(error instanceof Error) || !("code" in error)) return false;
|
|
33
|
+
return typeof error.code === "string" && error.code.startsWith("ERR_PARSE_ARGS");
|
|
34
|
+
}
|
|
35
|
+
function printFiles(files) {
|
|
36
|
+
for (const file of files) console.log(` ${relative(process.cwd(), file)}`);
|
|
29
37
|
}
|
|
30
38
|
function printWarnings({ warnings }) {
|
|
31
39
|
for (const warning of warnings) console.warn(`warning: ${warning.message}`);
|
|
@@ -33,6 +41,26 @@ function printWarnings({ warnings }) {
|
|
|
33
41
|
function printBuildTime(startedAt) {
|
|
34
42
|
console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
|
|
35
43
|
}
|
|
44
|
+
function printSimulatorUrl(url) {
|
|
45
|
+
console.log(`\n Simulator running at: ${url}\n`);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/utils.ts
|
|
49
|
+
function findFolderWith(fileName, dir) {
|
|
50
|
+
let current = dir;
|
|
51
|
+
while (true) {
|
|
52
|
+
if (existsSync(join(current, fileName))) return current;
|
|
53
|
+
const parent = dirname(current);
|
|
54
|
+
if (parent === current) return;
|
|
55
|
+
current = parent;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function findDotRoot(dir) {
|
|
59
|
+
return findFolderWith(DOT_JSON_FILE, dir);
|
|
60
|
+
}
|
|
61
|
+
function resolveOutDir(root, outDir) {
|
|
62
|
+
return resolve(root, outDir ?? "dist");
|
|
63
|
+
}
|
|
36
64
|
//#endregion
|
|
37
65
|
//#region src/create/template.ts
|
|
38
66
|
const TEMPLATES = ["starter", "showcase"];
|
|
@@ -60,8 +88,8 @@ const TEMPLATE_FILES = [
|
|
|
60
88
|
".env"
|
|
61
89
|
];
|
|
62
90
|
const DOT_JSON_SCHEMA = "https://unpkg.com/@monimesl/dot-runtime/dot.schema.json";
|
|
63
|
-
const
|
|
64
|
-
function
|
|
91
|
+
const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
|
|
92
|
+
function generatedFiles(options) {
|
|
65
93
|
return {
|
|
66
94
|
"package.json": packageJson(options),
|
|
67
95
|
"tsconfig.json": tsconfig(options),
|
|
@@ -72,7 +100,7 @@ function projectFiles(options) {
|
|
|
72
100
|
}
|
|
73
101
|
function packageJson({ packageName, dotVersion, workspace }) {
|
|
74
102
|
const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
|
|
75
|
-
return
|
|
103
|
+
return formatJson({
|
|
76
104
|
name: packageName,
|
|
77
105
|
private: true,
|
|
78
106
|
type: "module",
|
|
@@ -102,7 +130,7 @@ function tsconfig({ workspace }) {
|
|
|
102
130
|
verbatimModuleSyntax: true
|
|
103
131
|
};
|
|
104
132
|
if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
|
|
105
|
-
return
|
|
133
|
+
return formatJson({
|
|
106
134
|
compilerOptions,
|
|
107
135
|
include: [
|
|
108
136
|
"dot.ts",
|
|
@@ -112,7 +140,6 @@ function tsconfig({ workspace }) {
|
|
|
112
140
|
]
|
|
113
141
|
});
|
|
114
142
|
}
|
|
115
|
-
const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
|
|
116
143
|
function dotJson({ name, dotPackage, template }) {
|
|
117
144
|
const dot = {
|
|
118
145
|
id: UNREGISTERED_DOT_ID,
|
|
@@ -125,7 +152,8 @@ function dotJson({ name, dotPackage, template }) {
|
|
|
125
152
|
dot.permissions = {
|
|
126
153
|
camera: "Take a photo for your concert ticket, or scan a ticket's QR code",
|
|
127
154
|
photoLibrary: "Choose a photo for your concert ticket",
|
|
128
|
-
files: "Attach your concert ticket"
|
|
155
|
+
files: "Attach your concert ticket",
|
|
156
|
+
contacts: "Choose a friend to send a ticket to"
|
|
129
157
|
};
|
|
130
158
|
dot.network = { hosts: [
|
|
131
159
|
"itunes.apple.com",
|
|
@@ -133,7 +161,7 @@ function dotJson({ name, dotPackage, template }) {
|
|
|
133
161
|
"httpbin.org"
|
|
134
162
|
] };
|
|
135
163
|
}
|
|
136
|
-
return
|
|
164
|
+
return formatJson(dot);
|
|
137
165
|
}
|
|
138
166
|
const TEMPLATE_READMES = {
|
|
139
167
|
starter: `It starts with one page, \`index\`. It shows a greeting from \`.env\` and a counter you
|
|
@@ -172,7 +200,7 @@ also lists the page in \`dot.ts\`. The dot opens on the first page in that list.
|
|
|
172
200
|
|
|
173
201
|
\`dot.json\` describes the dot to Dash. Its \`package\` is a reverse domain name you own, such as
|
|
174
202
|
\`com.mysalone.thing\`. You can't change it after publishing. To publish, register the dot with
|
|
175
|
-
Monime first, and replace its \`id\`,
|
|
203
|
+
Monime first, and replace its \`id\`, \`${UNREGISTERED_DOT_ID}\`, with the ID Monime gives you.
|
|
176
204
|
\`network.hosts\` lists every host the dot sends requests to with \`dash.network\`. Requests to any
|
|
177
205
|
other host fail. Your editor can complete the other fields from the schema the file points to.
|
|
178
206
|
|
|
@@ -180,7 +208,7 @@ other host fail. Your editor can complete the other fields from the schema the f
|
|
|
180
208
|
|
|
181
209
|
Dash opens the dot unless it was built with a newer major version of \`@monimesl/dot-runtime\` than
|
|
182
210
|
the one Dash has. A newer minor version can add Dash calls that an older Dash doesn't have yet.
|
|
183
|
-
There, such a call fails with a \`
|
|
211
|
+
There, such a call fails with a \`DashError\` whose \`code\` is \`unsupported\`, so catch it and carry
|
|
184
212
|
on without the feature.
|
|
185
213
|
|
|
186
214
|
## .env
|
|
@@ -194,6 +222,9 @@ Never put a secret there. \`.env.development\` and \`.env.production\` override
|
|
|
194
222
|
function suggestedDotPackage(packageName) {
|
|
195
223
|
return `com.example.${packageName.slice(0, 63).replace(/-+$/, "")}`;
|
|
196
224
|
}
|
|
225
|
+
function formatJson(value) {
|
|
226
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
227
|
+
}
|
|
197
228
|
//#endregion
|
|
198
229
|
//#region src/create/create.ts
|
|
199
230
|
const DOT_PACKAGE_RULE = "Use lowercase words joined by dots, such as com.mysalone.thing.";
|
|
@@ -208,7 +239,7 @@ async function create({ name, dir, dotPackage, template = "starter" }) {
|
|
|
208
239
|
const source = templateDir(template);
|
|
209
240
|
await mkdir(root, { recursive: true });
|
|
210
241
|
for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
|
|
211
|
-
const generated =
|
|
242
|
+
const generated = generatedFiles({
|
|
212
243
|
name,
|
|
213
244
|
packageName,
|
|
214
245
|
dotPackage,
|
|
@@ -229,17 +260,11 @@ function toDisplayName(dir) {
|
|
|
229
260
|
return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
230
261
|
}
|
|
231
262
|
function isInDotWorkspace(dir) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
|
|
238
|
-
}
|
|
239
|
-
const parent = dirname(current);
|
|
240
|
-
if (parent === current) return false;
|
|
241
|
-
current = parent;
|
|
242
|
-
}
|
|
263
|
+
const workspaceRoot = findFolderWith("pnpm-workspace.yaml", dirname(dir));
|
|
264
|
+
if (workspaceRoot === void 0) return false;
|
|
265
|
+
const runtimeManifest = join(workspaceRoot, "packages/runtime/package.json");
|
|
266
|
+
if (!existsSync(runtimeManifest)) return false;
|
|
267
|
+
return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
|
|
243
268
|
}
|
|
244
269
|
function templateDir(template) {
|
|
245
270
|
const example = join(cliRoot(), "../../examples", template);
|
|
@@ -255,22 +280,12 @@ function cliRoot() {
|
|
|
255
280
|
return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
|
|
256
281
|
}
|
|
257
282
|
//#endregion
|
|
258
|
-
//#region src/dot-root.ts
|
|
259
|
-
function findDotRoot(dir) {
|
|
260
|
-
while (true) {
|
|
261
|
-
if (existsSync(join(dir, "dot.json"))) return dir;
|
|
262
|
-
const parent = dirname(dir);
|
|
263
|
-
if (parent === dir) return;
|
|
264
|
-
dir = parent;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
//#endregion
|
|
268
283
|
//#region src/create/page.ts
|
|
269
284
|
async function createPage(path, cwd) {
|
|
270
285
|
if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path. Use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
|
|
271
286
|
const root = findDotRoot(resolve(cwd));
|
|
272
287
|
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");
|
|
273
|
-
const dir = join(root,
|
|
288
|
+
const dir = join(root, PAGES_DIR, path);
|
|
274
289
|
if (existsSync(dir)) throw new CommandError(`pages/${path} already exists. Choose another path`);
|
|
275
290
|
await mkdir(dir, { recursive: true });
|
|
276
291
|
const files = [];
|
|
@@ -282,10 +297,10 @@ async function createPage(path, cwd) {
|
|
|
282
297
|
return {
|
|
283
298
|
dir,
|
|
284
299
|
files,
|
|
285
|
-
listing: await
|
|
300
|
+
listing: await addToDotPages(join(root, DOT_FILE), path)
|
|
286
301
|
};
|
|
287
302
|
}
|
|
288
|
-
async function
|
|
303
|
+
async function addToDotPages(dotFile, path) {
|
|
289
304
|
if (!existsSync(dotFile)) return "manual";
|
|
290
305
|
const source = await readFile(dotFile, "utf8");
|
|
291
306
|
let dot;
|
|
@@ -295,23 +310,25 @@ async function listPage(dotFile, path) {
|
|
|
295
310
|
return "manual";
|
|
296
311
|
}
|
|
297
312
|
if (dot.pages.includes(path)) return "already";
|
|
298
|
-
const updated =
|
|
313
|
+
const updated = insertPage(source, dot.lastPage, path);
|
|
299
314
|
if (!listsPage(updated, dotFile, path)) return "manual";
|
|
300
315
|
await writeFile(dotFile, updated);
|
|
301
316
|
return "added";
|
|
302
317
|
}
|
|
303
|
-
function
|
|
318
|
+
function insertPage(source, lastPage, path) {
|
|
304
319
|
const quote = source[lastPage.start] === "\"" ? "\"" : "'";
|
|
305
320
|
const page = `${quote}${path}${quote}`;
|
|
321
|
+
const before = source.slice(0, lastPage.end);
|
|
322
|
+
const after = source.slice(lastPage.end);
|
|
306
323
|
const lineStart = source.lastIndexOf("\n", lastPage.start - 1) + 1;
|
|
307
324
|
const indent = source.slice(lineStart, lastPage.start);
|
|
308
|
-
if (!/^\s*$/.test(indent)) return `${
|
|
309
|
-
const trailingComma = /^\s*,/.exec(
|
|
325
|
+
if (!/^\s*$/.test(indent)) return `${before}, ${page}${after}`;
|
|
326
|
+
const trailingComma = /^\s*,/.exec(after);
|
|
310
327
|
if (trailingComma) {
|
|
311
328
|
const end = lastPage.end + trailingComma[0].length;
|
|
312
329
|
return `${source.slice(0, end)}\n${indent}${page},${source.slice(end)}`;
|
|
313
330
|
}
|
|
314
|
-
return `${
|
|
331
|
+
return `${before},\n${indent}${page}${after}`;
|
|
315
332
|
}
|
|
316
333
|
function listsPage(source, dotFile, path) {
|
|
317
334
|
try {
|
|
@@ -334,10 +351,10 @@ function isPackageManager(name) {
|
|
|
334
351
|
function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
|
|
335
352
|
return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
|
|
336
353
|
}
|
|
337
|
-
function installedManagers(isInstalled =
|
|
354
|
+
function installedManagers(isInstalled = isOnPath) {
|
|
338
355
|
return PACKAGE_MANAGERS.filter(isInstalled);
|
|
339
356
|
}
|
|
340
|
-
function
|
|
357
|
+
function isOnPath(name) {
|
|
341
358
|
return spawnSync(name, ["--version"], {
|
|
342
359
|
stdio: "ignore",
|
|
343
360
|
shell: process.platform === "win32"
|
|
@@ -366,16 +383,18 @@ async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
|
|
|
366
383
|
return toDeveloperKey(privateKey);
|
|
367
384
|
}
|
|
368
385
|
function toDeveloperKey(privateKey) {
|
|
369
|
-
const
|
|
386
|
+
const publicKeyJwk = createPublicKey(privateKey).export({ format: "jwk" });
|
|
387
|
+
const rawPublicKey = Buffer.from(publicKeyJwk.x, "base64url");
|
|
370
388
|
return {
|
|
371
|
-
publicKey:
|
|
372
|
-
keyId:
|
|
389
|
+
publicKey: rawPublicKey.toString("base64"),
|
|
390
|
+
keyId: rawPublicKey.subarray(0, 8).toString("hex"),
|
|
373
391
|
sign: (bytes) => sign(null, bytes, privateKey).toString("base64")
|
|
374
392
|
};
|
|
375
393
|
}
|
|
376
394
|
//#endregion
|
|
377
395
|
//#region src/device/server.ts
|
|
378
396
|
const DEVICE_SCHEME = "dotdev";
|
|
397
|
+
const FILES_PATH = "/dev/files/";
|
|
379
398
|
const WAIT_MS = 25e3;
|
|
380
399
|
const MAX_LOG_BYTES = 1048576;
|
|
381
400
|
const LINK_LIFETIME_SECONDS = 43200;
|
|
@@ -390,130 +409,170 @@ function signedLinkMessage(server, secret, expires, keyId) {
|
|
|
390
409
|
].join("\n");
|
|
391
410
|
}
|
|
392
411
|
function deviceToken(secret) {
|
|
393
|
-
return
|
|
412
|
+
return hmacWithSecret(secret, "dotdev-token");
|
|
394
413
|
}
|
|
395
414
|
function buildMac(secret, body) {
|
|
396
|
-
return
|
|
415
|
+
return hmacWithSecret(secret, `dotdev-build\n${body}`);
|
|
397
416
|
}
|
|
398
417
|
async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {}, developerKey } = {}) {
|
|
399
418
|
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");
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
const deviceServer = new LocalDeviceServer(onLog, onConnect);
|
|
420
|
+
await deviceServer.listen(port, address, developerKey);
|
|
421
|
+
return deviceServer;
|
|
422
|
+
}
|
|
423
|
+
var LocalDeviceServer = class {
|
|
424
|
+
#onLog;
|
|
425
|
+
#onConnect;
|
|
426
|
+
#secret = randomBytes(16).toString("base64url");
|
|
427
|
+
#token = deviceToken(this.#secret);
|
|
428
|
+
#server;
|
|
429
|
+
#waiting = /* @__PURE__ */ new Set();
|
|
430
|
+
#connected = /* @__PURE__ */ new Set();
|
|
431
|
+
#link = "";
|
|
432
|
+
#signature = null;
|
|
433
|
+
#files = /* @__PURE__ */ new Map();
|
|
434
|
+
#generation = 0;
|
|
435
|
+
#hotSince = 0;
|
|
436
|
+
#error = null;
|
|
437
|
+
constructor(onLog, onConnect) {
|
|
438
|
+
this.#onLog = onLog;
|
|
439
|
+
this.#onConnect = onConnect;
|
|
440
|
+
this.#server = createServer((request, response) => {
|
|
441
|
+
this.#handle(request, response);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
get link() {
|
|
445
|
+
return this.#link;
|
|
446
|
+
}
|
|
447
|
+
async listen(port, address, developerKey) {
|
|
448
|
+
await new Promise((resolve, reject) => {
|
|
449
|
+
this.#server.once("error", reject);
|
|
450
|
+
this.#server.listen(port, address, () => resolve());
|
|
451
|
+
});
|
|
452
|
+
const { port: listeningPort } = this.#server.address();
|
|
453
|
+
const origin = `http://${address}:${listeningPort}`;
|
|
454
|
+
if (developerKey) {
|
|
455
|
+
const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
|
|
456
|
+
const keyId = developerKey.keyId;
|
|
457
|
+
const message = signedLinkMessage(origin, this.#secret, expires, keyId);
|
|
458
|
+
const sig = developerKey.sign(Buffer.from(message, "utf8"));
|
|
459
|
+
this.#signature = {
|
|
460
|
+
expires,
|
|
461
|
+
keyId,
|
|
462
|
+
sig
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const server = encodeURIComponent(origin);
|
|
466
|
+
this.#link = `${DEVICE_SCHEME}://connect?server=${server}&secret=${this.#secret}`;
|
|
467
|
+
}
|
|
468
|
+
async publish(dir, { hot = false } = {}) {
|
|
469
|
+
this.#files = await readBuild(dir);
|
|
470
|
+
this.#error = null;
|
|
471
|
+
this.#announce();
|
|
472
|
+
if (!hot) this.#hotSince = this.#generation;
|
|
473
|
+
}
|
|
474
|
+
fail(message) {
|
|
475
|
+
this.#error = message;
|
|
476
|
+
this.#announce();
|
|
477
|
+
}
|
|
478
|
+
close() {
|
|
479
|
+
for (const wake of this.#waiting) wake();
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
this.#server.close(() => resolve());
|
|
482
|
+
this.#server.closeAllConnections();
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
#announce() {
|
|
486
|
+
this.#generation += 1;
|
|
487
|
+
for (const wake of this.#waiting) wake();
|
|
488
|
+
}
|
|
489
|
+
#handle(request, response) {
|
|
414
490
|
const from = request.socket.remoteAddress ?? "a device";
|
|
415
491
|
if (request.method === "GET" && request.url === "/dev/link") {
|
|
416
|
-
|
|
417
|
-
else response.writeHead(404).end();
|
|
492
|
+
this.#sendSignature(response);
|
|
418
493
|
return;
|
|
419
494
|
}
|
|
420
|
-
if (!isAuthorized(request, token)) {
|
|
495
|
+
if (!isAuthorized(request, this.#token)) {
|
|
421
496
|
response.writeHead(401).end();
|
|
422
497
|
return;
|
|
423
498
|
}
|
|
424
499
|
const url = new URL(request.url ?? "/", "http://device");
|
|
425
500
|
const path = url.pathname;
|
|
426
|
-
if (request.method === "GET" && path === "/dev/build")
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
const files = Object.fromEntries([...build.files].map(([name, file]) => [name, file.hash]));
|
|
432
|
-
const body = JSON.stringify({
|
|
433
|
-
generation,
|
|
434
|
-
hotSince,
|
|
435
|
-
files,
|
|
436
|
-
error
|
|
437
|
-
});
|
|
438
|
-
response.writeHead(200, {
|
|
439
|
-
"content-type": "application/json",
|
|
440
|
-
[BUILD_MAC_HEADER]: buildMac(secret, body)
|
|
441
|
-
}).end(body);
|
|
442
|
-
} else if (request.method === "GET" && path.startsWith("/dev/files/")) {
|
|
443
|
-
if (url.searchParams.get("generation") !== String(generation)) {
|
|
444
|
-
response.writeHead(409).end();
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
const name = path.slice(11).split("/").map(decodeURIComponent).join("/");
|
|
448
|
-
const file = build.files.get(name);
|
|
449
|
-
if (!file) {
|
|
450
|
-
response.writeHead(404).end();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
|
|
454
|
-
} else if (request.method === "GET" && path === "/dev/changes") waitForChange(response, Number(url.searchParams.get("after")));
|
|
455
|
-
else if (request.method === "POST" && path === "/dev/log") readText(request).then((text) => {
|
|
456
|
-
for (const line of text.split("\n")) onLog(line, from);
|
|
457
|
-
response.writeHead(204).end();
|
|
458
|
-
}).catch(() => response.writeHead(413).end());
|
|
501
|
+
if (request.method === "GET" && path === "/dev/build") this.#sendBuild(response, from);
|
|
502
|
+
else if (request.method === "GET" && path.startsWith(FILES_PATH)) this.#sendFile(response, path, url.searchParams.get("generation"));
|
|
503
|
+
else if (request.method === "GET" && path === "/dev/changes") this.#waitForChange(response, Number(url.searchParams.get("after")));
|
|
504
|
+
else if (request.method === "POST" && path === "/dev/log") this.#receiveLog(request, response, from);
|
|
459
505
|
else response.writeHead(404).end();
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
|
|
506
|
+
}
|
|
507
|
+
#sendSignature(response) {
|
|
508
|
+
if (this.#signature) sendJson(response, this.#signature);
|
|
509
|
+
else response.writeHead(404).end();
|
|
510
|
+
}
|
|
511
|
+
#sendBuild(response, from) {
|
|
512
|
+
if (!this.#connected.has(from)) {
|
|
513
|
+
this.#connected.add(from);
|
|
514
|
+
this.#onConnect(from);
|
|
515
|
+
}
|
|
516
|
+
const hashes = {};
|
|
517
|
+
for (const [name, file] of this.#files) hashes[name] = file.hash;
|
|
518
|
+
const body = JSON.stringify({
|
|
519
|
+
generation: this.#generation,
|
|
520
|
+
hotSince: this.#hotSince,
|
|
521
|
+
files: hashes,
|
|
522
|
+
error: this.#error
|
|
523
|
+
});
|
|
524
|
+
response.writeHead(200, {
|
|
525
|
+
"content-type": "application/json",
|
|
526
|
+
[BUILD_MAC_HEADER]: buildMac(this.#secret, body)
|
|
527
|
+
}).end(body);
|
|
528
|
+
}
|
|
529
|
+
#sendFile(response, path, generation) {
|
|
530
|
+
if (generation !== String(this.#generation)) {
|
|
531
|
+
response.writeHead(409).end();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const fileName = buildFileName(path);
|
|
535
|
+
if (fileName === void 0) {
|
|
536
|
+
response.writeHead(400).end();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const file = this.#files.get(fileName);
|
|
540
|
+
if (!file) {
|
|
541
|
+
response.writeHead(404).end();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
|
|
545
|
+
}
|
|
546
|
+
#waitForChange(response, after) {
|
|
547
|
+
if (this.#generation > after) {
|
|
548
|
+
sendJson(response, { generation: this.#generation });
|
|
464
549
|
return;
|
|
465
550
|
}
|
|
466
551
|
const wake = () => {
|
|
467
552
|
clearTimeout(timer);
|
|
468
|
-
waiting.delete(wake);
|
|
469
|
-
sendJson(response, { generation });
|
|
553
|
+
this.#waiting.delete(wake);
|
|
554
|
+
sendJson(response, { generation: this.#generation });
|
|
470
555
|
};
|
|
471
556
|
const timer = setTimeout(() => {
|
|
472
|
-
waiting.delete(wake);
|
|
557
|
+
this.#waiting.delete(wake);
|
|
473
558
|
response.writeHead(204).end();
|
|
474
559
|
}, WAIT_MS);
|
|
475
|
-
waiting.add(wake);
|
|
560
|
+
this.#waiting.add(wake);
|
|
476
561
|
response.on("close", () => {
|
|
477
562
|
clearTimeout(timer);
|
|
478
|
-
waiting.delete(wake);
|
|
563
|
+
this.#waiting.delete(wake);
|
|
479
564
|
});
|
|
480
|
-
};
|
|
481
|
-
await new Promise((resolve, reject) => {
|
|
482
|
-
server.once("error", reject);
|
|
483
|
-
server.listen(port, address, () => resolve());
|
|
484
|
-
});
|
|
485
|
-
const { port: listening } = server.address();
|
|
486
|
-
const origin = `http://${address}:${listening}`;
|
|
487
|
-
if (developerKey) {
|
|
488
|
-
const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
|
|
489
|
-
const keyId = developerKey.keyId;
|
|
490
|
-
const message = signedLinkMessage(origin, secret, expires, keyId);
|
|
491
|
-
signature = {
|
|
492
|
-
expires,
|
|
493
|
-
keyId,
|
|
494
|
-
sig: developerKey.sign(Buffer.from(message, "utf8"))
|
|
495
|
-
};
|
|
496
565
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
announce();
|
|
508
|
-
},
|
|
509
|
-
close() {
|
|
510
|
-
for (const wake of waiting) wake();
|
|
511
|
-
return new Promise((resolve) => {
|
|
512
|
-
server.close(() => resolve());
|
|
513
|
-
server.closeAllConnections();
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
};
|
|
566
|
+
#receiveLog(request, response, from) {
|
|
567
|
+
readLogText(request).then((text) => {
|
|
568
|
+
for (const line of text.split("\n")) this.#onLog(line, from);
|
|
569
|
+
response.writeHead(204).end();
|
|
570
|
+
}).catch(() => response.writeHead(413).end());
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
function hmacWithSecret(secret, text) {
|
|
574
|
+
const key = Buffer.from(secret, "base64url");
|
|
575
|
+
return createHmac("sha256", key).update(text, "utf8").digest("base64url");
|
|
517
576
|
}
|
|
518
577
|
function localNetworkAddress() {
|
|
519
578
|
for (const addresses of Object.values(networkInterfaces())) for (const candidate of addresses ?? []) if (candidate.family === "IPv4" && !candidate.internal) return candidate.address;
|
|
@@ -529,12 +588,21 @@ async function readBuild(dir) {
|
|
|
529
588
|
const path = join(entry.parentPath, entry.name);
|
|
530
589
|
const bytes = await readFile(path);
|
|
531
590
|
const hash = createHash("sha256").update(bytes).digest("base64url");
|
|
532
|
-
|
|
591
|
+
const name = relative(dir, path).split(sep).join("/");
|
|
592
|
+
files.set(name, {
|
|
533
593
|
bytes,
|
|
534
594
|
hash
|
|
535
595
|
});
|
|
536
596
|
}
|
|
537
|
-
return
|
|
597
|
+
return files;
|
|
598
|
+
}
|
|
599
|
+
function buildFileName(path) {
|
|
600
|
+
const encodedParts = path.slice(11).split("/");
|
|
601
|
+
try {
|
|
602
|
+
return encodedParts.map((part) => decodeURIComponent(part)).join("/");
|
|
603
|
+
} catch {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
538
606
|
}
|
|
539
607
|
function isAuthorized(request, token) {
|
|
540
608
|
const given = Buffer.from(request.headers.authorization ?? "");
|
|
@@ -544,7 +612,7 @@ function isAuthorized(request, token) {
|
|
|
544
612
|
function sendJson(response, body) {
|
|
545
613
|
response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
546
614
|
}
|
|
547
|
-
async function
|
|
615
|
+
async function readLogText(request) {
|
|
548
616
|
const chunks = [];
|
|
549
617
|
let size = 0;
|
|
550
618
|
for await (const chunk of request) {
|
|
@@ -570,64 +638,40 @@ function startDotSimulator(project, outDir, port) {
|
|
|
570
638
|
const CHANGE_DEBOUNCE_MS = 50;
|
|
571
639
|
async function dev({ port, device, ...options }) {
|
|
572
640
|
const project = await loadProject(options.root);
|
|
573
|
-
const outDir =
|
|
641
|
+
const outDir = resolveOutDir(project.root, options.outDir);
|
|
574
642
|
const buildOptions = {
|
|
575
643
|
mode: "development",
|
|
576
644
|
...options,
|
|
577
645
|
outDir
|
|
578
646
|
};
|
|
579
|
-
const
|
|
580
|
-
const started = performance.now();
|
|
581
|
-
try {
|
|
582
|
-
printWarnings(await build(buildOptions));
|
|
583
|
-
printBuildTime(started);
|
|
584
|
-
return null;
|
|
585
|
-
} catch (error) {
|
|
586
|
-
const message = describeError(error).replaceAll(project.root + sep, "");
|
|
587
|
-
console.error(`error: ${message}`);
|
|
588
|
-
return message;
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
const firstError = await buildApp();
|
|
647
|
+
const firstError = await buildDot(buildOptions, project.root);
|
|
592
648
|
const simulator = await startDotSimulator(project, outDir, port);
|
|
593
649
|
if (firstError) simulator.error(firstError);
|
|
594
|
-
|
|
595
|
-
let
|
|
596
|
-
if (device)
|
|
597
|
-
|
|
598
|
-
devices = await startDeviceServer({
|
|
599
|
-
developerKey,
|
|
600
|
-
onConnect: (from) => console.log(`A device connected from ${from}`),
|
|
601
|
-
onLog: (line) => console.log(`device ${line}`)
|
|
602
|
-
});
|
|
603
|
-
if (firstError) devices.fail(firstError);
|
|
604
|
-
else await devices.publish(outDir);
|
|
605
|
-
printDeviceLink(devices.link);
|
|
606
|
-
if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
|
|
607
|
-
else console.log(`${missingDeveloperKeyHint()}\n`);
|
|
608
|
-
}
|
|
609
|
-
const changed = /* @__PURE__ */ new Set();
|
|
650
|
+
printSimulatorUrl(simulator.url);
|
|
651
|
+
let deviceServer;
|
|
652
|
+
if (device) deviceServer = await startDevices(outDir, firstError);
|
|
653
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
610
654
|
const pagesDir = join(project.root, PAGES_DIR) + sep;
|
|
611
655
|
const rebuilds = new RebuildQueue(async () => {
|
|
612
|
-
const
|
|
613
|
-
const error = await
|
|
656
|
+
const filesInThisBuild = [...changedFiles];
|
|
657
|
+
const error = await buildDot(buildOptions, project.root);
|
|
614
658
|
if (error) {
|
|
615
659
|
simulator.error(error);
|
|
616
|
-
|
|
660
|
+
deviceServer?.fail(error);
|
|
617
661
|
return;
|
|
618
662
|
}
|
|
619
|
-
for (const path of
|
|
620
|
-
const hot =
|
|
663
|
+
for (const path of filesInThisBuild) changedFiles.delete(path);
|
|
664
|
+
const hot = filesInThisBuild.every((path) => path.startsWith(pagesDir));
|
|
621
665
|
if (hot) simulator.hotReload();
|
|
622
666
|
else simulator.reload();
|
|
623
|
-
await
|
|
667
|
+
await deviceServer?.publish(outDir, { hot });
|
|
624
668
|
});
|
|
625
669
|
let debounceTimer;
|
|
626
670
|
const watcher = watch(project.root, { recursive: true }, (_event, file) => {
|
|
627
671
|
if (!file || isIgnored(file)) return;
|
|
628
672
|
const path = resolve(project.root, file);
|
|
629
673
|
if (path === outDir || path.startsWith(outDir + sep)) return;
|
|
630
|
-
|
|
674
|
+
changedFiles.add(path);
|
|
631
675
|
clearTimeout(debounceTimer);
|
|
632
676
|
debounceTimer = setTimeout(() => {
|
|
633
677
|
console.log(`Changed ${relative(project.root, path)}`);
|
|
@@ -637,12 +681,39 @@ async function dev({ port, device, ...options }) {
|
|
|
637
681
|
return async () => {
|
|
638
682
|
clearTimeout(debounceTimer);
|
|
639
683
|
watcher.close();
|
|
640
|
-
await Promise.all([simulator.close(),
|
|
684
|
+
await Promise.all([simulator.close(), deviceServer?.close()]);
|
|
641
685
|
};
|
|
642
686
|
}
|
|
687
|
+
async function buildDot(buildOptions, root) {
|
|
688
|
+
const startedAt = performance.now();
|
|
689
|
+
try {
|
|
690
|
+
printWarnings(await build(buildOptions));
|
|
691
|
+
printBuildTime(startedAt);
|
|
692
|
+
return null;
|
|
693
|
+
} catch (error) {
|
|
694
|
+
const message = describeError(error).replaceAll(root + sep, "");
|
|
695
|
+
console.error(`error: ${message}`);
|
|
696
|
+
return message;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function startDevices(outDir, firstError) {
|
|
700
|
+
const developerKey = await loadDeveloperKey();
|
|
701
|
+
const deviceServer = await startDeviceServer({
|
|
702
|
+
developerKey,
|
|
703
|
+
onConnect: (from) => console.log(`A device connected from ${from}`),
|
|
704
|
+
onLog: (line) => console.log(`device ${line}`)
|
|
705
|
+
});
|
|
706
|
+
if (firstError) deviceServer.fail(firstError);
|
|
707
|
+
else await deviceServer.publish(outDir);
|
|
708
|
+
printDeviceLink(deviceServer.link);
|
|
709
|
+
if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
|
|
710
|
+
else console.log(`${missingDeveloperKeyHint()}\n`);
|
|
711
|
+
return deviceServer;
|
|
712
|
+
}
|
|
643
713
|
function printDeviceLink(link) {
|
|
644
714
|
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");
|
|
645
|
-
|
|
715
|
+
const qrCode = renderUnicodeCompact(link, { border: 1 });
|
|
716
|
+
console.log(qrCode.replace(/^/gm, " "));
|
|
646
717
|
console.log(`\n ${link}\n`);
|
|
647
718
|
}
|
|
648
719
|
var RebuildQueue = class {
|
|
@@ -680,16 +751,16 @@ function isIgnored(file) {
|
|
|
680
751
|
//#region src/preview.ts
|
|
681
752
|
async function preview({ port, ...options }) {
|
|
682
753
|
const project = await loadProject(options.root);
|
|
683
|
-
const outDir =
|
|
684
|
-
const
|
|
754
|
+
const outDir = resolveOutDir(project.root, options.outDir);
|
|
755
|
+
const startedAt = performance.now();
|
|
685
756
|
printWarnings(await build({
|
|
686
757
|
...options,
|
|
687
758
|
outDir,
|
|
688
759
|
mode: "production"
|
|
689
760
|
}));
|
|
690
|
-
printBuildTime(
|
|
761
|
+
printBuildTime(startedAt);
|
|
691
762
|
const simulator = await startDotSimulator(project, outDir, port);
|
|
692
|
-
|
|
763
|
+
printSimulatorUrl(simulator.url);
|
|
693
764
|
return simulator;
|
|
694
765
|
}
|
|
695
766
|
//#endregion
|
|
@@ -701,29 +772,33 @@ const TERMINAL = {
|
|
|
701
772
|
const ERASE_BELOW = "\x1B[0J";
|
|
702
773
|
const HIDE_CURSOR = "\x1B[?25l";
|
|
703
774
|
const SHOW_CURSOR = "\x1B[?25h";
|
|
704
|
-
const cursorUp = (lines) => `\x1b[${lines}A`;
|
|
705
775
|
async function select(question, choices, fallback, { input, output } = TERMINAL) {
|
|
706
776
|
if (!input.isTTY) return fallback;
|
|
707
|
-
let
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
777
|
+
let selectedIndex = Math.max(0, choices.findIndex((choice) => choice.value === fallback));
|
|
778
|
+
let valueWidth = 0;
|
|
779
|
+
if (choices.some((choice) => choice.hint)) valueWidth = Math.max(...choices.map((choice) => choice.value.length));
|
|
780
|
+
const formatChoice = (choice, index) => {
|
|
781
|
+
const value = choice.value.padEnd(valueWidth);
|
|
711
782
|
const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
|
|
712
783
|
const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
|
|
713
|
-
if (index ===
|
|
714
|
-
return ` ${
|
|
784
|
+
if (index === selectedIndex) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], value)}${hint}${defaultMark}`;
|
|
785
|
+
return ` ${value}${hint}${defaultMark}`;
|
|
715
786
|
};
|
|
716
787
|
let drawnLines = 0;
|
|
717
788
|
const draw = (lines) => {
|
|
718
|
-
|
|
789
|
+
const moveUp = drawnLines > 0 ? cursorUp(drawnLines) : "";
|
|
790
|
+
output.write(`${moveUp}${ERASE_BELOW}${lines.join("\n")}\n`);
|
|
719
791
|
drawnLines = lines.length;
|
|
720
792
|
};
|
|
721
|
-
const
|
|
793
|
+
const drawQuestionAndChoices = () => {
|
|
794
|
+
const keysHint = styleText("dim", "↑/↓ to move, Enter to choose");
|
|
795
|
+
draw([`${styleText("bold", question)} ${keysHint}`, ...choices.map(formatChoice)]);
|
|
796
|
+
};
|
|
722
797
|
emitKeypressEvents(input);
|
|
723
798
|
input.setRawMode(true);
|
|
724
799
|
input.resume();
|
|
725
800
|
output.write(HIDE_CURSOR);
|
|
726
|
-
|
|
801
|
+
drawQuestionAndChoices();
|
|
727
802
|
return new Promise((resolve) => {
|
|
728
803
|
const onKeypress = (_text, key) => {
|
|
729
804
|
if (key.ctrl && key.name === "c") {
|
|
@@ -731,13 +806,13 @@ async function select(question, choices, fallback, { input, output } = TERMINAL)
|
|
|
731
806
|
process.exit(130);
|
|
732
807
|
}
|
|
733
808
|
if (key.name === "up" || key.name === "k") {
|
|
734
|
-
|
|
735
|
-
|
|
809
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
810
|
+
drawQuestionAndChoices();
|
|
736
811
|
} else if (key.name === "down" || key.name === "j") {
|
|
737
|
-
|
|
738
|
-
|
|
812
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
813
|
+
drawQuestionAndChoices();
|
|
739
814
|
} else if (key.name === "return" || key.name === "enter") {
|
|
740
|
-
const { value } = choices[
|
|
815
|
+
const { value } = choices[selectedIndex];
|
|
741
816
|
input.off("keypress", onKeypress);
|
|
742
817
|
input.setRawMode(false);
|
|
743
818
|
input.pause();
|
|
@@ -755,14 +830,14 @@ async function confirm(question, fallback, terminal = TERMINAL) {
|
|
|
755
830
|
}
|
|
756
831
|
async function ask(question, fallback, hint, { input, output } = TERMINAL) {
|
|
757
832
|
if (!input.isTTY) return fallback;
|
|
758
|
-
const
|
|
833
|
+
const hintText = hint ? styleText("dim", `, ${hint}`) : "";
|
|
759
834
|
const defaultMark = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
|
|
760
835
|
const readline = createInterface({
|
|
761
836
|
input,
|
|
762
837
|
output
|
|
763
838
|
});
|
|
764
839
|
try {
|
|
765
|
-
return (await readline.question(`${styleText("bold", question)}${
|
|
840
|
+
return (await readline.question(`${styleText("bold", question)}${hintText}${defaultMark}: `)).trim() || fallback;
|
|
766
841
|
} catch (error) {
|
|
767
842
|
if (error.name === "AbortError") return;
|
|
768
843
|
throw error;
|
|
@@ -770,6 +845,9 @@ async function ask(question, fallback, hint, { input, output } = TERMINAL) {
|
|
|
770
845
|
readline.close();
|
|
771
846
|
}
|
|
772
847
|
}
|
|
848
|
+
function cursorUp(lines) {
|
|
849
|
+
return `\x1b[${lines}A`;
|
|
850
|
+
}
|
|
773
851
|
//#endregion
|
|
774
852
|
//#region src/cli.ts
|
|
775
853
|
const USAGE = `Usage:
|
|
@@ -822,57 +900,85 @@ async function main(argv) {
|
|
|
822
900
|
}
|
|
823
901
|
});
|
|
824
902
|
const [command, target, pagePath] = positionals;
|
|
825
|
-
if (
|
|
826
|
-
const { serveStdio } = await import("./server-uIyYmYnp.js");
|
|
827
|
-
serveStdio();
|
|
828
|
-
return new Promise(() => {});
|
|
829
|
-
}
|
|
830
|
-
if (command === "keys" && !values.help) return keys(target);
|
|
831
|
-
if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
|
|
832
|
-
if (command === "create" && !values.help) return createApp(target, values.name, values.package, values.template, values.pm);
|
|
833
|
-
if (values.help || command !== "build" && command !== "dev" && command !== "preview") {
|
|
903
|
+
if (values.help) {
|
|
834
904
|
console.log(USAGE);
|
|
835
|
-
return
|
|
905
|
+
return 0;
|
|
836
906
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
907
|
+
switch (command) {
|
|
908
|
+
case "lsp": return serveLanguageServer();
|
|
909
|
+
case "keys": return showKeys(target);
|
|
910
|
+
case "create":
|
|
911
|
+
if (target === "page") return addPage(pagePath);
|
|
912
|
+
return createDot(target, {
|
|
913
|
+
name: values.name,
|
|
914
|
+
dotPackage: values.package,
|
|
915
|
+
template: values.template,
|
|
916
|
+
packageManager: values.pm
|
|
917
|
+
});
|
|
918
|
+
case "build":
|
|
919
|
+
case "dev":
|
|
920
|
+
case "preview": break;
|
|
921
|
+
default:
|
|
922
|
+
console.log(USAGE);
|
|
923
|
+
return 1;
|
|
924
|
+
}
|
|
925
|
+
let port;
|
|
926
|
+
if (values.port !== void 0) {
|
|
927
|
+
port = Number(values.port);
|
|
928
|
+
if (!Number.isInteger(port)) {
|
|
929
|
+
console.error(`--port must be a whole number, not "${values.port}"`);
|
|
930
|
+
return 1;
|
|
931
|
+
}
|
|
841
932
|
}
|
|
842
933
|
const options = { root: target ?? "." };
|
|
843
934
|
if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
|
|
844
935
|
if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
|
|
845
|
-
if (command === "dev")
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
936
|
+
if (command === "dev") return runDev(options, port, values.device);
|
|
937
|
+
if (command === "preview") return runPreview(options, port, values.device);
|
|
938
|
+
return runBuild(options);
|
|
939
|
+
}
|
|
940
|
+
async function serveLanguageServer() {
|
|
941
|
+
const { serveStdio } = await import("./server-B9-F93mS.js");
|
|
942
|
+
serveStdio();
|
|
943
|
+
return new Promise(() => {});
|
|
944
|
+
}
|
|
945
|
+
async function runDev(options, port, device) {
|
|
946
|
+
const stop = await dev({
|
|
947
|
+
...options,
|
|
948
|
+
port,
|
|
949
|
+
device
|
|
950
|
+
});
|
|
951
|
+
await waitForCtrlC();
|
|
952
|
+
await stop();
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
async function runPreview(options, port, device) {
|
|
956
|
+
if (device) {
|
|
957
|
+
console.error("--device only works with dotctl dev");
|
|
958
|
+
return 1;
|
|
867
959
|
}
|
|
868
|
-
const
|
|
960
|
+
const simulator = await preview({
|
|
961
|
+
...options,
|
|
962
|
+
port
|
|
963
|
+
});
|
|
964
|
+
await waitForCtrlC();
|
|
965
|
+
await simulator.close();
|
|
966
|
+
return 0;
|
|
967
|
+
}
|
|
968
|
+
async function runBuild(options) {
|
|
969
|
+
const startedAt = performance.now();
|
|
869
970
|
const result = await build(options);
|
|
870
|
-
|
|
971
|
+
printFiles(result.files.filter((file) => !file.endsWith(".map")));
|
|
871
972
|
printWarnings(result);
|
|
872
|
-
printBuildTime(
|
|
973
|
+
printBuildTime(startedAt);
|
|
873
974
|
return 0;
|
|
874
975
|
}
|
|
875
|
-
|
|
976
|
+
function waitForCtrlC() {
|
|
977
|
+
return new Promise((resolve) => {
|
|
978
|
+
process.once("SIGINT", () => resolve());
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async function showKeys(action) {
|
|
876
982
|
if (action !== "show") {
|
|
877
983
|
console.error("Usage: dotctl keys show");
|
|
878
984
|
return 1;
|
|
@@ -886,40 +992,51 @@ async function keys(action) {
|
|
|
886
992
|
console.log(`\n Public key: ${key.publicKey}\n Key ID: ${key.keyId}\n`);
|
|
887
993
|
return 0;
|
|
888
994
|
}
|
|
889
|
-
async function addPage(
|
|
890
|
-
if (
|
|
995
|
+
async function addPage(typedPath) {
|
|
996
|
+
if (typedPath === void 0) {
|
|
891
997
|
console.error("Give the page a path, such as: dotctl create page cart/checkout");
|
|
892
998
|
return 1;
|
|
893
999
|
}
|
|
894
|
-
|
|
895
|
-
if (path.endsWith("/")) path = path.substring(0, path.length - 1);
|
|
1000
|
+
const path = trimSlashes(typedPath);
|
|
896
1001
|
const { files, listing } = await createPage(path, process.cwd());
|
|
897
|
-
|
|
1002
|
+
printFiles(files);
|
|
898
1003
|
if (listing === "added") console.log(`\nListed '${path}' in dot.ts.`);
|
|
899
1004
|
else if (listing === "manual") console.log(`\nAdd it to the pages in dot.ts so it gets built: defineDot({ pages: [..., '${path}'] })`);
|
|
900
1005
|
return 0;
|
|
901
1006
|
}
|
|
902
|
-
|
|
1007
|
+
function trimSlashes(path) {
|
|
1008
|
+
let trimmed = path;
|
|
1009
|
+
if (trimmed.startsWith("/")) trimmed = trimmed.substring(1);
|
|
1010
|
+
if (trimmed.endsWith("/")) trimmed = trimmed.substring(0, trimmed.length - 1);
|
|
1011
|
+
return trimmed;
|
|
1012
|
+
}
|
|
1013
|
+
async function createDot(dir, flags) {
|
|
1014
|
+
const templateFlag = flags.template;
|
|
903
1015
|
if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
|
|
904
1016
|
console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
|
|
905
1017
|
return 1;
|
|
906
1018
|
}
|
|
1019
|
+
const packageManagerFlag = flags.packageManager;
|
|
907
1020
|
if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
|
|
908
1021
|
console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
|
|
909
1022
|
return 1;
|
|
910
1023
|
}
|
|
911
|
-
name
|
|
1024
|
+
let name = flags.name;
|
|
1025
|
+
if (name === void 0) name = await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
|
|
912
1026
|
if (!name) {
|
|
913
1027
|
console.error("The dot needs a name. Pass one with: dotctl create [dir] --name \"My Dot\"");
|
|
914
1028
|
return 1;
|
|
915
1029
|
}
|
|
1030
|
+
const dotPackageFlag = flags.dotPackage;
|
|
916
1031
|
if (dotPackageFlag !== void 0 && !isDotPackage(dotPackageFlag)) {
|
|
917
1032
|
console.error(`--package "${dotPackageFlag}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
918
1033
|
return 1;
|
|
919
1034
|
}
|
|
920
|
-
|
|
1035
|
+
let dotPackage = dotPackageFlag;
|
|
1036
|
+
if (dotPackage === void 0) dotPackage = await askDotPackage(suggestedDotPackage(toPackageName(name)));
|
|
921
1037
|
if (!dotPackage) return 1;
|
|
922
|
-
|
|
1038
|
+
let template = templateFlag;
|
|
1039
|
+
if (template === void 0) template = await chooseTemplate();
|
|
923
1040
|
const { root, workspace } = await create({
|
|
924
1041
|
name,
|
|
925
1042
|
dir: dir ?? toPackageName(name),
|
|
@@ -929,17 +1046,19 @@ async function createApp(dir, name, dotPackageFlag, templateFlag, packageManager
|
|
|
929
1046
|
const path = relative(process.cwd(), root) || ".";
|
|
930
1047
|
console.log(`\nCreated ${name} in ${path}`);
|
|
931
1048
|
if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
|
|
932
|
-
let packageManager
|
|
1049
|
+
let packageManager;
|
|
1050
|
+
if (workspace) packageManager = "pnpm";
|
|
1051
|
+
else packageManager = packageManagerFlag ?? launchedBy();
|
|
933
1052
|
console.log();
|
|
934
1053
|
if (await confirm("Install and start it now?", true)) {
|
|
935
1054
|
if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
|
|
936
|
-
if (await
|
|
1055
|
+
if (await runCommand(packageManager, ["install"], root) === 0) return runCommand(packageManager, ["run", "dev"], root);
|
|
937
1056
|
console.error(`\n${packageManager} install failed. Fix the problem above, then run the steps below.`);
|
|
938
1057
|
}
|
|
939
1058
|
console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
|
|
940
1059
|
return 0;
|
|
941
1060
|
}
|
|
942
|
-
function
|
|
1061
|
+
function runCommand(command, args, cwd) {
|
|
943
1062
|
return new Promise((resolvePromise, reject) => {
|
|
944
1063
|
const child = spawn(command, args, {
|
|
945
1064
|
cwd,
|
|
@@ -967,10 +1086,11 @@ function chooseTemplate() {
|
|
|
967
1086
|
async function choosePackageManager(suggested) {
|
|
968
1087
|
const installed = installedManagers();
|
|
969
1088
|
if (installed.length < 2) return installed[0] ?? suggested;
|
|
970
|
-
|
|
1089
|
+
let fallback = installed[0];
|
|
1090
|
+
if (installed.includes(suggested)) fallback = suggested;
|
|
971
1091
|
return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
|
|
972
1092
|
}
|
|
973
|
-
main(process.argv.slice(2)).then((
|
|
1093
|
+
main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode), (error) => {
|
|
974
1094
|
console.error(`error: ${describeError(error)}`);
|
|
975
1095
|
process.exit(1);
|
|
976
1096
|
});
|