@monimesl/dot-cli 0.1.0-alpha.2 → 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/README.md +91 -17
- package/dist/cli.js +832 -156
- package/dist/cli.js.map +1 -1
- package/dist/server-B9-F93mS.js +11295 -0
- package/dist/server-B9-F93mS.js.map +1 -0
- package/dist/templates/showcase/.env +3 -0
- package/dist/templates/showcase/dot.ts +52 -0
- package/dist/templates/showcase/pages/basics/bars/controller.ts +36 -0
- package/dist/templates/showcase/pages/basics/bars/state.ts +6 -0
- package/dist/templates/showcase/pages/basics/bars/view.dml +28 -0
- package/dist/templates/showcase/pages/basics/bindings/controller.ts +15 -0
- package/dist/templates/showcase/pages/basics/bindings/state.ts +14 -0
- package/dist/templates/showcase/pages/basics/bindings/view.dml +41 -0
- package/dist/templates/showcase/pages/basics/control-flow/controller.ts +18 -0
- package/dist/templates/showcase/pages/basics/control-flow/state.ts +16 -0
- package/dist/templates/showcase/pages/basics/control-flow/view.dml +55 -0
- package/dist/templates/showcase/pages/basics/events/controller.ts +43 -0
- package/dist/templates/showcase/pages/basics/events/state.ts +11 -0
- package/dist/templates/showcase/pages/basics/events/view.dml +51 -0
- package/dist/templates/showcase/pages/basics/hooks/controller.ts +43 -0
- package/dist/templates/showcase/pages/basics/hooks/state.ts +10 -0
- package/dist/templates/showcase/pages/basics/hooks/view.dml +37 -0
- package/dist/templates/showcase/pages/basics/navigation/controller.ts +41 -0
- package/dist/templates/showcase/pages/basics/navigation/state.ts +11 -0
- package/dist/templates/showcase/pages/basics/navigation/view.dml +48 -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/basics/state/controller.ts +23 -0
- package/dist/templates/showcase/pages/basics/state/state.ts +10 -0
- package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
- package/dist/templates/showcase/pages/components/carousel/controller.ts +41 -0
- package/dist/templates/showcase/pages/components/carousel/state.ts +77 -0
- package/dist/templates/showcase/pages/components/carousel/view.dml +42 -0
- package/dist/templates/showcase/pages/components/feedback/controller.ts +32 -0
- package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
- package/dist/templates/showcase/pages/components/feedback/view.dml +116 -0
- package/dist/templates/showcase/pages/components/forms/controller.ts +72 -0
- package/dist/templates/showcase/pages/components/forms/state.ts +37 -0
- package/dist/templates/showcase/pages/components/forms/view.dml +97 -0
- package/dist/templates/showcase/pages/components/icons/controller.ts +15 -0
- package/dist/templates/showcase/pages/components/icons/state.ts +75 -0
- package/dist/templates/showcase/pages/components/icons/view.dml +36 -0
- package/dist/templates/showcase/pages/components/images/controller.ts +44 -0
- package/dist/templates/showcase/pages/components/images/state.ts +10 -0
- package/dist/templates/showcase/pages/components/images/view.dml +83 -0
- package/dist/templates/showcase/pages/components/layout/controller.ts +61 -0
- package/dist/templates/showcase/pages/components/layout/state.ts +34 -0
- package/dist/templates/showcase/pages/components/layout/view.dml +143 -0
- package/dist/templates/showcase/pages/components/lists/controller.ts +18 -0
- package/dist/templates/showcase/pages/components/lists/state.ts +130 -0
- package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
- package/dist/templates/showcase/pages/components/money/controller.ts +45 -0
- package/dist/templates/showcase/pages/components/money/state.ts +13 -0
- package/dist/templates/showcase/pages/components/money/view.dml +54 -0
- package/dist/templates/showcase/pages/components/reading/controller.ts +6 -0
- package/dist/templates/showcase/pages/components/reading/state.ts +23 -0
- package/dist/templates/showcase/pages/components/reading/view.dml +42 -0
- package/dist/templates/showcase/pages/components/sheets/controller.ts +36 -0
- package/dist/templates/showcase/pages/components/sheets/state.ts +26 -0
- package/dist/templates/showcase/pages/components/sheets/view.dml +38 -0
- package/dist/templates/showcase/pages/components/styles/controller.ts +6 -0
- package/dist/templates/showcase/pages/components/styles/state.ts +3 -0
- package/dist/templates/showcase/pages/components/styles/view.dml +101 -0
- package/dist/templates/showcase/pages/components/tabs/controller.ts +41 -0
- package/dist/templates/showcase/pages/components/tabs/state.ts +127 -0
- package/dist/templates/showcase/pages/components/tabs/view.dml +57 -0
- package/dist/templates/showcase/pages/components/text/controller.ts +10 -0
- package/dist/templates/showcase/pages/components/text/state.ts +3 -0
- package/dist/templates/showcase/pages/components/text/view.dml +95 -0
- package/dist/templates/showcase/pages/dash/cache/controller.ts +50 -0
- package/dist/templates/showcase/pages/dash/cache/state.ts +17 -0
- package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
- package/dist/templates/showcase/pages/dash/calls/controller.ts +50 -0
- package/dist/templates/showcase/pages/dash/calls/state.ts +6 -0
- package/dist/templates/showcase/pages/dash/calls/view.dml +52 -0
- 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 +91 -0
- package/dist/templates/showcase/pages/dash/files/state.ts +24 -0
- package/dist/templates/showcase/pages/dash/files/view.dml +79 -0
- package/dist/templates/showcase/pages/dash/media/controller.ts +36 -0
- package/dist/templates/showcase/pages/dash/media/state.ts +8 -0
- package/dist/templates/showcase/pages/dash/media/view.dml +34 -0
- package/dist/templates/showcase/pages/dash/network/controller.ts +215 -0
- package/dist/templates/showcase/pages/dash/network/state.ts +35 -0
- package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
- package/dist/templates/showcase/pages/dash/secrets/controller.ts +44 -0
- package/dist/templates/showcase/pages/dash/secrets/state.ts +5 -0
- package/dist/templates/showcase/pages/dash/secrets/view.dml +26 -0
- package/dist/templates/showcase/pages/dash/system/controller.ts +89 -0
- package/dist/templates/showcase/pages/dash/system/state.ts +13 -0
- package/dist/templates/showcase/pages/dash/system/view.dml +97 -0
- package/dist/templates/showcase/pages/dash/ui/controller.ts +70 -0
- package/dist/templates/showcase/pages/dash/ui/state.ts +8 -0
- package/dist/templates/showcase/pages/dash/ui/view.dml +31 -0
- package/dist/templates/showcase/pages/index/controller.ts +10 -0
- package/dist/templates/showcase/pages/index/state.ts +212 -0
- package/dist/templates/showcase/pages/index/view.dml +23 -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/package.json +12 -7
- package/dist/template/.env +0 -3
- package/dist/template/dot.ts +0 -6
- package/dist/template/pages/help/tips/controller.ts +0 -30
- package/dist/template/pages/help/tips/state.ts +0 -11
- package/dist/template/pages/help/tips/view.dml +0 -15
- package/dist/template/pages/index/controller.ts +0 -67
- package/dist/template/pages/index/state.ts +0 -17
- package/dist/template/pages/index/view.dml +0 -66
- /package/dist/{template → templates/starter}/statics/logo.png +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,46 +1,113 @@
|
|
|
1
1
|
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
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { CompileError, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
|
|
4
|
+
import { parseArgs, styleText } from "node:util";
|
|
5
|
+
import { CompileError, DOT_FILE, DOT_JSON_FILE, PAGES_DIR, build, installedRuntime, isDotPackage, loadProject } from "@monimesl/dot-compiler";
|
|
7
6
|
import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
|
|
8
7
|
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
9
|
-
import { cp, mkdir, writeFile } from "node:fs/promises";
|
|
8
|
+
import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
10
9
|
import { DmlParseError } from "@monimesl/dot-dml-parser";
|
|
11
|
-
import { ScriptParseError } from "@monimesl/dot-script-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 (isArgumentError(error)) {
|
|
25
|
+
const [firstSentence] = error.message.split(". ");
|
|
26
|
+
return `${firstSentence}. See dotctl --help.`;
|
|
27
|
+
}
|
|
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)}`);
|
|
37
|
+
}
|
|
38
|
+
function printWarnings({ warnings }) {
|
|
39
|
+
for (const warning of warnings) console.warn(`warning: ${warning.message}`);
|
|
40
|
+
}
|
|
41
|
+
function printBuildTime(startedAt) {
|
|
42
|
+
console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
|
|
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
|
+
}
|
|
64
|
+
//#endregion
|
|
12
65
|
//#region src/create/template.ts
|
|
66
|
+
const TEMPLATES = ["starter", "showcase"];
|
|
67
|
+
const TEMPLATE_HINTS = {
|
|
68
|
+
starter: "one page to build on",
|
|
69
|
+
showcase: "a page for each feature, to learn from"
|
|
70
|
+
};
|
|
71
|
+
function isTemplate(name) {
|
|
72
|
+
return TEMPLATES.includes(name);
|
|
73
|
+
}
|
|
74
|
+
const PAGE_FILES = {
|
|
75
|
+
"view.dml": "<View>\n <Text>My new page</Text>\n</View>\n",
|
|
76
|
+
"state.ts": "export default {};\n",
|
|
77
|
+
"controller.ts": `import state from './state';
|
|
78
|
+
|
|
79
|
+
export default definePage({
|
|
80
|
+
initialState: state,
|
|
81
|
+
});
|
|
82
|
+
`
|
|
83
|
+
};
|
|
13
84
|
const TEMPLATE_FILES = [
|
|
14
85
|
"dot.ts",
|
|
15
86
|
"pages",
|
|
16
87
|
"statics",
|
|
17
88
|
".env"
|
|
18
89
|
];
|
|
19
|
-
const DOT_JSON_SCHEMA = "
|
|
20
|
-
const
|
|
21
|
-
function
|
|
90
|
+
const DOT_JSON_SCHEMA = "https://unpkg.com/@monimesl/dot-runtime/dot.schema.json";
|
|
91
|
+
const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
|
|
92
|
+
function generatedFiles(options) {
|
|
22
93
|
return {
|
|
23
94
|
"package.json": packageJson(options),
|
|
24
95
|
"tsconfig.json": tsconfig(options),
|
|
25
96
|
".gitignore": "node_modules/\ndist/\n# Types generated by dotctl\n.dot/\n",
|
|
26
|
-
"dot.json":
|
|
27
|
-
|
|
28
|
-
id: `com.example.${idWord(options.packageName)}`,
|
|
29
|
-
name: options.name,
|
|
30
|
-
version: "0.1.0"
|
|
31
|
-
}),
|
|
32
|
-
"README.md": readme(options.name)
|
|
97
|
+
"dot.json": dotJson(options),
|
|
98
|
+
"README.md": readme(options)
|
|
33
99
|
};
|
|
34
100
|
}
|
|
35
101
|
function packageJson({ packageName, dotVersion, workspace }) {
|
|
36
102
|
const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
|
|
37
|
-
return
|
|
103
|
+
return formatJson({
|
|
38
104
|
name: packageName,
|
|
39
105
|
private: true,
|
|
40
106
|
type: "module",
|
|
41
107
|
scripts: {
|
|
42
108
|
dev: `dotctl dev${conditions}`,
|
|
43
109
|
build: `dotctl build${conditions}`,
|
|
110
|
+
preview: `dotctl preview${conditions}`,
|
|
44
111
|
typecheck: "tsc --noEmit"
|
|
45
112
|
},
|
|
46
113
|
dependencies: { "@monimesl/dot-runtime": dotVersion },
|
|
@@ -63,7 +130,7 @@ function tsconfig({ workspace }) {
|
|
|
63
130
|
verbatimModuleSyntax: true
|
|
64
131
|
};
|
|
65
132
|
if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
|
|
66
|
-
return
|
|
133
|
+
return formatJson({
|
|
67
134
|
compilerOptions,
|
|
68
135
|
include: [
|
|
69
136
|
"dot.ts",
|
|
@@ -73,7 +140,39 @@ function tsconfig({ workspace }) {
|
|
|
73
140
|
]
|
|
74
141
|
});
|
|
75
142
|
}
|
|
76
|
-
function
|
|
143
|
+
function dotJson({ name, dotPackage, template }) {
|
|
144
|
+
const dot = {
|
|
145
|
+
id: UNREGISTERED_DOT_ID,
|
|
146
|
+
$schema: DOT_JSON_SCHEMA,
|
|
147
|
+
package: dotPackage,
|
|
148
|
+
name,
|
|
149
|
+
version: "0.1.0"
|
|
150
|
+
};
|
|
151
|
+
if (template === "showcase") {
|
|
152
|
+
dot.permissions = {
|
|
153
|
+
camera: "Take a photo for your concert ticket, or scan a ticket's QR code",
|
|
154
|
+
photoLibrary: "Choose a photo for your concert ticket",
|
|
155
|
+
files: "Attach your concert ticket",
|
|
156
|
+
contacts: "Choose a friend to send a ticket to"
|
|
157
|
+
};
|
|
158
|
+
dot.network = { hosts: [
|
|
159
|
+
"itunes.apple.com",
|
|
160
|
+
"*.mzstatic.com",
|
|
161
|
+
"httpbin.org"
|
|
162
|
+
] };
|
|
163
|
+
}
|
|
164
|
+
return formatJson(dot);
|
|
165
|
+
}
|
|
166
|
+
const TEMPLATE_READMES = {
|
|
167
|
+
starter: `It starts with one page, \`index\`. It shows a greeting from \`.env\` and a counter you
|
|
168
|
+
can tap. Change it, and add pages as the dot grows. To see how each feature works, make a showcase
|
|
169
|
+
dot with \`npx dotctl create my-tour --template showcase\`.`,
|
|
170
|
+
showcase: `It starts as a showcase. The \`index\` page links to a page for each feature, in three
|
|
171
|
+
folders: \`pages/basics/\` for how a page works, \`pages/components/\` for what a view can show,
|
|
172
|
+
and \`pages/dash/\` for the phone's features. Read a page to see how it works. Then delete the
|
|
173
|
+
pages you don't need, both from \`pages/\` and from \`dot.ts\`.`
|
|
174
|
+
};
|
|
175
|
+
function readme({ name, template }) {
|
|
77
176
|
return `# ${name}
|
|
78
177
|
|
|
79
178
|
A dot: a mini app that runs inside Dash.
|
|
@@ -84,42 +183,69 @@ npm run dev # run it in the simulator; it reloads on every change
|
|
|
84
183
|
npm run build # build it for Dash
|
|
85
184
|
\`\`\`
|
|
86
185
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
186
|
+
${TEMPLATE_READMES[template]}
|
|
187
|
+
|
|
188
|
+
## Pages
|
|
189
|
+
|
|
190
|
+
Each page is a folder under \`pages/\` with three files:
|
|
191
|
+
|
|
192
|
+
- \`view.dml\`: what the page shows. It has one root \`<View>\`, with everything else inside it.
|
|
193
|
+
- \`state.ts\`: the data the page starts with.
|
|
194
|
+
- \`controller.ts\`: how the data changes.
|
|
195
|
+
|
|
196
|
+
\`npx dotctl create page <path>\` adds a page, such as \`cart/checkout\`, with these three files. It
|
|
197
|
+
also lists the page in \`dot.ts\`. The dot opens on the first page in that list.
|
|
198
|
+
|
|
199
|
+
## dot.json
|
|
200
|
+
|
|
201
|
+
\`dot.json\` describes the dot to Dash. Its \`package\` is a reverse domain name you own, such as
|
|
202
|
+
\`com.mysalone.thing\`. You can't change it after publishing. To publish, register the dot with
|
|
203
|
+
Monime first, and replace its \`id\`, \`${UNREGISTERED_DOT_ID}\`, with the ID Monime gives you.
|
|
204
|
+
\`network.hosts\` lists every host the dot sends requests to with \`dash.network\`. Requests to any
|
|
205
|
+
other host fail. Your editor can complete the other fields from the schema the file points to.
|
|
90
206
|
|
|
91
|
-
|
|
92
|
-
name you own before you publish: it can't change afterwards. Your editor completes the other fields
|
|
93
|
-
from the schema it points to.
|
|
207
|
+
## Which Dash opens it
|
|
94
208
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
209
|
+
Dash opens the dot unless it was built with a newer major version of \`@monimesl/dot-runtime\` than
|
|
210
|
+
the one Dash has. A newer minor version can add Dash calls that an older Dash doesn't have yet.
|
|
211
|
+
There, such a call fails with a \`DashError\` whose \`code\` is \`unsupported\`, so catch it and carry
|
|
212
|
+
on without the feature.
|
|
98
213
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
214
|
+
## .env
|
|
215
|
+
|
|
216
|
+
\`.env\` holds public configuration. Code in the dot reads each variable from \`env\`, such as
|
|
217
|
+
\`env.GREETING\`. Every variable is built into the dot, so anyone who has the dot can read it.
|
|
218
|
+
Never put a secret there. \`.env.development\` and \`.env.production\` override it for \`dev\` and
|
|
219
|
+
\`build\`.
|
|
102
220
|
`;
|
|
103
221
|
}
|
|
104
|
-
function
|
|
105
|
-
return packageName.slice(0, 63).replace(/-+$/, "")
|
|
222
|
+
function suggestedDotPackage(packageName) {
|
|
223
|
+
return `com.example.${packageName.slice(0, 63).replace(/-+$/, "")}`;
|
|
224
|
+
}
|
|
225
|
+
function formatJson(value) {
|
|
226
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
106
227
|
}
|
|
107
228
|
//#endregion
|
|
108
229
|
//#region src/create/create.ts
|
|
109
|
-
|
|
230
|
+
const DOT_PACKAGE_RULE = "Use lowercase words joined by dots, such as com.mysalone.thing.";
|
|
231
|
+
async function create({ name, dir, dotPackage, template = "starter" }) {
|
|
110
232
|
const root = resolve(dir);
|
|
111
|
-
if (existsSync(root) && readdirSync(root).length > 0) throw new
|
|
233
|
+
if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty. Choose another folder`);
|
|
112
234
|
const packageName = toPackageName(name);
|
|
113
|
-
if (!packageName) throw new
|
|
235
|
+
if (!packageName) throw new CommandError(`The name "${name}" needs at least one letter or digit`);
|
|
236
|
+
dotPackage ??= suggestedDotPackage(packageName);
|
|
237
|
+
if (!isDotPackage(dotPackage)) throw new CommandError(`"${dotPackage}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
114
238
|
const workspace = isInDotWorkspace(root);
|
|
115
|
-
const
|
|
239
|
+
const source = templateDir(template);
|
|
116
240
|
await mkdir(root, { recursive: true });
|
|
117
|
-
for (const path of TEMPLATE_FILES) await cp(join(
|
|
118
|
-
const generated =
|
|
241
|
+
for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
|
|
242
|
+
const generated = generatedFiles({
|
|
119
243
|
name,
|
|
120
244
|
packageName,
|
|
245
|
+
dotPackage,
|
|
121
246
|
dotVersion: workspace ? "workspace:*" : `^${cliVersion()}`,
|
|
122
|
-
workspace
|
|
247
|
+
workspace,
|
|
248
|
+
template
|
|
123
249
|
});
|
|
124
250
|
for (const [path, content] of Object.entries(generated)) await writeFile(join(root, path), content);
|
|
125
251
|
return {
|
|
@@ -134,21 +260,15 @@ function toDisplayName(dir) {
|
|
|
134
260
|
return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
135
261
|
}
|
|
136
262
|
function isInDotWorkspace(dir) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
|
|
143
|
-
}
|
|
144
|
-
const parent = dirname(current);
|
|
145
|
-
if (parent === current) return false;
|
|
146
|
-
current = parent;
|
|
147
|
-
}
|
|
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";
|
|
148
268
|
}
|
|
149
|
-
function templateDir() {
|
|
150
|
-
const example = join(cliRoot(), "../../examples
|
|
151
|
-
return existsSync(example) ? example : join(cliRoot(), "dist/template
|
|
269
|
+
function templateDir(template) {
|
|
270
|
+
const example = join(cliRoot(), "../../examples", template);
|
|
271
|
+
return existsSync(example) ? example : join(cliRoot(), "dist/templates", template);
|
|
152
272
|
}
|
|
153
273
|
function cliVersion() {
|
|
154
274
|
return readPackageJson(join(cliRoot(), "package.json")).version;
|
|
@@ -160,6 +280,64 @@ function cliRoot() {
|
|
|
160
280
|
return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
|
|
161
281
|
}
|
|
162
282
|
//#endregion
|
|
283
|
+
//#region src/create/page.ts
|
|
284
|
+
async function createPage(path, cwd) {
|
|
285
|
+
if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path. Use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
|
|
286
|
+
const root = findDotRoot(resolve(cwd));
|
|
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");
|
|
288
|
+
const dir = join(root, PAGES_DIR, path);
|
|
289
|
+
if (existsSync(dir)) throw new CommandError(`pages/${path} already exists. Choose another path`);
|
|
290
|
+
await mkdir(dir, { recursive: true });
|
|
291
|
+
const files = [];
|
|
292
|
+
for (const [name, content] of Object.entries(PAGE_FILES)) {
|
|
293
|
+
const file = join(dir, name);
|
|
294
|
+
await writeFile(file, content);
|
|
295
|
+
files.push(file);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
dir,
|
|
299
|
+
files,
|
|
300
|
+
listing: await addToDotPages(join(root, DOT_FILE), path)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function addToDotPages(dotFile, path) {
|
|
304
|
+
if (!existsSync(dotFile)) return "manual";
|
|
305
|
+
const source = await readFile(dotFile, "utf8");
|
|
306
|
+
let dot;
|
|
307
|
+
try {
|
|
308
|
+
dot = parseDotModule(source, dotFile);
|
|
309
|
+
} catch {
|
|
310
|
+
return "manual";
|
|
311
|
+
}
|
|
312
|
+
if (dot.pages.includes(path)) return "already";
|
|
313
|
+
const updated = insertPage(source, dot.lastPage, path);
|
|
314
|
+
if (!listsPage(updated, dotFile, path)) return "manual";
|
|
315
|
+
await writeFile(dotFile, updated);
|
|
316
|
+
return "added";
|
|
317
|
+
}
|
|
318
|
+
function insertPage(source, lastPage, path) {
|
|
319
|
+
const quote = source[lastPage.start] === "\"" ? "\"" : "'";
|
|
320
|
+
const page = `${quote}${path}${quote}`;
|
|
321
|
+
const before = source.slice(0, lastPage.end);
|
|
322
|
+
const after = source.slice(lastPage.end);
|
|
323
|
+
const lineStart = source.lastIndexOf("\n", lastPage.start - 1) + 1;
|
|
324
|
+
const indent = source.slice(lineStart, lastPage.start);
|
|
325
|
+
if (!/^\s*$/.test(indent)) return `${before}, ${page}${after}`;
|
|
326
|
+
const trailingComma = /^\s*,/.exec(after);
|
|
327
|
+
if (trailingComma) {
|
|
328
|
+
const end = lastPage.end + trailingComma[0].length;
|
|
329
|
+
return `${source.slice(0, end)}\n${indent}${page},${source.slice(end)}`;
|
|
330
|
+
}
|
|
331
|
+
return `${before},\n${indent}${page}${after}`;
|
|
332
|
+
}
|
|
333
|
+
function listsPage(source, dotFile, path) {
|
|
334
|
+
try {
|
|
335
|
+
return parseDotModule(source, dotFile).pages.includes(path);
|
|
336
|
+
} catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
163
341
|
//#region src/create/package-manager.ts
|
|
164
342
|
const PACKAGE_MANAGERS = [
|
|
165
343
|
"npm",
|
|
@@ -173,73 +351,327 @@ function isPackageManager(name) {
|
|
|
173
351
|
function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
|
|
174
352
|
return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
|
|
175
353
|
}
|
|
176
|
-
function installedManagers(isInstalled =
|
|
354
|
+
function installedManagers(isInstalled = isOnPath) {
|
|
177
355
|
return PACKAGE_MANAGERS.filter(isInstalled);
|
|
178
356
|
}
|
|
179
|
-
function
|
|
357
|
+
function isOnPath(name) {
|
|
180
358
|
return spawnSync(name, ["--version"], {
|
|
181
359
|
stdio: "ignore",
|
|
182
360
|
shell: process.platform === "win32"
|
|
183
361
|
}).status === 0;
|
|
184
362
|
}
|
|
185
363
|
//#endregion
|
|
186
|
-
//#region src/
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
364
|
+
//#region src/device/developer-key.ts
|
|
365
|
+
function defaultDeveloperKeyPath() {
|
|
366
|
+
return process.env.DOT_DEVELOPER_KEY ?? join(homedir(), ".dot", "developer-key.pem");
|
|
367
|
+
}
|
|
368
|
+
function missingDeveloperKeyHint(path = defaultDeveloperKeyPath()) {
|
|
369
|
+
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:
|
|
370
|
+
|
|
371
|
+
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.`;
|
|
372
|
+
}
|
|
373
|
+
async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
|
|
374
|
+
let pem;
|
|
375
|
+
try {
|
|
376
|
+
pem = await readFile(path, "utf8");
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error.code === "ENOENT") return;
|
|
379
|
+
throw error;
|
|
192
380
|
}
|
|
193
|
-
|
|
381
|
+
const privateKey = createPrivateKey(pem);
|
|
382
|
+
if (privateKey.asymmetricKeyType !== "ed25519") throw new Error(`${path} is not an Ed25519 key. Make one with: openssl genpkey -algorithm ed25519 -out ${path}`);
|
|
383
|
+
return toDeveloperKey(privateKey);
|
|
194
384
|
}
|
|
195
|
-
function
|
|
196
|
-
|
|
385
|
+
function toDeveloperKey(privateKey) {
|
|
386
|
+
const publicKeyJwk = createPublicKey(privateKey).export({ format: "jwk" });
|
|
387
|
+
const rawPublicKey = Buffer.from(publicKeyJwk.x, "base64url");
|
|
388
|
+
return {
|
|
389
|
+
publicKey: rawPublicKey.toString("base64"),
|
|
390
|
+
keyId: rawPublicKey.subarray(0, 8).toString("hex"),
|
|
391
|
+
sign: (bytes) => sign(null, bytes, privateKey).toString("base64")
|
|
392
|
+
};
|
|
197
393
|
}
|
|
198
|
-
|
|
199
|
-
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/device/server.ts
|
|
396
|
+
const DEVICE_SCHEME = "dotdev";
|
|
397
|
+
const FILES_PATH = "/dev/files/";
|
|
398
|
+
const WAIT_MS = 25e3;
|
|
399
|
+
const MAX_LOG_BYTES = 1048576;
|
|
400
|
+
const LINK_LIFETIME_SECONDS = 43200;
|
|
401
|
+
const BUILD_MAC_HEADER = "dot-build-mac";
|
|
402
|
+
function signedLinkMessage(server, secret, expires, keyId) {
|
|
403
|
+
return [
|
|
404
|
+
"dotdev-connect",
|
|
405
|
+
server,
|
|
406
|
+
secret,
|
|
407
|
+
String(expires),
|
|
408
|
+
keyId
|
|
409
|
+
].join("\n");
|
|
410
|
+
}
|
|
411
|
+
function deviceToken(secret) {
|
|
412
|
+
return hmacWithSecret(secret, "dotdev-token");
|
|
413
|
+
}
|
|
414
|
+
function buildMac(secret, body) {
|
|
415
|
+
return hmacWithSecret(secret, `dotdev-build\n${body}`);
|
|
416
|
+
}
|
|
417
|
+
async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {}, developerKey } = {}) {
|
|
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");
|
|
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) {
|
|
490
|
+
const from = request.socket.remoteAddress ?? "a device";
|
|
491
|
+
if (request.method === "GET" && request.url === "/dev/link") {
|
|
492
|
+
this.#sendSignature(response);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!isAuthorized(request, this.#token)) {
|
|
496
|
+
response.writeHead(401).end();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const url = new URL(request.url ?? "/", "http://device");
|
|
500
|
+
const path = url.pathname;
|
|
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);
|
|
505
|
+
else response.writeHead(404).end();
|
|
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 });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const wake = () => {
|
|
552
|
+
clearTimeout(timer);
|
|
553
|
+
this.#waiting.delete(wake);
|
|
554
|
+
sendJson(response, { generation: this.#generation });
|
|
555
|
+
};
|
|
556
|
+
const timer = setTimeout(() => {
|
|
557
|
+
this.#waiting.delete(wake);
|
|
558
|
+
response.writeHead(204).end();
|
|
559
|
+
}, WAIT_MS);
|
|
560
|
+
this.#waiting.add(wake);
|
|
561
|
+
response.on("close", () => {
|
|
562
|
+
clearTimeout(timer);
|
|
563
|
+
this.#waiting.delete(wake);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
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");
|
|
576
|
+
}
|
|
577
|
+
function localNetworkAddress() {
|
|
578
|
+
for (const addresses of Object.values(networkInterfaces())) for (const candidate of addresses ?? []) if (candidate.family === "IPv4" && !candidate.internal) return candidate.address;
|
|
579
|
+
}
|
|
580
|
+
async function readBuild(dir) {
|
|
581
|
+
const files = /* @__PURE__ */ new Map();
|
|
582
|
+
const entries = await readdir(dir, {
|
|
583
|
+
recursive: true,
|
|
584
|
+
withFileTypes: true
|
|
585
|
+
});
|
|
586
|
+
for (const entry of entries) {
|
|
587
|
+
if (!entry.isFile() || entry.name.endsWith(".map") || entry.name.endsWith(".zip")) continue;
|
|
588
|
+
const path = join(entry.parentPath, entry.name);
|
|
589
|
+
const bytes = await readFile(path);
|
|
590
|
+
const hash = createHash("sha256").update(bytes).digest("base64url");
|
|
591
|
+
const name = relative(dir, path).split(sep).join("/");
|
|
592
|
+
files.set(name, {
|
|
593
|
+
bytes,
|
|
594
|
+
hash
|
|
595
|
+
});
|
|
596
|
+
}
|
|
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
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function isAuthorized(request, token) {
|
|
608
|
+
const given = Buffer.from(request.headers.authorization ?? "");
|
|
609
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
610
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
611
|
+
}
|
|
612
|
+
function sendJson(response, body) {
|
|
613
|
+
response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
614
|
+
}
|
|
615
|
+
async function readLogText(request) {
|
|
616
|
+
const chunks = [];
|
|
617
|
+
let size = 0;
|
|
618
|
+
for await (const chunk of request) {
|
|
619
|
+
size += chunk.length;
|
|
620
|
+
if (size > MAX_LOG_BYTES) throw new Error(`A device sent more than ${MAX_LOG_BYTES} bytes of log at once`);
|
|
621
|
+
chunks.push(chunk);
|
|
622
|
+
}
|
|
623
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/simulator.ts
|
|
627
|
+
function startDotSimulator(project, outDir, port) {
|
|
628
|
+
return startSimulator({
|
|
629
|
+
dotDir: outDir,
|
|
630
|
+
reactHostDir: installedRuntime(project.root).react.dir,
|
|
631
|
+
name: project.dotJson.name,
|
|
632
|
+
port,
|
|
633
|
+
storageDir: join(project.root, ".dot", "files")
|
|
634
|
+
});
|
|
200
635
|
}
|
|
201
636
|
//#endregion
|
|
202
637
|
//#region src/dev.ts
|
|
203
638
|
const CHANGE_DEBOUNCE_MS = 50;
|
|
204
|
-
async function dev({ port, ...options }) {
|
|
639
|
+
async function dev({ port, device, ...options }) {
|
|
205
640
|
const project = await loadProject(options.root);
|
|
206
|
-
const outDir =
|
|
641
|
+
const outDir = resolveOutDir(project.root, options.outDir);
|
|
207
642
|
const buildOptions = {
|
|
208
643
|
mode: "development",
|
|
209
644
|
...options,
|
|
210
645
|
outDir
|
|
211
646
|
};
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
printWarnings(await build(buildOptions));
|
|
216
|
-
printBuildTime(started);
|
|
217
|
-
return null;
|
|
218
|
-
} catch (error) {
|
|
219
|
-
const message = describeError(error).replaceAll(project.root + sep, "");
|
|
220
|
-
console.error(`error: ${message}`);
|
|
221
|
-
return message;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
const firstError = await buildApp();
|
|
225
|
-
const simulator = await startSimulator({
|
|
226
|
-
dotDir: outDir,
|
|
227
|
-
reactHostDir: installedRuntime(project.root).react.dir,
|
|
228
|
-
name: project.dotJson.name,
|
|
229
|
-
port
|
|
230
|
-
});
|
|
647
|
+
const firstError = await buildDot(buildOptions, project.root);
|
|
648
|
+
const simulator = await startDotSimulator(project, outDir, port);
|
|
231
649
|
if (firstError) simulator.error(firstError);
|
|
232
|
-
|
|
650
|
+
printSimulatorUrl(simulator.url);
|
|
651
|
+
let deviceServer;
|
|
652
|
+
if (device) deviceServer = await startDevices(outDir, firstError);
|
|
653
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
654
|
+
const pagesDir = join(project.root, PAGES_DIR) + sep;
|
|
233
655
|
const rebuilds = new RebuildQueue(async () => {
|
|
234
|
-
const
|
|
235
|
-
|
|
656
|
+
const filesInThisBuild = [...changedFiles];
|
|
657
|
+
const error = await buildDot(buildOptions, project.root);
|
|
658
|
+
if (error) {
|
|
659
|
+
simulator.error(error);
|
|
660
|
+
deviceServer?.fail(error);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
for (const path of filesInThisBuild) changedFiles.delete(path);
|
|
664
|
+
const hot = filesInThisBuild.every((path) => path.startsWith(pagesDir));
|
|
665
|
+
if (hot) simulator.hotReload();
|
|
236
666
|
else simulator.reload();
|
|
667
|
+
await deviceServer?.publish(outDir, { hot });
|
|
237
668
|
});
|
|
238
669
|
let debounceTimer;
|
|
239
670
|
const watcher = watch(project.root, { recursive: true }, (_event, file) => {
|
|
240
671
|
if (!file || isIgnored(file)) return;
|
|
241
672
|
const path = resolve(project.root, file);
|
|
242
673
|
if (path === outDir || path.startsWith(outDir + sep)) return;
|
|
674
|
+
changedFiles.add(path);
|
|
243
675
|
clearTimeout(debounceTimer);
|
|
244
676
|
debounceTimer = setTimeout(() => {
|
|
245
677
|
console.log(`Changed ${relative(project.root, path)}`);
|
|
@@ -249,9 +681,41 @@ async function dev({ port, ...options }) {
|
|
|
249
681
|
return async () => {
|
|
250
682
|
clearTimeout(debounceTimer);
|
|
251
683
|
watcher.close();
|
|
252
|
-
await simulator.close();
|
|
684
|
+
await Promise.all([simulator.close(), deviceServer?.close()]);
|
|
253
685
|
};
|
|
254
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
|
+
}
|
|
713
|
+
function printDeviceLink(link) {
|
|
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");
|
|
715
|
+
const qrCode = renderUnicodeCompact(link, { border: 1 });
|
|
716
|
+
console.log(qrCode.replace(/^/gm, " "));
|
|
717
|
+
console.log(`\n ${link}\n`);
|
|
718
|
+
}
|
|
255
719
|
var RebuildQueue = class {
|
|
256
720
|
#rebuild;
|
|
257
721
|
#building = false;
|
|
@@ -284,18 +748,136 @@ function isIgnored(file) {
|
|
|
284
748
|
});
|
|
285
749
|
}
|
|
286
750
|
//#endregion
|
|
751
|
+
//#region src/preview.ts
|
|
752
|
+
async function preview({ port, ...options }) {
|
|
753
|
+
const project = await loadProject(options.root);
|
|
754
|
+
const outDir = resolveOutDir(project.root, options.outDir);
|
|
755
|
+
const startedAt = performance.now();
|
|
756
|
+
printWarnings(await build({
|
|
757
|
+
...options,
|
|
758
|
+
outDir,
|
|
759
|
+
mode: "production"
|
|
760
|
+
}));
|
|
761
|
+
printBuildTime(startedAt);
|
|
762
|
+
const simulator = await startDotSimulator(project, outDir, port);
|
|
763
|
+
printSimulatorUrl(simulator.url);
|
|
764
|
+
return simulator;
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/prompts.ts
|
|
768
|
+
const TERMINAL = {
|
|
769
|
+
input: process.stdin,
|
|
770
|
+
output: process.stdout
|
|
771
|
+
};
|
|
772
|
+
const ERASE_BELOW = "\x1B[0J";
|
|
773
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
774
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
775
|
+
async function select(question, choices, fallback, { input, output } = TERMINAL) {
|
|
776
|
+
if (!input.isTTY) return fallback;
|
|
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);
|
|
782
|
+
const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
|
|
783
|
+
const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
|
|
784
|
+
if (index === selectedIndex) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], value)}${hint}${defaultMark}`;
|
|
785
|
+
return ` ${value}${hint}${defaultMark}`;
|
|
786
|
+
};
|
|
787
|
+
let drawnLines = 0;
|
|
788
|
+
const draw = (lines) => {
|
|
789
|
+
const moveUp = drawnLines > 0 ? cursorUp(drawnLines) : "";
|
|
790
|
+
output.write(`${moveUp}${ERASE_BELOW}${lines.join("\n")}\n`);
|
|
791
|
+
drawnLines = lines.length;
|
|
792
|
+
};
|
|
793
|
+
const drawQuestionAndChoices = () => {
|
|
794
|
+
const keysHint = styleText("dim", "↑/↓ to move, Enter to choose");
|
|
795
|
+
draw([`${styleText("bold", question)} ${keysHint}`, ...choices.map(formatChoice)]);
|
|
796
|
+
};
|
|
797
|
+
emitKeypressEvents(input);
|
|
798
|
+
input.setRawMode(true);
|
|
799
|
+
input.resume();
|
|
800
|
+
output.write(HIDE_CURSOR);
|
|
801
|
+
drawQuestionAndChoices();
|
|
802
|
+
return new Promise((resolve) => {
|
|
803
|
+
const onKeypress = (_text, key) => {
|
|
804
|
+
if (key.ctrl && key.name === "c") {
|
|
805
|
+
output.write(`${SHOW_CURSOR}\n`);
|
|
806
|
+
process.exit(130);
|
|
807
|
+
}
|
|
808
|
+
if (key.name === "up" || key.name === "k") {
|
|
809
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
810
|
+
drawQuestionAndChoices();
|
|
811
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
812
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
813
|
+
drawQuestionAndChoices();
|
|
814
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
815
|
+
const { value } = choices[selectedIndex];
|
|
816
|
+
input.off("keypress", onKeypress);
|
|
817
|
+
input.setRawMode(false);
|
|
818
|
+
input.pause();
|
|
819
|
+
draw([`${styleText("bold", question)} ${styleText("cyan", value)}`]);
|
|
820
|
+
output.write(SHOW_CURSOR);
|
|
821
|
+
resolve(value);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
input.on("keypress", onKeypress);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async function confirm(question, fallback, terminal = TERMINAL) {
|
|
828
|
+
if (!terminal.input.isTTY) return false;
|
|
829
|
+
return await select(question, [{ value: "Yes" }, { value: "No" }], fallback ? "Yes" : "No", terminal) === "Yes";
|
|
830
|
+
}
|
|
831
|
+
async function ask(question, fallback, hint, { input, output } = TERMINAL) {
|
|
832
|
+
if (!input.isTTY) return fallback;
|
|
833
|
+
const hintText = hint ? styleText("dim", `, ${hint}`) : "";
|
|
834
|
+
const defaultMark = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
|
|
835
|
+
const readline = createInterface({
|
|
836
|
+
input,
|
|
837
|
+
output
|
|
838
|
+
});
|
|
839
|
+
try {
|
|
840
|
+
return (await readline.question(`${styleText("bold", question)}${hintText}${defaultMark}: `)).trim() || fallback;
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (error.name === "AbortError") return;
|
|
843
|
+
throw error;
|
|
844
|
+
} finally {
|
|
845
|
+
readline.close();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function cursorUp(lines) {
|
|
849
|
+
return `\x1b[${lines}A`;
|
|
850
|
+
}
|
|
851
|
+
//#endregion
|
|
287
852
|
//#region src/cli.ts
|
|
288
853
|
const USAGE = `Usage:
|
|
289
|
-
dotctl create [dir] [--name <name>] [--
|
|
290
|
-
|
|
854
|
+
dotctl create [dir] [--name <name>] [--package <com.mysalone.thing>]
|
|
855
|
+
[--template <starter|showcase>] [--pm <npm|pnpm|yarn|bun>]
|
|
856
|
+
Create a dot. Asks for its name, its package name
|
|
857
|
+
(a reverse domain name you own) and its template:
|
|
858
|
+
starter is one page to build on, showcase has a
|
|
859
|
+
page for each feature
|
|
860
|
+
dotctl create page <path> Add a page to the dot you are in. Writes
|
|
861
|
+
pages/<path>/ with view.dml, state.ts and controller.ts
|
|
291
862
|
dotctl build [root] [options] Build the minified bundles the host app runs
|
|
292
|
-
dotctl dev [root] [options] Run in the browser simulator
|
|
863
|
+
dotctl dev [root] [options] Run the dot in the browser simulator and rebuild on
|
|
864
|
+
every change
|
|
865
|
+
dotctl preview [root] [options] Build as \`build\` does, then run that build in the
|
|
866
|
+
browser simulator, exactly as the host app gets it
|
|
867
|
+
dotctl keys show Show this computer's developer key: its public key
|
|
868
|
+
and key ID. The key signs the links that
|
|
869
|
+
\`dev --device\` shows, so Dash will open them
|
|
870
|
+
dotctl lsp [--stdio] Start the .dml language server for editors. It talks
|
|
871
|
+
over stdin and stdout
|
|
293
872
|
|
|
294
873
|
Options:
|
|
295
874
|
--out-dir <dir> Output directory (default: <root>/dist)
|
|
296
|
-
--conditions <list> Extra export conditions,
|
|
297
|
-
--port <port> Simulator port for \`dev
|
|
298
|
-
(default: ${DEFAULT_PORT})
|
|
875
|
+
--conditions <list> Extra export conditions, separated by commas
|
|
876
|
+
--port <port> Simulator port for \`dev\` and \`preview\`. If it is taken, the
|
|
877
|
+
next free port is used (default: ${DEFAULT_PORT})
|
|
878
|
+
--device With \`dev\`, also serve the dot to the Dot example apps on phones
|
|
879
|
+
on your local network, through a link and QR code. The apps
|
|
880
|
+
reload after each build
|
|
299
881
|
-h, --help Show this message`;
|
|
300
882
|
async function main(argv) {
|
|
301
883
|
const { values, positionals } = parseArgs({
|
|
@@ -305,71 +887,178 @@ async function main(argv) {
|
|
|
305
887
|
"out-dir": { type: "string" },
|
|
306
888
|
conditions: { type: "string" },
|
|
307
889
|
port: { type: "string" },
|
|
890
|
+
device: { type: "boolean" },
|
|
308
891
|
name: { type: "string" },
|
|
892
|
+
package: { type: "string" },
|
|
893
|
+
template: { type: "string" },
|
|
309
894
|
pm: { type: "string" },
|
|
310
895
|
help: {
|
|
311
896
|
type: "boolean",
|
|
312
897
|
short: "h"
|
|
313
|
-
}
|
|
898
|
+
},
|
|
899
|
+
stdio: { type: "boolean" }
|
|
314
900
|
}
|
|
315
901
|
});
|
|
316
|
-
const [command, target] = positionals;
|
|
317
|
-
if (
|
|
318
|
-
if (values.help || command !== "build" && command !== "dev") {
|
|
902
|
+
const [command, target, pagePath] = positionals;
|
|
903
|
+
if (values.help) {
|
|
319
904
|
console.log(USAGE);
|
|
320
|
-
return
|
|
905
|
+
return 0;
|
|
321
906
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}
|
|
326
932
|
}
|
|
327
933
|
const options = { root: target ?? "." };
|
|
328
934
|
if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
|
|
329
935
|
if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
|
|
330
|
-
if (command === "dev")
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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;
|
|
338
959
|
}
|
|
339
|
-
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();
|
|
340
970
|
const result = await build(options);
|
|
341
|
-
|
|
971
|
+
printFiles(result.files.filter((file) => !file.endsWith(".map")));
|
|
342
972
|
printWarnings(result);
|
|
343
|
-
printBuildTime(
|
|
973
|
+
printBuildTime(startedAt);
|
|
344
974
|
return 0;
|
|
345
975
|
}
|
|
346
|
-
|
|
976
|
+
function waitForCtrlC() {
|
|
977
|
+
return new Promise((resolve) => {
|
|
978
|
+
process.once("SIGINT", () => resolve());
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async function showKeys(action) {
|
|
982
|
+
if (action !== "show") {
|
|
983
|
+
console.error("Usage: dotctl keys show");
|
|
984
|
+
return 1;
|
|
985
|
+
}
|
|
986
|
+
const path = defaultDeveloperKeyPath();
|
|
987
|
+
const key = await loadDeveloperKey(path);
|
|
988
|
+
if (!key) {
|
|
989
|
+
console.error(missingDeveloperKeyHint(path));
|
|
990
|
+
return 1;
|
|
991
|
+
}
|
|
992
|
+
console.log(`\n Public key: ${key.publicKey}\n Key ID: ${key.keyId}\n`);
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
async function addPage(typedPath) {
|
|
996
|
+
if (typedPath === void 0) {
|
|
997
|
+
console.error("Give the page a path, such as: dotctl create page cart/checkout");
|
|
998
|
+
return 1;
|
|
999
|
+
}
|
|
1000
|
+
const path = trimSlashes(typedPath);
|
|
1001
|
+
const { files, listing } = await createPage(path, process.cwd());
|
|
1002
|
+
printFiles(files);
|
|
1003
|
+
if (listing === "added") console.log(`\nListed '${path}' in dot.ts.`);
|
|
1004
|
+
else if (listing === "manual") console.log(`\nAdd it to the pages in dot.ts so it gets built: defineDot({ pages: [..., '${path}'] })`);
|
|
1005
|
+
return 0;
|
|
1006
|
+
}
|
|
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;
|
|
1015
|
+
if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
|
|
1016
|
+
console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
|
|
1017
|
+
return 1;
|
|
1018
|
+
}
|
|
1019
|
+
const packageManagerFlag = flags.packageManager;
|
|
347
1020
|
if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
|
|
348
1021
|
console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
|
|
349
1022
|
return 1;
|
|
350
1023
|
}
|
|
351
|
-
name
|
|
1024
|
+
let name = flags.name;
|
|
1025
|
+
if (name === void 0) name = await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
|
|
352
1026
|
if (!name) {
|
|
353
|
-
console.error("
|
|
1027
|
+
console.error("The dot needs a name. Pass one with: dotctl create [dir] --name \"My Dot\"");
|
|
1028
|
+
return 1;
|
|
1029
|
+
}
|
|
1030
|
+
const dotPackageFlag = flags.dotPackage;
|
|
1031
|
+
if (dotPackageFlag !== void 0 && !isDotPackage(dotPackageFlag)) {
|
|
1032
|
+
console.error(`--package "${dotPackageFlag}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
354
1033
|
return 1;
|
|
355
1034
|
}
|
|
1035
|
+
let dotPackage = dotPackageFlag;
|
|
1036
|
+
if (dotPackage === void 0) dotPackage = await askDotPackage(suggestedDotPackage(toPackageName(name)));
|
|
1037
|
+
if (!dotPackage) return 1;
|
|
1038
|
+
let template = templateFlag;
|
|
1039
|
+
if (template === void 0) template = await chooseTemplate();
|
|
356
1040
|
const { root, workspace } = await create({
|
|
357
1041
|
name,
|
|
358
|
-
dir: dir ?? toPackageName(name)
|
|
1042
|
+
dir: dir ?? toPackageName(name),
|
|
1043
|
+
dotPackage,
|
|
1044
|
+
template
|
|
359
1045
|
});
|
|
360
1046
|
const path = relative(process.cwd(), root) || ".";
|
|
361
1047
|
console.log(`\nCreated ${name} in ${path}`);
|
|
362
1048
|
if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
|
|
363
|
-
let packageManager
|
|
364
|
-
if (
|
|
1049
|
+
let packageManager;
|
|
1050
|
+
if (workspace) packageManager = "pnpm";
|
|
1051
|
+
else packageManager = packageManagerFlag ?? launchedBy();
|
|
1052
|
+
console.log();
|
|
1053
|
+
if (await confirm("Install and start it now?", true)) {
|
|
365
1054
|
if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
|
|
366
|
-
if (await
|
|
367
|
-
console.error(`\n${packageManager} install failed.`);
|
|
1055
|
+
if (await runCommand(packageManager, ["install"], root) === 0) return runCommand(packageManager, ["run", "dev"], root);
|
|
1056
|
+
console.error(`\n${packageManager} install failed. Fix the problem above, then run the steps below.`);
|
|
368
1057
|
}
|
|
369
1058
|
console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
|
|
370
1059
|
return 0;
|
|
371
1060
|
}
|
|
372
|
-
function
|
|
1061
|
+
function runCommand(command, args, cwd) {
|
|
373
1062
|
return new Promise((resolvePromise, reject) => {
|
|
374
1063
|
const child = spawn(command, args, {
|
|
375
1064
|
cwd,
|
|
@@ -380,45 +1069,32 @@ function run(command, args, cwd) {
|
|
|
380
1069
|
child.on("exit", (code) => resolvePromise(code ?? 1));
|
|
381
1070
|
});
|
|
382
1071
|
}
|
|
383
|
-
async function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
async function ask(question, fallback) {
|
|
389
|
-
return readAnswer(`${question}${fallback ? ` (${fallback})` : ""}`, fallback);
|
|
390
|
-
}
|
|
391
|
-
async function readAnswer(prompt, fallback) {
|
|
392
|
-
if (!process.stdin.isTTY) return fallback;
|
|
393
|
-
const readline = createInterface({
|
|
394
|
-
input: process.stdin,
|
|
395
|
-
output: process.stdout
|
|
396
|
-
});
|
|
397
|
-
try {
|
|
398
|
-
return (await readline.question(`${prompt} `)).trim() || fallback;
|
|
399
|
-
} catch (error) {
|
|
400
|
-
if (error.name === "AbortError") return;
|
|
401
|
-
throw error;
|
|
402
|
-
} finally {
|
|
403
|
-
readline.close();
|
|
1072
|
+
async function askDotPackage(suggested) {
|
|
1073
|
+
while (true) {
|
|
1074
|
+
const answer = await ask("Package name", suggested, "a reverse domain name you own");
|
|
1075
|
+
if (answer === void 0 || isDotPackage(answer)) return answer;
|
|
1076
|
+
console.error(`"${answer}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
404
1077
|
}
|
|
405
1078
|
}
|
|
1079
|
+
function chooseTemplate() {
|
|
1080
|
+
console.log();
|
|
1081
|
+
return select("Template?", TEMPLATES.map((template) => ({
|
|
1082
|
+
value: template,
|
|
1083
|
+
hint: TEMPLATE_HINTS[template]
|
|
1084
|
+
})), "starter");
|
|
1085
|
+
}
|
|
406
1086
|
async function choosePackageManager(suggested) {
|
|
407
1087
|
const installed = installedManagers();
|
|
408
1088
|
if (installed.length < 2) return installed[0] ?? suggested;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (answer === void 0) return fallback;
|
|
413
|
-
if (isPackageManager(answer) && installed.includes(answer)) return answer;
|
|
414
|
-
console.log(`Choose one of ${installed.join(", ")}.`);
|
|
415
|
-
}
|
|
1089
|
+
let fallback = installed[0];
|
|
1090
|
+
if (installed.includes(suggested)) fallback = suggested;
|
|
1091
|
+
return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
|
|
416
1092
|
}
|
|
417
|
-
main(process.argv.slice(2)).then((
|
|
1093
|
+
main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode), (error) => {
|
|
418
1094
|
console.error(`error: ${describeError(error)}`);
|
|
419
1095
|
process.exit(1);
|
|
420
1096
|
});
|
|
421
1097
|
//#endregion
|
|
422
|
-
export {};
|
|
1098
|
+
export { findDotRoot as t };
|
|
423
1099
|
|
|
424
1100
|
//# sourceMappingURL=cli.js.map
|