@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.
- package/README.md +96 -0
- package/bin/dotctl.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +942 -0
- package/dist/cli.js.map +1 -0
- package/dist/server-Dbrgkelo.js +11215 -0
- package/dist/server-Dbrgkelo.js.map +1 -0
- package/dist/templates/showcase/.env +3 -0
- package/dist/templates/showcase/dot.ts +24 -0
- package/dist/templates/showcase/pages/cache/controller.ts +45 -0
- package/dist/templates/showcase/pages/cache/state.ts +17 -0
- package/dist/templates/showcase/pages/cache/view.dml +36 -0
- package/dist/templates/showcase/pages/carousel/controller.ts +40 -0
- package/dist/templates/showcase/pages/carousel/state.ts +77 -0
- package/dist/templates/showcase/pages/carousel/view.dml +40 -0
- package/dist/templates/showcase/pages/device/controller.ts +80 -0
- package/dist/templates/showcase/pages/device/state.ts +7 -0
- package/dist/templates/showcase/pages/device/view.dml +49 -0
- package/dist/templates/showcase/pages/feedback/controller.ts +21 -0
- package/dist/templates/showcase/pages/feedback/state.ts +8 -0
- package/dist/templates/showcase/pages/feedback/view.dml +87 -0
- package/dist/templates/showcase/pages/files/controller.ts +66 -0
- package/dist/templates/showcase/pages/files/state.ts +19 -0
- package/dist/templates/showcase/pages/files/view.dml +50 -0
- package/dist/templates/showcase/pages/filters/controller.ts +27 -0
- package/dist/templates/showcase/pages/filters/state.ts +118 -0
- package/dist/templates/showcase/pages/filters/view.dml +44 -0
- package/dist/templates/showcase/pages/forms/controller.ts +68 -0
- package/dist/templates/showcase/pages/forms/state.ts +34 -0
- package/dist/templates/showcase/pages/forms/view.dml +49 -0
- package/dist/templates/showcase/pages/images/controller.ts +41 -0
- package/dist/templates/showcase/pages/images/state.ts +10 -0
- package/dist/templates/showcase/pages/images/view.dml +53 -0
- package/dist/templates/showcase/pages/index/controller.ts +10 -0
- package/dist/templates/showcase/pages/index/state.ts +128 -0
- package/dist/templates/showcase/pages/index/view.dml +20 -0
- package/dist/templates/showcase/pages/layout/controller.ts +60 -0
- package/dist/templates/showcase/pages/layout/state.ts +34 -0
- package/dist/templates/showcase/pages/layout/view.dml +103 -0
- package/dist/templates/showcase/pages/lists/controller.ts +37 -0
- package/dist/templates/showcase/pages/lists/state.ts +117 -0
- package/dist/templates/showcase/pages/lists/view.dml +59 -0
- package/dist/templates/showcase/pages/navigation/controller.ts +65 -0
- package/dist/templates/showcase/pages/navigation/state.ts +17 -0
- package/dist/templates/showcase/pages/navigation/view.dml +34 -0
- package/dist/templates/showcase/pages/network/controller.ts +151 -0
- package/dist/templates/showcase/pages/network/state.ts +22 -0
- package/dist/templates/showcase/pages/network/view.dml +67 -0
- package/dist/templates/showcase/pages/payments/controller.ts +46 -0
- package/dist/templates/showcase/pages/payments/state.ts +13 -0
- package/dist/templates/showcase/pages/payments/view.dml +45 -0
- package/dist/templates/showcase/pages/secrets/controller.ts +43 -0
- package/dist/templates/showcase/pages/secrets/state.ts +5 -0
- package/dist/templates/showcase/pages/secrets/view.dml +24 -0
- package/dist/templates/showcase/pages/state/controller.ts +22 -0
- package/dist/templates/showcase/pages/state/state.ts +10 -0
- package/dist/templates/showcase/pages/state/view.dml +33 -0
- package/dist/templates/showcase/pages/text/controller.ts +10 -0
- package/dist/templates/showcase/pages/text/state.ts +8 -0
- package/dist/templates/showcase/pages/text/view.dml +58 -0
- package/dist/templates/showcase/statics/logo.png +0 -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 +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
|