@monimesl/dot-cli 0.1.0-alpha.9 → 1.0.0-beta.1
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 +72 -59
- package/dist/cli.js +539 -341
- package/dist/cli.js.map +1 -1
- package/dist/{server-BfPAnsSf.js → server-DMnWDdJ6.js} +570 -478
- package/dist/server-DMnWDdJ6.js.map +1 -0
- package/dist/templates/showcase/dot.ts +53 -16
- package/dist/templates/showcase/pages/basics/bars/controller.ts +47 -0
- package/dist/templates/showcase/pages/basics/bars/state.ts +7 -0
- package/dist/templates/showcase/pages/basics/bars/view.dml +36 -0
- package/dist/templates/showcase/pages/{text → basics/bindings}/controller.ts +5 -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/colors/controller.ts +112 -0
- package/dist/templates/showcase/pages/basics/colors/palette.ts +52 -0
- package/dist/templates/showcase/pages/basics/colors/state.ts +17 -0
- package/dist/templates/showcase/pages/basics/colors/view.dml +142 -0
- package/dist/templates/showcase/pages/basics/concert/controller.ts +16 -0
- package/dist/templates/showcase/pages/basics/concert/state.ts +7 -0
- package/dist/templates/showcase/pages/basics/concert/view.dml +20 -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 +18 -0
- package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
- package/dist/templates/showcase/pages/basics/pick-genre/view.dml +10 -0
- package/dist/templates/showcase/pages/basics/share/concerts.ts +31 -0
- package/dist/templates/showcase/pages/basics/share/controller.ts +12 -0
- package/dist/templates/showcase/pages/basics/share/state.ts +5 -0
- package/dist/templates/showcase/pages/basics/share/view.dml +21 -0
- package/dist/templates/showcase/pages/{state → basics/state}/controller.ts +1 -0
- package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
- package/dist/templates/showcase/pages/{carousel → components/carousel}/controller.ts +2 -1
- package/dist/templates/showcase/pages/{carousel → components/carousel}/state.ts +5 -5
- package/dist/templates/showcase/pages/{carousel → components/carousel}/view.dml +7 -5
- package/dist/templates/showcase/pages/{feedback → components/feedback}/controller.ts +13 -2
- package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
- package/dist/templates/showcase/pages/{feedback → components/feedback}/view.dml +38 -9
- package/dist/templates/showcase/pages/{forms → components/forms}/controller.ts +23 -19
- package/dist/templates/showcase/pages/{forms → components/forms}/state.ts +6 -3
- 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/{images → components/images}/controller.ts +4 -1
- package/dist/templates/showcase/pages/components/images/view.dml +83 -0
- package/dist/templates/showcase/pages/{layout → components/layout}/controller.ts +3 -2
- 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/{lists → components/lists}/state.ts +43 -30
- package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
- package/dist/templates/showcase/pages/{payments → components/money}/controller.ts +7 -8
- 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/{lists → components/sheets}/controller.ts +10 -11
- 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/{filters → components/tabs}/state.ts +32 -23
- 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/account/controller.ts +32 -0
- package/dist/templates/showcase/pages/dash/account/state.ts +9 -0
- package/dist/templates/showcase/pages/dash/account/view.dml +33 -0
- package/dist/templates/showcase/pages/{cache → dash/cache}/controller.ts +9 -4
- package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
- package/dist/templates/showcase/pages/dash/calls/controller.ts +67 -0
- package/dist/templates/showcase/pages/dash/calls/state.ts +9 -0
- package/dist/templates/showcase/pages/dash/calls/view.dml +69 -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/crypto/controller.ts +38 -0
- package/dist/templates/showcase/pages/dash/crypto/state.ts +10 -0
- package/dist/templates/showcase/pages/dash/crypto/view.dml +48 -0
- package/dist/templates/showcase/pages/{files → dash/files}/controller.ts +7 -2
- package/dist/templates/showcase/pages/{files → dash/files}/state.ts +4 -0
- package/dist/templates/showcase/pages/dash/files/view.dml +78 -0
- package/dist/templates/showcase/pages/dash/media/controller.ts +65 -0
- package/dist/templates/showcase/pages/dash/media/state.ts +12 -0
- package/dist/templates/showcase/pages/dash/media/view.dml +53 -0
- package/dist/templates/showcase/pages/{network → dash/network}/controller.ts +84 -20
- package/dist/templates/showcase/pages/{network → dash/network}/state.ts +14 -1
- package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
- package/dist/templates/showcase/pages/dash/payments/cases.ts +445 -0
- package/dist/templates/showcase/pages/dash/payments/controller.ts +149 -0
- package/dist/templates/showcase/pages/dash/payments/state.ts +33 -0
- package/dist/templates/showcase/pages/dash/payments/view.dml +71 -0
- package/dist/templates/showcase/pages/{secrets → dash/secrets}/controller.ts +2 -1
- 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 +96 -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 +1 -1
- package/dist/templates/showcase/pages/index/state.ts +175 -60
- package/dist/templates/showcase/pages/index/view.dml +6 -3
- package/package.json +6 -6
- package/dist/server-BfPAnsSf.js.map +0 -1
- package/dist/templates/showcase/pages/cache/view.dml +0 -36
- package/dist/templates/showcase/pages/device/controller.ts +0 -80
- package/dist/templates/showcase/pages/device/state.ts +0 -7
- package/dist/templates/showcase/pages/device/view.dml +0 -49
- package/dist/templates/showcase/pages/feedback/state.ts +0 -8
- package/dist/templates/showcase/pages/files/view.dml +0 -50
- package/dist/templates/showcase/pages/filters/controller.ts +0 -27
- package/dist/templates/showcase/pages/filters/view.dml +0 -44
- package/dist/templates/showcase/pages/forms/view.dml +0 -49
- package/dist/templates/showcase/pages/images/view.dml +0 -53
- package/dist/templates/showcase/pages/layout/view.dml +0 -103
- package/dist/templates/showcase/pages/lists/view.dml +0 -59
- package/dist/templates/showcase/pages/navigation/controller.ts +0 -65
- package/dist/templates/showcase/pages/navigation/state.ts +0 -17
- package/dist/templates/showcase/pages/navigation/view.dml +0 -34
- package/dist/templates/showcase/pages/network/view.dml +0 -67
- package/dist/templates/showcase/pages/payments/view.dml +0 -45
- package/dist/templates/showcase/pages/secrets/view.dml +0 -24
- package/dist/templates/showcase/pages/state/view.dml +0 -33
- package/dist/templates/showcase/pages/text/state.ts +0 -8
- package/dist/templates/showcase/pages/text/view.dml +0 -58
- /package/dist/templates/showcase/pages/{state → basics/state}/state.ts +0 -0
- /package/dist/templates/showcase/pages/{images → components/images}/state.ts +0 -0
- /package/dist/templates/showcase/pages/{layout → components/layout}/state.ts +0 -0
- /package/dist/templates/showcase/pages/{payments → components/money}/state.ts +0 -0
- /package/dist/templates/showcase/pages/{cache → dash/cache}/state.ts +0 -0
- /package/dist/templates/showcase/pages/{secrets → dash/secrets}/state.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,13 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parseArgs, styleText } from "node:util";
|
|
5
|
-
import { CompileError, PAGES_DIR, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
|
|
5
|
+
import { CompileError, DOT_FILE, DOT_JSON_FILE, PAGES_DIR, build, installedRuntime, isDotPackage, loadProject } from "@monimesl/dot-compiler";
|
|
6
6
|
import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
8
8
|
import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
9
9
|
import { DmlParseError } from "@monimesl/dot-dml-parser";
|
|
10
10
|
import { ScriptParseError, parseDotModule } from "@monimesl/dot-script-parser";
|
|
11
|
-
import { isPageId } from "@monimesl/dot-core";
|
|
11
|
+
import { DEFAULT_DASH_API_VERSION, isPageId } from "@monimesl/dot-core";
|
|
12
12
|
import { renderUnicodeCompact } from "uqr";
|
|
13
13
|
import { createHash, createHmac, createPrivateKey, createPublicKey, randomBytes, sign, timingSafeEqual } from "node:crypto";
|
|
14
14
|
import { homedir, networkInterfaces } from "node:os";
|
|
@@ -16,17 +16,24 @@ import { createServer } from "node:http";
|
|
|
16
16
|
import { emitKeypressEvents } from "node:readline";
|
|
17
17
|
import { createInterface } from "node:readline/promises";
|
|
18
18
|
//#region src/report.ts
|
|
19
|
-
/** A mistake in how a command was used, which its message alone explains. */
|
|
20
19
|
var CommandError = class extends Error {
|
|
21
20
|
name = "CommandError";
|
|
22
21
|
};
|
|
23
22
|
function describeError(error) {
|
|
24
23
|
if (error instanceof CommandError || error instanceof CompileError || error instanceof DmlParseError || error instanceof ScriptParseError) return error.message;
|
|
25
|
-
if (error
|
|
24
|
+
if (isArgumentError(error)) {
|
|
26
25
|
const [firstSentence] = error.message.split(". ");
|
|
27
26
|
return `${firstSentence}. See dotctl --help.`;
|
|
28
27
|
}
|
|
29
|
-
|
|
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)}`);
|
|
30
37
|
}
|
|
31
38
|
function printWarnings({ warnings }) {
|
|
32
39
|
for (const warning of warnings) console.warn(`warning: ${warning.message}`);
|
|
@@ -34,15 +41,38 @@ function printWarnings({ warnings }) {
|
|
|
34
41
|
function printBuildTime(startedAt) {
|
|
35
42
|
console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
|
|
36
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
|
+
function isOneOf(values, name) {
|
|
65
|
+
return values.includes(name);
|
|
66
|
+
}
|
|
37
67
|
//#endregion
|
|
38
68
|
//#region src/create/template.ts
|
|
39
69
|
const TEMPLATES = ["starter", "showcase"];
|
|
40
70
|
const TEMPLATE_HINTS = {
|
|
41
71
|
starter: "one page to build on",
|
|
42
|
-
showcase: "a page for each feature, to
|
|
72
|
+
showcase: "a page for each feature, to learn from"
|
|
43
73
|
};
|
|
44
74
|
function isTemplate(name) {
|
|
45
|
-
return TEMPLATES
|
|
75
|
+
return isOneOf(TEMPLATES, name);
|
|
46
76
|
}
|
|
47
77
|
const PAGE_FILES = {
|
|
48
78
|
"view.dml": "<View>\n <Text>My new page</Text>\n</View>\n",
|
|
@@ -60,9 +90,9 @@ const TEMPLATE_FILES = [
|
|
|
60
90
|
"statics",
|
|
61
91
|
".env"
|
|
62
92
|
];
|
|
63
|
-
const DOT_JSON_SCHEMA = "
|
|
64
|
-
const
|
|
65
|
-
function
|
|
93
|
+
const DOT_JSON_SCHEMA = "https://unpkg.com/@monimesl/dot-runtime/dot.schema.json";
|
|
94
|
+
const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
|
|
95
|
+
function generatedFiles(options) {
|
|
66
96
|
return {
|
|
67
97
|
"package.json": packageJson(options),
|
|
68
98
|
"tsconfig.json": tsconfig(options),
|
|
@@ -73,7 +103,7 @@ function projectFiles(options) {
|
|
|
73
103
|
}
|
|
74
104
|
function packageJson({ packageName, dotVersion, workspace }) {
|
|
75
105
|
const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
|
|
76
|
-
return
|
|
106
|
+
return formatJson({
|
|
77
107
|
name: packageName,
|
|
78
108
|
private: true,
|
|
79
109
|
type: "module",
|
|
@@ -103,7 +133,7 @@ function tsconfig({ workspace }) {
|
|
|
103
133
|
verbatimModuleSyntax: true
|
|
104
134
|
};
|
|
105
135
|
if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
|
|
106
|
-
return
|
|
136
|
+
return formatJson({
|
|
107
137
|
compilerOptions,
|
|
108
138
|
include: [
|
|
109
139
|
"dot.ts",
|
|
@@ -113,27 +143,40 @@ function tsconfig({ workspace }) {
|
|
|
113
143
|
]
|
|
114
144
|
});
|
|
115
145
|
}
|
|
116
|
-
function dotJson({ name,
|
|
146
|
+
function dotJson({ name, dotPackage, template }) {
|
|
117
147
|
const dot = {
|
|
148
|
+
id: UNREGISTERED_DOT_ID,
|
|
118
149
|
$schema: DOT_JSON_SCHEMA,
|
|
119
|
-
|
|
150
|
+
package: dotPackage,
|
|
120
151
|
name,
|
|
121
152
|
version: "0.1.0"
|
|
122
153
|
};
|
|
123
|
-
if (template === "showcase")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
if (template === "showcase") {
|
|
155
|
+
dot.permissions = {
|
|
156
|
+
account: "Show your name on your concert ticket",
|
|
157
|
+
camera: "Take a photo for your concert ticket, or scan a ticket's QR code",
|
|
158
|
+
photoLibrary: "Choose a photo for your concert ticket",
|
|
159
|
+
files: "Attach your concert ticket",
|
|
160
|
+
contacts: "Choose a friend to send a ticket to",
|
|
161
|
+
payments: "Pay for concert tickets"
|
|
162
|
+
};
|
|
163
|
+
dot.network = { hosts: [
|
|
164
|
+
"itunes.apple.com",
|
|
165
|
+
"*.mzstatic.com",
|
|
166
|
+
"httpbin.org"
|
|
167
|
+
] };
|
|
168
|
+
}
|
|
169
|
+
dot.dash = { minApiVersion: DEFAULT_DASH_API_VERSION };
|
|
170
|
+
return formatJson(dot);
|
|
129
171
|
}
|
|
130
172
|
const TEMPLATE_READMES = {
|
|
131
|
-
starter: `It starts with one page, \`index
|
|
132
|
-
and add pages as the dot grows.
|
|
133
|
-
|
|
134
|
-
showcase: `It starts as a showcase
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
starter: `It starts with one page, \`index\`. It shows a greeting from \`.env\` and a counter you
|
|
174
|
+
can tap. Change it, and add pages as the dot grows. To see how each feature works, make a showcase
|
|
175
|
+
dot with \`npx dotctl create my-tour --template showcase\`.`,
|
|
176
|
+
showcase: `It starts as a showcase. The \`index\` page links to a page for each feature, in three
|
|
177
|
+
folders: \`pages/basics/\` for how a page works, \`pages/components/\` for what a view can show,
|
|
178
|
+
and \`pages/dash/\` for the phone's features. Read a page to see how it works. Then delete the
|
|
179
|
+
pages you don't need, both from \`pages/\` and from \`dot.ts\`.`
|
|
137
180
|
};
|
|
138
181
|
function readme({ name, template }) {
|
|
139
182
|
return `# ${name}
|
|
@@ -146,41 +189,67 @@ npm run dev # run it in the simulator; it reloads on every change
|
|
|
146
189
|
npm run build # build it for Dash
|
|
147
190
|
\`\`\`
|
|
148
191
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
as \`cart/checkout\`, with the three files to start from, and lists it in \`dot.ts\`. The first page
|
|
153
|
-
there is where the dot starts.
|
|
192
|
+
${TEMPLATE_READMES[template]}
|
|
193
|
+
|
|
194
|
+
## Pages
|
|
154
195
|
|
|
155
|
-
|
|
156
|
-
name you own before you publish: it can't change afterwards. Its \`network.hosts\` lists every host
|
|
157
|
-
the dot sends requests to with \`dash.network\`; requests to any other fail. Your editor completes
|
|
158
|
-
the other fields from the schema it points to.
|
|
196
|
+
Each page is a folder under \`pages/\` with three files:
|
|
159
197
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
198
|
+
- \`view.dml\`: what the page shows. It has one root \`<View>\`, with everything else inside it.
|
|
199
|
+
- \`state.ts\`: the data the page starts with.
|
|
200
|
+
- \`controller.ts\`: how the data changes.
|
|
163
201
|
|
|
164
|
-
|
|
202
|
+
\`npx dotctl create page <path>\` adds a page, such as \`cart/checkout\`, with these three files. It
|
|
203
|
+
also lists the page in \`dot.ts\`. The dot opens on the first page in that list.
|
|
204
|
+
|
|
205
|
+
## dot.json
|
|
206
|
+
|
|
207
|
+
\`dot.json\` describes the dot to Dash. Its \`package\` is a reverse domain name you own, such as
|
|
208
|
+
\`com.mysalone.thing\`. You can't change it after publishing. To publish, register the dot with
|
|
209
|
+
Monime first, and replace its \`id\`, \`${UNREGISTERED_DOT_ID}\`, with the ID Monime gives you.
|
|
210
|
+
\`network.hosts\` lists every host the dot sends requests to with \`dash.network\`. Requests to any
|
|
211
|
+
other host fail. Your editor can complete the other fields from the schema the file points to.
|
|
212
|
+
|
|
213
|
+
## Which Dash opens it
|
|
214
|
+
|
|
215
|
+
\`dash.minApiVersion\` in \`dot.json\` is the Dash API version the dot needs, such as \`1.0\`. Dash
|
|
216
|
+
opens the dot when it offers that version, or a newer minor of the same major. Each Dash call's docs
|
|
217
|
+
say which version it came in, after \`@since\`. To use a newer call and still open on older Dash,
|
|
218
|
+
keep \`minApiVersion\` and check \`this.dash.supports('contacts.pickPhone')\` before the call.
|
|
219
|
+
\`dotctl dev\` warns when the dot makes a newer call without checking.
|
|
220
|
+
|
|
221
|
+
## .env
|
|
222
|
+
|
|
223
|
+
\`.env\` holds public configuration. Code in the dot reads each variable from \`env\`, such as
|
|
224
|
+
\`env.GREETING\`. Every variable is built into the dot, so anyone who has the dot can read it.
|
|
225
|
+
Never put a secret there. \`.env.development\` and \`.env.production\` override it for \`dev\` and
|
|
226
|
+
\`build\`.
|
|
165
227
|
`;
|
|
166
228
|
}
|
|
167
|
-
function
|
|
168
|
-
return packageName.slice(0, 63).replace(/-+$/, "")
|
|
229
|
+
function suggestedDotPackage(packageName) {
|
|
230
|
+
return `com.example.${packageName.slice(0, 63).replace(/-+$/, "")}`;
|
|
231
|
+
}
|
|
232
|
+
function formatJson(value) {
|
|
233
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
169
234
|
}
|
|
170
235
|
//#endregion
|
|
171
236
|
//#region src/create/create.ts
|
|
172
|
-
|
|
237
|
+
const DOT_PACKAGE_RULE = "Use lowercase words joined by dots, such as com.mysalone.thing.";
|
|
238
|
+
async function create({ name, dir, dotPackage, template = "starter" }) {
|
|
173
239
|
const root = resolve(dir);
|
|
174
|
-
if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty`);
|
|
240
|
+
if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty. Choose another folder`);
|
|
175
241
|
const packageName = toPackageName(name);
|
|
176
|
-
if (!packageName) throw new CommandError(`"${name}" needs at least one letter or digit`);
|
|
242
|
+
if (!packageName) throw new CommandError(`The name "${name}" needs at least one letter or digit`);
|
|
243
|
+
dotPackage ??= suggestedDotPackage(packageName);
|
|
244
|
+
if (!isDotPackage(dotPackage)) throw new CommandError(`"${dotPackage}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
177
245
|
const workspace = isInDotWorkspace(root);
|
|
178
246
|
const source = templateDir(template);
|
|
179
247
|
await mkdir(root, { recursive: true });
|
|
180
248
|
for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
|
|
181
|
-
const generated =
|
|
249
|
+
const generated = generatedFiles({
|
|
182
250
|
name,
|
|
183
251
|
packageName,
|
|
252
|
+
dotPackage,
|
|
184
253
|
dotVersion: workspace ? "workspace:*" : `^${cliVersion()}`,
|
|
185
254
|
workspace,
|
|
186
255
|
template
|
|
@@ -198,21 +267,16 @@ function toDisplayName(dir) {
|
|
|
198
267
|
return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
199
268
|
}
|
|
200
269
|
function isInDotWorkspace(dir) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
|
|
207
|
-
}
|
|
208
|
-
const parent = dirname(current);
|
|
209
|
-
if (parent === current) return false;
|
|
210
|
-
current = parent;
|
|
211
|
-
}
|
|
270
|
+
const workspaceRoot = findFolderWith("pnpm-workspace.yaml", dirname(dir));
|
|
271
|
+
if (workspaceRoot === void 0) return false;
|
|
272
|
+
const runtimeManifest = join(workspaceRoot, "packages/runtime/package.json");
|
|
273
|
+
if (!existsSync(runtimeManifest)) return false;
|
|
274
|
+
return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
|
|
212
275
|
}
|
|
213
276
|
function templateDir(template) {
|
|
214
277
|
const example = join(cliRoot(), "../../examples", template);
|
|
215
|
-
|
|
278
|
+
if (existsSync(example)) return example;
|
|
279
|
+
return join(cliRoot(), "dist/templates", template);
|
|
216
280
|
}
|
|
217
281
|
function cliVersion() {
|
|
218
282
|
return readPackageJson(join(cliRoot(), "package.json")).version;
|
|
@@ -224,23 +288,13 @@ function cliRoot() {
|
|
|
224
288
|
return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
|
|
225
289
|
}
|
|
226
290
|
//#endregion
|
|
227
|
-
//#region src/dot-root.ts
|
|
228
|
-
function findDotRoot(dir) {
|
|
229
|
-
while (true) {
|
|
230
|
-
if (existsSync(join(dir, "dot.json"))) return dir;
|
|
231
|
-
const parent = dirname(dir);
|
|
232
|
-
if (parent === dir) return;
|
|
233
|
-
dir = parent;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
//#endregion
|
|
237
291
|
//#region src/create/page.ts
|
|
238
292
|
async function createPage(path, cwd) {
|
|
239
|
-
if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path
|
|
293
|
+
if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path. Use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
|
|
240
294
|
const root = findDotRoot(resolve(cwd));
|
|
241
|
-
if (root === void 0) throw new CommandError("There is no dot.json
|
|
242
|
-
const dir = join(root,
|
|
243
|
-
if (existsSync(dir)) throw new CommandError(`pages/${path} already exists`);
|
|
295
|
+
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");
|
|
296
|
+
const dir = join(root, PAGES_DIR, path);
|
|
297
|
+
if (existsSync(dir)) throw new CommandError(`pages/${path} already exists. Choose another path`);
|
|
244
298
|
await mkdir(dir, { recursive: true });
|
|
245
299
|
const files = [];
|
|
246
300
|
for (const [name, content] of Object.entries(PAGE_FILES)) {
|
|
@@ -251,10 +305,10 @@ async function createPage(path, cwd) {
|
|
|
251
305
|
return {
|
|
252
306
|
dir,
|
|
253
307
|
files,
|
|
254
|
-
listing: await
|
|
308
|
+
listing: await addToDotPages(join(root, DOT_FILE), path)
|
|
255
309
|
};
|
|
256
310
|
}
|
|
257
|
-
async function
|
|
311
|
+
async function addToDotPages(dotFile, path) {
|
|
258
312
|
if (!existsSync(dotFile)) return "manual";
|
|
259
313
|
const source = await readFile(dotFile, "utf8");
|
|
260
314
|
let dot;
|
|
@@ -264,23 +318,25 @@ async function listPage(dotFile, path) {
|
|
|
264
318
|
return "manual";
|
|
265
319
|
}
|
|
266
320
|
if (dot.pages.includes(path)) return "already";
|
|
267
|
-
const updated =
|
|
321
|
+
const updated = insertPage(source, dot.lastPage, path);
|
|
268
322
|
if (!listsPage(updated, dotFile, path)) return "manual";
|
|
269
323
|
await writeFile(dotFile, updated);
|
|
270
324
|
return "added";
|
|
271
325
|
}
|
|
272
|
-
function
|
|
326
|
+
function insertPage(source, lastPage, path) {
|
|
273
327
|
const quote = source[lastPage.start] === "\"" ? "\"" : "'";
|
|
274
328
|
const page = `${quote}${path}${quote}`;
|
|
329
|
+
const before = source.slice(0, lastPage.end);
|
|
330
|
+
const after = source.slice(lastPage.end);
|
|
275
331
|
const lineStart = source.lastIndexOf("\n", lastPage.start - 1) + 1;
|
|
276
332
|
const indent = source.slice(lineStart, lastPage.start);
|
|
277
|
-
if (!/^\s*$/.test(indent)) return `${
|
|
278
|
-
const trailingComma = /^\s*,/.exec(
|
|
333
|
+
if (!/^\s*$/.test(indent)) return `${before}, ${page}${after}`;
|
|
334
|
+
const trailingComma = /^\s*,/.exec(after);
|
|
279
335
|
if (trailingComma) {
|
|
280
336
|
const end = lastPage.end + trailingComma[0].length;
|
|
281
337
|
return `${source.slice(0, end)}\n${indent}${page},${source.slice(end)}`;
|
|
282
338
|
}
|
|
283
|
-
return `${
|
|
339
|
+
return `${before},\n${indent}${page}${after}`;
|
|
284
340
|
}
|
|
285
341
|
function listsPage(source, dotFile, path) {
|
|
286
342
|
try {
|
|
@@ -298,15 +354,15 @@ const PACKAGE_MANAGERS = [
|
|
|
298
354
|
"bun"
|
|
299
355
|
];
|
|
300
356
|
function isPackageManager(name) {
|
|
301
|
-
return PACKAGE_MANAGERS
|
|
357
|
+
return isOneOf(PACKAGE_MANAGERS, name);
|
|
302
358
|
}
|
|
303
359
|
function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
|
|
304
360
|
return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
|
|
305
361
|
}
|
|
306
|
-
function installedManagers(isInstalled =
|
|
362
|
+
function installedManagers(isInstalled = isOnPath) {
|
|
307
363
|
return PACKAGE_MANAGERS.filter(isInstalled);
|
|
308
364
|
}
|
|
309
|
-
function
|
|
365
|
+
function isOnPath(name) {
|
|
310
366
|
return spawnSync(name, ["--version"], {
|
|
311
367
|
stdio: "ignore",
|
|
312
368
|
shell: process.platform === "win32"
|
|
@@ -318,9 +374,9 @@ function defaultDeveloperKeyPath() {
|
|
|
318
374
|
return process.env.DOT_DEVELOPER_KEY ?? join(homedir(), ".dot", "developer-key.pem");
|
|
319
375
|
}
|
|
320
376
|
function missingDeveloperKeyHint(path = defaultDeveloperKeyPath()) {
|
|
321
|
-
return `
|
|
377
|
+
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:
|
|
322
378
|
|
|
323
|
-
mkdir -p ${dirname(path)} && openssl genpkey -algorithm ed25519 -out ${path}\n\n Then add the public key
|
|
379
|
+
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.`;
|
|
324
380
|
}
|
|
325
381
|
async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
|
|
326
382
|
let pem;
|
|
@@ -331,20 +387,22 @@ async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
|
|
|
331
387
|
throw error;
|
|
332
388
|
}
|
|
333
389
|
const privateKey = createPrivateKey(pem);
|
|
334
|
-
if (privateKey.asymmetricKeyType !== "ed25519") throw new Error(`${path} is not an Ed25519 key`);
|
|
390
|
+
if (privateKey.asymmetricKeyType !== "ed25519") throw new Error(`${path} is not an Ed25519 key. Make one with: openssl genpkey -algorithm ed25519 -out ${path}`);
|
|
335
391
|
return toDeveloperKey(privateKey);
|
|
336
392
|
}
|
|
337
393
|
function toDeveloperKey(privateKey) {
|
|
338
|
-
const
|
|
394
|
+
const publicKeyJwk = createPublicKey(privateKey).export({ format: "jwk" });
|
|
395
|
+
const rawPublicKey = Buffer.from(publicKeyJwk.x, "base64url");
|
|
339
396
|
return {
|
|
340
|
-
publicKey:
|
|
341
|
-
keyId:
|
|
397
|
+
publicKey: rawPublicKey.toString("base64"),
|
|
398
|
+
keyId: rawPublicKey.subarray(0, 8).toString("hex"),
|
|
342
399
|
sign: (bytes) => sign(null, bytes, privateKey).toString("base64")
|
|
343
400
|
};
|
|
344
401
|
}
|
|
345
402
|
//#endregion
|
|
346
403
|
//#region src/device/server.ts
|
|
347
404
|
const DEVICE_SCHEME = "dotdev";
|
|
405
|
+
const FILES_PATH = "/dev/files/";
|
|
348
406
|
const WAIT_MS = 25e3;
|
|
349
407
|
const MAX_LOG_BYTES = 1048576;
|
|
350
408
|
const LINK_LIFETIME_SECONDS = 43200;
|
|
@@ -359,133 +417,183 @@ function signedLinkMessage(server, secret, expires, keyId) {
|
|
|
359
417
|
].join("\n");
|
|
360
418
|
}
|
|
361
419
|
function deviceToken(secret) {
|
|
362
|
-
return
|
|
420
|
+
return hmacWithSecret(secret, "dotdev-token");
|
|
363
421
|
}
|
|
364
422
|
function buildMac(secret, body) {
|
|
365
|
-
return
|
|
423
|
+
return hmacWithSecret(secret, `dotdev-build\n${body}`);
|
|
366
424
|
}
|
|
367
425
|
async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {}, developerKey } = {}) {
|
|
368
|
-
if (address === void 0) throw new Error("This computer has no local network address
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
426
|
+
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");
|
|
427
|
+
const deviceServer = new LocalDeviceServer(onLog, onConnect);
|
|
428
|
+
await deviceServer.listen(port, address, developerKey);
|
|
429
|
+
return deviceServer;
|
|
430
|
+
}
|
|
431
|
+
var LocalDeviceServer = class {
|
|
432
|
+
#onLog;
|
|
433
|
+
#onConnect;
|
|
434
|
+
#secret = randomBytes(16).toString("base64url");
|
|
435
|
+
#token = deviceToken(this.#secret);
|
|
436
|
+
#server;
|
|
437
|
+
#waiting = /* @__PURE__ */ new Set();
|
|
438
|
+
#connected = /* @__PURE__ */ new Set();
|
|
439
|
+
#link = "";
|
|
440
|
+
#signature = null;
|
|
441
|
+
#files = /* @__PURE__ */ new Map();
|
|
442
|
+
#generation = 0;
|
|
443
|
+
#hotSince = 0;
|
|
444
|
+
#error = null;
|
|
445
|
+
constructor(onLog, onConnect) {
|
|
446
|
+
this.#onLog = onLog;
|
|
447
|
+
this.#onConnect = onConnect;
|
|
448
|
+
this.#server = createServer((request, response) => {
|
|
449
|
+
this.#handle(request, response);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
get link() {
|
|
453
|
+
return this.#link;
|
|
454
|
+
}
|
|
455
|
+
async listen(port, address, developerKey) {
|
|
456
|
+
await new Promise((resolve, reject) => {
|
|
457
|
+
this.#server.once("error", reject);
|
|
458
|
+
this.#server.listen(port, address, () => resolve());
|
|
459
|
+
});
|
|
460
|
+
const { port: listeningPort } = this.#server.address();
|
|
461
|
+
const origin = `http://${address}:${listeningPort}`;
|
|
462
|
+
if (developerKey) {
|
|
463
|
+
const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
|
|
464
|
+
const keyId = developerKey.keyId;
|
|
465
|
+
const message = signedLinkMessage(origin, this.#secret, expires, keyId);
|
|
466
|
+
const sig = developerKey.sign(Buffer.from(message, "utf8"));
|
|
467
|
+
this.#signature = {
|
|
468
|
+
expires,
|
|
469
|
+
keyId,
|
|
470
|
+
sig
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const server = encodeURIComponent(origin);
|
|
474
|
+
this.#link = `${DEVICE_SCHEME}://connect?server=${server}&secret=${this.#secret}`;
|
|
475
|
+
}
|
|
476
|
+
async publish(dir, { hot = false } = {}) {
|
|
477
|
+
this.#files = await readBuild(dir);
|
|
478
|
+
this.#error = null;
|
|
479
|
+
this.#announce();
|
|
480
|
+
if (!hot) this.#hotSince = this.#generation;
|
|
481
|
+
}
|
|
482
|
+
fail(message) {
|
|
483
|
+
this.#error = message;
|
|
484
|
+
this.#announce();
|
|
485
|
+
}
|
|
486
|
+
close() {
|
|
487
|
+
this.#wakeWaitingDevices();
|
|
488
|
+
return new Promise((resolve) => {
|
|
489
|
+
this.#server.close(() => resolve());
|
|
490
|
+
this.#server.closeAllConnections();
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
#announce() {
|
|
494
|
+
this.#generation += 1;
|
|
495
|
+
this.#wakeWaitingDevices();
|
|
496
|
+
}
|
|
497
|
+
#wakeWaitingDevices() {
|
|
498
|
+
for (const wake of this.#waiting) wake();
|
|
499
|
+
}
|
|
500
|
+
#handle(request, response) {
|
|
383
501
|
const from = request.socket.remoteAddress ?? "a device";
|
|
384
502
|
if (request.method === "GET" && request.url === "/dev/link") {
|
|
385
|
-
|
|
386
|
-
else response.writeHead(404).end();
|
|
503
|
+
this.#sendSignature(response);
|
|
387
504
|
return;
|
|
388
505
|
}
|
|
389
|
-
if (!isAuthorized(request, token)) {
|
|
506
|
+
if (!isAuthorized(request, this.#token)) {
|
|
390
507
|
response.writeHead(401).end();
|
|
391
508
|
return;
|
|
392
509
|
}
|
|
393
510
|
const url = new URL(request.url ?? "/", "http://device");
|
|
394
511
|
const path = url.pathname;
|
|
395
|
-
if (request.method === "GET" && path === "/dev/build")
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
const files = Object.fromEntries([...build.files].map(([name, file]) => [name, file.hash]));
|
|
401
|
-
const body = JSON.stringify({
|
|
402
|
-
generation,
|
|
403
|
-
hotSince,
|
|
404
|
-
files,
|
|
405
|
-
error
|
|
406
|
-
});
|
|
407
|
-
response.writeHead(200, {
|
|
408
|
-
"content-type": "application/json",
|
|
409
|
-
[BUILD_MAC_HEADER]: buildMac(secret, body)
|
|
410
|
-
}).end(body);
|
|
411
|
-
} else if (request.method === "GET" && path.startsWith("/dev/files/")) {
|
|
412
|
-
if (url.searchParams.get("generation") !== String(generation)) {
|
|
413
|
-
response.writeHead(409).end();
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
const name = path.slice(11).split("/").map(decodeURIComponent).join("/");
|
|
417
|
-
const file = build.files.get(name);
|
|
418
|
-
if (!file) {
|
|
419
|
-
response.writeHead(404).end();
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
|
|
423
|
-
} else if (request.method === "GET" && path === "/dev/changes") waitForChange(response, Number(url.searchParams.get("after")));
|
|
424
|
-
else if (request.method === "POST" && path === "/dev/log") readText(request).then((text) => {
|
|
425
|
-
for (const line of text.split("\n")) onLog(line, from);
|
|
426
|
-
response.writeHead(204).end();
|
|
427
|
-
}).catch(() => response.writeHead(413).end());
|
|
512
|
+
if (request.method === "GET" && path === "/dev/build") this.#sendBuild(response, from);
|
|
513
|
+
else if (request.method === "GET" && path.startsWith(FILES_PATH)) this.#sendFile(response, path, url.searchParams.get("generation"));
|
|
514
|
+
else if (request.method === "GET" && path === "/dev/changes") this.#waitForChange(response, Number(url.searchParams.get("after")));
|
|
515
|
+
else if (request.method === "POST" && path === "/dev/log") this.#receiveLog(request, response, from);
|
|
428
516
|
else response.writeHead(404).end();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
517
|
+
}
|
|
518
|
+
#sendSignature(response) {
|
|
519
|
+
if (this.#signature) sendJson(response, this.#signature);
|
|
520
|
+
else response.writeHead(404).end();
|
|
521
|
+
}
|
|
522
|
+
#sendBuild(response, from) {
|
|
523
|
+
if (!this.#connected.has(from)) {
|
|
524
|
+
this.#connected.add(from);
|
|
525
|
+
this.#onConnect(from);
|
|
526
|
+
}
|
|
527
|
+
const hashes = {};
|
|
528
|
+
for (const [name, file] of this.#files) hashes[name] = file.hash;
|
|
529
|
+
const body = JSON.stringify({
|
|
530
|
+
generation: this.#generation,
|
|
531
|
+
hotSince: this.#hotSince,
|
|
532
|
+
files: hashes,
|
|
533
|
+
error: this.#error
|
|
534
|
+
});
|
|
535
|
+
response.writeHead(200, {
|
|
536
|
+
"content-type": "application/json",
|
|
537
|
+
[BUILD_MAC_HEADER]: buildMac(this.#secret, body)
|
|
538
|
+
}).end(body);
|
|
539
|
+
}
|
|
540
|
+
#sendFile(response, path, generation) {
|
|
541
|
+
if (generation !== String(this.#generation)) {
|
|
542
|
+
response.writeHead(409).end();
|
|
433
543
|
return;
|
|
434
544
|
}
|
|
435
|
-
const
|
|
545
|
+
const fileName = buildFileName(path);
|
|
546
|
+
if (fileName === void 0) {
|
|
547
|
+
response.writeHead(400).end();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const file = this.#files.get(fileName);
|
|
551
|
+
if (!file) {
|
|
552
|
+
response.writeHead(404).end();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
|
|
556
|
+
}
|
|
557
|
+
#waitForChange(response, after) {
|
|
558
|
+
if (this.#generation > after) {
|
|
559
|
+
sendJson(response, { generation: this.#generation });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const stopWaiting = () => {
|
|
436
563
|
clearTimeout(timer);
|
|
437
|
-
waiting.delete(wake);
|
|
438
|
-
|
|
564
|
+
this.#waiting.delete(wake);
|
|
565
|
+
};
|
|
566
|
+
const wake = () => {
|
|
567
|
+
stopWaiting();
|
|
568
|
+
sendJson(response, { generation: this.#generation });
|
|
439
569
|
};
|
|
440
570
|
const timer = setTimeout(() => {
|
|
441
|
-
|
|
571
|
+
stopWaiting();
|
|
442
572
|
response.writeHead(204).end();
|
|
443
573
|
}, WAIT_MS);
|
|
444
|
-
waiting.add(wake);
|
|
445
|
-
response.on("close",
|
|
446
|
-
clearTimeout(timer);
|
|
447
|
-
waiting.delete(wake);
|
|
448
|
-
});
|
|
449
|
-
};
|
|
450
|
-
await new Promise((resolve, reject) => {
|
|
451
|
-
server.once("error", reject);
|
|
452
|
-
server.listen(port, address, () => resolve());
|
|
453
|
-
});
|
|
454
|
-
const { port: listening } = server.address();
|
|
455
|
-
const origin = `http://${address}:${listening}`;
|
|
456
|
-
if (developerKey) {
|
|
457
|
-
const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
|
|
458
|
-
const keyId = developerKey.keyId;
|
|
459
|
-
const message = signedLinkMessage(origin, secret, expires, keyId);
|
|
460
|
-
signature = {
|
|
461
|
-
expires,
|
|
462
|
-
keyId,
|
|
463
|
-
sig: developerKey.sign(Buffer.from(message, "utf8"))
|
|
464
|
-
};
|
|
574
|
+
this.#waiting.add(wake);
|
|
575
|
+
response.on("close", stopWaiting);
|
|
465
576
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
announce();
|
|
477
|
-
},
|
|
478
|
-
close() {
|
|
479
|
-
for (const wake of waiting) wake();
|
|
480
|
-
return new Promise((resolve) => {
|
|
481
|
-
server.close(() => resolve());
|
|
482
|
-
server.closeAllConnections();
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
};
|
|
577
|
+
#receiveLog(request, response, from) {
|
|
578
|
+
readLogText(request).then((text) => {
|
|
579
|
+
for (const line of text.split("\n")) this.#onLog(withoutControlCharacters(line), from);
|
|
580
|
+
response.writeHead(204).end();
|
|
581
|
+
}).catch(() => response.writeHead(413).end());
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
function hmacWithSecret(secret, text) {
|
|
585
|
+
const key = Buffer.from(secret, "base64url");
|
|
586
|
+
return createHmac("sha256", key).update(text, "utf8").digest("base64url");
|
|
486
587
|
}
|
|
487
|
-
|
|
488
|
-
|
|
588
|
+
const UNREACHABLE_INTERFACE = /^(bridge|utun|tun|tap|ipsec|ppp|awdl|llw|vmnet|vboxnet|docker|br-|veth|virbr|zt|tailscale|wg)/i;
|
|
589
|
+
function localNetworkAddress(interfaces = networkInterfaces()) {
|
|
590
|
+
let fallback;
|
|
591
|
+
for (const [name, addresses] of Object.entries(interfaces)) for (const candidate of addresses ?? []) {
|
|
592
|
+
if (candidate.family !== "IPv4" || candidate.internal) continue;
|
|
593
|
+
if (!candidate.address.startsWith("169.254.") && !UNREACHABLE_INTERFACE.test(name)) return candidate.address;
|
|
594
|
+
fallback ??= candidate.address;
|
|
595
|
+
}
|
|
596
|
+
return fallback;
|
|
489
597
|
}
|
|
490
598
|
async function readBuild(dir) {
|
|
491
599
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -498,28 +606,42 @@ async function readBuild(dir) {
|
|
|
498
606
|
const path = join(entry.parentPath, entry.name);
|
|
499
607
|
const bytes = await readFile(path);
|
|
500
608
|
const hash = createHash("sha256").update(bytes).digest("base64url");
|
|
501
|
-
|
|
609
|
+
const name = relative(dir, path).split(sep).join("/");
|
|
610
|
+
files.set(name, {
|
|
502
611
|
bytes,
|
|
503
612
|
hash
|
|
504
613
|
});
|
|
505
614
|
}
|
|
506
|
-
return
|
|
615
|
+
return files;
|
|
616
|
+
}
|
|
617
|
+
function buildFileName(path) {
|
|
618
|
+
const encodedParts = path.slice(11).split("/");
|
|
619
|
+
try {
|
|
620
|
+
return encodedParts.map((part) => decodeURIComponent(part)).join("/");
|
|
621
|
+
} catch {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
507
624
|
}
|
|
508
625
|
function isAuthorized(request, token) {
|
|
509
626
|
const given = Buffer.from(request.headers.authorization ?? "");
|
|
510
627
|
const expected = Buffer.from(`Bearer ${token}`);
|
|
511
|
-
|
|
628
|
+
if (given.length !== expected.length) return false;
|
|
629
|
+
return timingSafeEqual(given, expected);
|
|
512
630
|
}
|
|
513
631
|
function sendJson(response, body) {
|
|
514
632
|
response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
515
633
|
}
|
|
516
|
-
|
|
634
|
+
function withoutControlCharacters(line) {
|
|
635
|
+
return line.replace(/[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/g, "");
|
|
636
|
+
}
|
|
637
|
+
async function readLogText(request) {
|
|
517
638
|
const chunks = [];
|
|
518
639
|
let size = 0;
|
|
519
640
|
for await (const chunk of request) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
641
|
+
const bytes = chunk;
|
|
642
|
+
size += bytes.length;
|
|
643
|
+
if (size > MAX_LOG_BYTES) throw new Error(`A device sent more than ${MAX_LOG_BYTES} bytes of log at once`);
|
|
644
|
+
chunks.push(bytes);
|
|
523
645
|
}
|
|
524
646
|
return Buffer.concat(chunks).toString("utf8");
|
|
525
647
|
}
|
|
@@ -539,64 +661,40 @@ function startDotSimulator(project, outDir, port) {
|
|
|
539
661
|
const CHANGE_DEBOUNCE_MS = 50;
|
|
540
662
|
async function dev({ port, device, ...options }) {
|
|
541
663
|
const project = await loadProject(options.root);
|
|
542
|
-
const outDir =
|
|
664
|
+
const outDir = resolveOutDir(project.root, options.outDir);
|
|
543
665
|
const buildOptions = {
|
|
544
666
|
mode: "development",
|
|
545
667
|
...options,
|
|
546
668
|
outDir
|
|
547
669
|
};
|
|
548
|
-
const
|
|
549
|
-
const started = performance.now();
|
|
550
|
-
try {
|
|
551
|
-
printWarnings(await build(buildOptions));
|
|
552
|
-
printBuildTime(started);
|
|
553
|
-
return null;
|
|
554
|
-
} catch (error) {
|
|
555
|
-
const message = describeError(error).replaceAll(project.root + sep, "");
|
|
556
|
-
console.error(`error: ${message}`);
|
|
557
|
-
return message;
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
const firstError = await buildApp();
|
|
670
|
+
const firstError = await buildDot(buildOptions, project.root);
|
|
561
671
|
const simulator = await startDotSimulator(project, outDir, port);
|
|
562
672
|
if (firstError) simulator.error(firstError);
|
|
563
|
-
|
|
564
|
-
let
|
|
565
|
-
if (device)
|
|
566
|
-
|
|
567
|
-
devices = await startDeviceServer({
|
|
568
|
-
developerKey,
|
|
569
|
-
onConnect: (from) => console.log(`A device connected from ${from}`),
|
|
570
|
-
onLog: (line) => console.log(`device ${line}`)
|
|
571
|
-
});
|
|
572
|
-
if (firstError) devices.fail(firstError);
|
|
573
|
-
else await devices.publish(outDir);
|
|
574
|
-
printDeviceLink(devices.link);
|
|
575
|
-
if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
|
|
576
|
-
else console.log(`${missingDeveloperKeyHint()}\n`);
|
|
577
|
-
}
|
|
578
|
-
const changed = /* @__PURE__ */ new Set();
|
|
673
|
+
printSimulatorUrl(simulator.url);
|
|
674
|
+
let deviceServer;
|
|
675
|
+
if (device) deviceServer = await startDevices(outDir, firstError);
|
|
676
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
579
677
|
const pagesDir = join(project.root, PAGES_DIR) + sep;
|
|
580
678
|
const rebuilds = new RebuildQueue(async () => {
|
|
581
|
-
const
|
|
582
|
-
const error = await
|
|
679
|
+
const filesInThisBuild = [...changedFiles];
|
|
680
|
+
const error = await buildDot(buildOptions, project.root);
|
|
583
681
|
if (error) {
|
|
584
682
|
simulator.error(error);
|
|
585
|
-
|
|
683
|
+
deviceServer?.fail(error);
|
|
586
684
|
return;
|
|
587
685
|
}
|
|
588
|
-
for (const path of
|
|
589
|
-
const hot =
|
|
686
|
+
for (const path of filesInThisBuild) changedFiles.delete(path);
|
|
687
|
+
const hot = filesInThisBuild.every((path) => path.startsWith(pagesDir));
|
|
590
688
|
if (hot) simulator.hotReload();
|
|
591
689
|
else simulator.reload();
|
|
592
|
-
await
|
|
690
|
+
await deviceServer?.publish(outDir, { hot });
|
|
593
691
|
});
|
|
594
692
|
let debounceTimer;
|
|
595
693
|
const watcher = watch(project.root, { recursive: true }, (_event, file) => {
|
|
596
694
|
if (!file || isIgnored(file)) return;
|
|
597
695
|
const path = resolve(project.root, file);
|
|
598
696
|
if (path === outDir || path.startsWith(outDir + sep)) return;
|
|
599
|
-
|
|
697
|
+
changedFiles.add(path);
|
|
600
698
|
clearTimeout(debounceTimer);
|
|
601
699
|
debounceTimer = setTimeout(() => {
|
|
602
700
|
console.log(`Changed ${relative(project.root, path)}`);
|
|
@@ -606,12 +704,39 @@ async function dev({ port, device, ...options }) {
|
|
|
606
704
|
return async () => {
|
|
607
705
|
clearTimeout(debounceTimer);
|
|
608
706
|
watcher.close();
|
|
609
|
-
await Promise.all([simulator.close(),
|
|
707
|
+
await Promise.all([simulator.close(), deviceServer?.close()]);
|
|
610
708
|
};
|
|
611
709
|
}
|
|
710
|
+
async function buildDot(buildOptions, root) {
|
|
711
|
+
const startedAt = performance.now();
|
|
712
|
+
try {
|
|
713
|
+
printWarnings(await build(buildOptions));
|
|
714
|
+
printBuildTime(startedAt);
|
|
715
|
+
return null;
|
|
716
|
+
} catch (error) {
|
|
717
|
+
const message = describeError(error).replaceAll(root + sep, "");
|
|
718
|
+
console.error(`error: ${message}`);
|
|
719
|
+
return message;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function startDevices(outDir, firstError) {
|
|
723
|
+
const developerKey = await loadDeveloperKey();
|
|
724
|
+
const deviceServer = await startDeviceServer({
|
|
725
|
+
developerKey,
|
|
726
|
+
onConnect: (from) => console.log(`A device connected from ${from}`),
|
|
727
|
+
onLog: (line) => console.log(`device ${line}`)
|
|
728
|
+
});
|
|
729
|
+
if (firstError) deviceServer.fail(firstError);
|
|
730
|
+
else await deviceServer.publish(outDir);
|
|
731
|
+
printDeviceLink(deviceServer.link);
|
|
732
|
+
if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
|
|
733
|
+
else console.log(`${missingDeveloperKeyHint()}\n`);
|
|
734
|
+
return deviceServer;
|
|
735
|
+
}
|
|
612
736
|
function printDeviceLink(link) {
|
|
613
|
-
console.log("
|
|
614
|
-
|
|
737
|
+
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");
|
|
738
|
+
const qrCode = renderUnicodeCompact(link, { border: 1 });
|
|
739
|
+
console.log(qrCode.replace(/^/gm, " "));
|
|
615
740
|
console.log(`\n ${link}\n`);
|
|
616
741
|
}
|
|
617
742
|
var RebuildQueue = class {
|
|
@@ -649,16 +774,16 @@ function isIgnored(file) {
|
|
|
649
774
|
//#region src/preview.ts
|
|
650
775
|
async function preview({ port, ...options }) {
|
|
651
776
|
const project = await loadProject(options.root);
|
|
652
|
-
const outDir =
|
|
653
|
-
const
|
|
777
|
+
const outDir = resolveOutDir(project.root, options.outDir);
|
|
778
|
+
const startedAt = performance.now();
|
|
654
779
|
printWarnings(await build({
|
|
655
780
|
...options,
|
|
656
781
|
outDir,
|
|
657
782
|
mode: "production"
|
|
658
783
|
}));
|
|
659
|
-
printBuildTime(
|
|
784
|
+
printBuildTime(startedAt);
|
|
660
785
|
const simulator = await startDotSimulator(project, outDir, port);
|
|
661
|
-
|
|
786
|
+
printSimulatorUrl(simulator.url);
|
|
662
787
|
return simulator;
|
|
663
788
|
}
|
|
664
789
|
//#endregion
|
|
@@ -670,29 +795,35 @@ const TERMINAL = {
|
|
|
670
795
|
const ERASE_BELOW = "\x1B[0J";
|
|
671
796
|
const HIDE_CURSOR = "\x1B[?25l";
|
|
672
797
|
const SHOW_CURSOR = "\x1B[?25h";
|
|
673
|
-
const cursorUp = (lines) => `\x1b[${lines}A`;
|
|
674
798
|
async function select(question, choices, fallback, { input, output } = TERMINAL) {
|
|
675
799
|
if (!input.isTTY) return fallback;
|
|
676
|
-
let
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
800
|
+
let selectedIndex = choices.findIndex((choice) => choice.value === fallback);
|
|
801
|
+
if (selectedIndex < 0) selectedIndex = 0;
|
|
802
|
+
let valueWidth = 0;
|
|
803
|
+
if (choices.some((choice) => choice.hint)) valueWidth = Math.max(...choices.map((choice) => choice.value.length));
|
|
804
|
+
const formatChoice = (choice, index) => {
|
|
805
|
+
const value = choice.value.padEnd(valueWidth);
|
|
680
806
|
const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
|
|
681
807
|
const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
|
|
682
|
-
if (index ===
|
|
683
|
-
return ` ${
|
|
808
|
+
if (index === selectedIndex) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], value)}${hint}${defaultMark}`;
|
|
809
|
+
return ` ${value}${hint}${defaultMark}`;
|
|
684
810
|
};
|
|
685
811
|
let drawnLines = 0;
|
|
686
812
|
const draw = (lines) => {
|
|
687
|
-
|
|
813
|
+
let moveUp = "";
|
|
814
|
+
if (drawnLines > 0) moveUp = cursorUp(drawnLines);
|
|
815
|
+
output.write(`${moveUp}${ERASE_BELOW}${lines.join("\n")}\n`);
|
|
688
816
|
drawnLines = lines.length;
|
|
689
817
|
};
|
|
690
|
-
const
|
|
818
|
+
const drawQuestionAndChoices = () => {
|
|
819
|
+
const keysHint = styleText("dim", "↑/↓ to move, Enter to choose");
|
|
820
|
+
draw([`${styleText("bold", question)} ${keysHint}`, ...choices.map(formatChoice)]);
|
|
821
|
+
};
|
|
691
822
|
emitKeypressEvents(input);
|
|
692
823
|
input.setRawMode(true);
|
|
693
824
|
input.resume();
|
|
694
825
|
output.write(HIDE_CURSOR);
|
|
695
|
-
|
|
826
|
+
drawQuestionAndChoices();
|
|
696
827
|
return new Promise((resolve) => {
|
|
697
828
|
const onKeypress = (_text, key) => {
|
|
698
829
|
if (key.ctrl && key.name === "c") {
|
|
@@ -700,13 +831,13 @@ async function select(question, choices, fallback, { input, output } = TERMINAL)
|
|
|
700
831
|
process.exit(130);
|
|
701
832
|
}
|
|
702
833
|
if (key.name === "up" || key.name === "k") {
|
|
703
|
-
|
|
704
|
-
|
|
834
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
835
|
+
drawQuestionAndChoices();
|
|
705
836
|
} else if (key.name === "down" || key.name === "j") {
|
|
706
|
-
|
|
707
|
-
|
|
837
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
838
|
+
drawQuestionAndChoices();
|
|
708
839
|
} else if (key.name === "return" || key.name === "enter") {
|
|
709
|
-
const { value } = choices[
|
|
840
|
+
const { value } = choices[selectedIndex];
|
|
710
841
|
input.off("keypress", onKeypress);
|
|
711
842
|
input.setRawMode(false);
|
|
712
843
|
input.pause();
|
|
@@ -722,15 +853,18 @@ async function confirm(question, fallback, terminal = TERMINAL) {
|
|
|
722
853
|
if (!terminal.input.isTTY) return false;
|
|
723
854
|
return await select(question, [{ value: "Yes" }, { value: "No" }], fallback ? "Yes" : "No", terminal) === "Yes";
|
|
724
855
|
}
|
|
725
|
-
async function ask(question, fallback, { input, output } = TERMINAL) {
|
|
856
|
+
async function ask(question, fallback, hint, { input, output } = TERMINAL) {
|
|
726
857
|
if (!input.isTTY) return fallback;
|
|
727
|
-
const
|
|
858
|
+
const hintText = hint ? styleText("dim", `, ${hint}`) : "";
|
|
859
|
+
const defaultMark = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
|
|
728
860
|
const readline = createInterface({
|
|
729
861
|
input,
|
|
730
862
|
output
|
|
731
863
|
});
|
|
732
864
|
try {
|
|
733
|
-
|
|
865
|
+
const trimmedAnswer = (await readline.question(`${styleText("bold", question)}${hintText}${defaultMark}: `)).trim();
|
|
866
|
+
if (trimmedAnswer === "") return fallback;
|
|
867
|
+
return trimmedAnswer;
|
|
734
868
|
} catch (error) {
|
|
735
869
|
if (error.name === "AbortError") return;
|
|
736
870
|
throw error;
|
|
@@ -738,33 +872,39 @@ async function ask(question, fallback, { input, output } = TERMINAL) {
|
|
|
738
872
|
readline.close();
|
|
739
873
|
}
|
|
740
874
|
}
|
|
875
|
+
function cursorUp(lines) {
|
|
876
|
+
return `\x1b[${lines}A`;
|
|
877
|
+
}
|
|
741
878
|
//#endregion
|
|
742
879
|
//#region src/cli.ts
|
|
743
880
|
const USAGE = `Usage:
|
|
744
|
-
dotctl create [dir] [--name <name>] [--
|
|
745
|
-
[--pm <npm|pnpm|yarn|bun>]
|
|
746
|
-
Create a dot
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
881
|
+
dotctl create [dir] [--name <name>] [--package <com.mysalone.thing>]
|
|
882
|
+
[--template <starter|showcase>] [--pm <npm|pnpm|yarn|bun>]
|
|
883
|
+
Create a dot. Asks for its name, its package name
|
|
884
|
+
(a reverse domain name you own) and its template:
|
|
885
|
+
starter is one page to build on, showcase has a
|
|
886
|
+
page for each feature
|
|
887
|
+
dotctl create page <path> Add a page to the dot you are in. Writes
|
|
888
|
+
pages/<path>/ with view.dml, state.ts and controller.ts
|
|
751
889
|
dotctl build [root] [options] Build the minified bundles the host app runs
|
|
752
|
-
dotctl dev [root] [options] Run in the browser simulator
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
890
|
+
dotctl dev [root] [options] Run the dot in the browser simulator and rebuild on
|
|
891
|
+
every change
|
|
892
|
+
dotctl preview [root] [options] Build as \`build\` does, then run that build in the
|
|
893
|
+
browser simulator, exactly as the host app gets it
|
|
894
|
+
dotctl keys show Show this computer's developer key: its public key
|
|
895
|
+
and key ID. The key signs the links that
|
|
896
|
+
\`dev --device\` shows, so Dash will open them
|
|
897
|
+
dotctl lsp [--stdio] Start the .dml language server for editors. It talks
|
|
898
|
+
over stdin and stdout
|
|
760
899
|
|
|
761
900
|
Options:
|
|
762
901
|
--out-dir <dir> Output directory (default: <root>/dist)
|
|
763
|
-
--conditions <list> Extra export conditions,
|
|
764
|
-
--port <port> Simulator port for \`dev\` and \`preview
|
|
765
|
-
|
|
766
|
-
--device With \`dev\`, also serve the dot to the Dot example apps on phones
|
|
767
|
-
|
|
902
|
+
--conditions <list> Extra export conditions, separated by commas
|
|
903
|
+
--port <port> Simulator port for \`dev\` and \`preview\`. If it is taken, the
|
|
904
|
+
next free port is used (default: ${DEFAULT_PORT})
|
|
905
|
+
--device With \`dev\`, also serve the dot to the Dot example apps on phones
|
|
906
|
+
on your local network, through a link and QR code. The apps
|
|
907
|
+
reload after each build
|
|
768
908
|
-h, --help Show this message`;
|
|
769
909
|
async function main(argv) {
|
|
770
910
|
const { values, positionals } = parseArgs({
|
|
@@ -776,6 +916,7 @@ async function main(argv) {
|
|
|
776
916
|
port: { type: "string" },
|
|
777
917
|
device: { type: "boolean" },
|
|
778
918
|
name: { type: "string" },
|
|
919
|
+
package: { type: "string" },
|
|
779
920
|
template: { type: "string" },
|
|
780
921
|
pm: { type: "string" },
|
|
781
922
|
help: {
|
|
@@ -786,59 +927,87 @@ async function main(argv) {
|
|
|
786
927
|
}
|
|
787
928
|
});
|
|
788
929
|
const [command, target, pagePath] = positionals;
|
|
789
|
-
if (
|
|
790
|
-
const { serveStdio } = await import("./server-BfPAnsSf.js");
|
|
791
|
-
serveStdio();
|
|
792
|
-
return new Promise(() => {});
|
|
793
|
-
}
|
|
794
|
-
if (command === "keys" && !values.help) return keys(target);
|
|
795
|
-
if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
|
|
796
|
-
if (command === "create" && !values.help) return createApp(target, values.name, values.template, values.pm);
|
|
797
|
-
if (values.help || command !== "build" && command !== "dev" && command !== "preview") {
|
|
930
|
+
if (values.help) {
|
|
798
931
|
console.log(USAGE);
|
|
799
|
-
return
|
|
932
|
+
return 0;
|
|
800
933
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
934
|
+
switch (command) {
|
|
935
|
+
case "lsp": return serveLanguageServer();
|
|
936
|
+
case "keys": return showKeys(target);
|
|
937
|
+
case "create":
|
|
938
|
+
if (target === "page") return addPage(pagePath);
|
|
939
|
+
return createDot(target, {
|
|
940
|
+
name: values.name,
|
|
941
|
+
dotPackage: values.package,
|
|
942
|
+
template: values.template,
|
|
943
|
+
packageManager: values.pm
|
|
944
|
+
});
|
|
945
|
+
case "build":
|
|
946
|
+
case "dev":
|
|
947
|
+
case "preview": break;
|
|
948
|
+
default:
|
|
949
|
+
console.log(USAGE);
|
|
950
|
+
return 1;
|
|
951
|
+
}
|
|
952
|
+
let port;
|
|
953
|
+
if (values.port !== void 0) {
|
|
954
|
+
port = Number(values.port);
|
|
955
|
+
if (!Number.isInteger(port)) {
|
|
956
|
+
console.error(`--port must be a whole number, not "${values.port}"`);
|
|
957
|
+
return 1;
|
|
958
|
+
}
|
|
805
959
|
}
|
|
806
960
|
const options = { root: target ?? "." };
|
|
807
961
|
if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
|
|
808
962
|
if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
|
|
809
|
-
if (command === "dev")
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
963
|
+
if (command === "dev") return runDev(options, port, values.device);
|
|
964
|
+
if (command === "preview") return runPreview(options, port, values.device);
|
|
965
|
+
return runBuild(options);
|
|
966
|
+
}
|
|
967
|
+
async function serveLanguageServer() {
|
|
968
|
+
const { serveStdio } = await import("./server-DMnWDdJ6.js");
|
|
969
|
+
serveStdio();
|
|
970
|
+
return new Promise(() => {});
|
|
971
|
+
}
|
|
972
|
+
async function runDev(options, port, device) {
|
|
973
|
+
const stop = await dev({
|
|
974
|
+
...options,
|
|
975
|
+
port,
|
|
976
|
+
device
|
|
977
|
+
});
|
|
978
|
+
await waitForCtrlC();
|
|
979
|
+
await stop();
|
|
980
|
+
return 0;
|
|
981
|
+
}
|
|
982
|
+
async function runPreview(options, port, device) {
|
|
983
|
+
if (device) {
|
|
984
|
+
console.error("--device only works with dotctl dev");
|
|
985
|
+
return 1;
|
|
831
986
|
}
|
|
832
|
-
const
|
|
987
|
+
const simulator = await preview({
|
|
988
|
+
...options,
|
|
989
|
+
port
|
|
990
|
+
});
|
|
991
|
+
await waitForCtrlC();
|
|
992
|
+
await simulator.close();
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
async function runBuild(options) {
|
|
996
|
+
const startedAt = performance.now();
|
|
833
997
|
const result = await build(options);
|
|
834
|
-
|
|
998
|
+
printFiles(result.files.filter((file) => !file.endsWith(".map")));
|
|
835
999
|
printWarnings(result);
|
|
836
|
-
printBuildTime(
|
|
1000
|
+
printBuildTime(startedAt);
|
|
837
1001
|
return 0;
|
|
838
1002
|
}
|
|
839
|
-
|
|
1003
|
+
function waitForCtrlC() {
|
|
1004
|
+
return new Promise((resolve) => {
|
|
1005
|
+
process.once("SIGINT", () => resolve());
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
async function showKeys(action) {
|
|
840
1009
|
if (action !== "show") {
|
|
841
|
-
console.error("dotctl keys show");
|
|
1010
|
+
console.error("Usage: dotctl keys show");
|
|
842
1011
|
return 1;
|
|
843
1012
|
}
|
|
844
1013
|
const path = defaultDeveloperKeyPath();
|
|
@@ -850,63 +1019,91 @@ async function keys(action) {
|
|
|
850
1019
|
console.log(`\n Public key: ${key.publicKey}\n Key ID: ${key.keyId}\n`);
|
|
851
1020
|
return 0;
|
|
852
1021
|
}
|
|
853
|
-
async function addPage(
|
|
854
|
-
if (
|
|
855
|
-
console.error("
|
|
1022
|
+
async function addPage(typedPath) {
|
|
1023
|
+
if (typedPath === void 0) {
|
|
1024
|
+
console.error("Give the page a path, such as: dotctl create page cart/checkout");
|
|
856
1025
|
return 1;
|
|
857
1026
|
}
|
|
858
|
-
|
|
859
|
-
if (path.endsWith("/")) path = path.substring(0, path.length - 1);
|
|
1027
|
+
const path = trimSlashes(typedPath);
|
|
860
1028
|
const { files, listing } = await createPage(path, process.cwd());
|
|
861
|
-
|
|
1029
|
+
printFiles(files);
|
|
862
1030
|
if (listing === "added") console.log(`\nListed '${path}' in dot.ts.`);
|
|
863
|
-
else if (listing === "manual") console.log(`\
|
|
1031
|
+
else if (listing === "manual") console.log(`\nAdd it to the pages in dot.ts so it gets built: defineDot({ pages: [..., '${path}'] })`);
|
|
864
1032
|
return 0;
|
|
865
1033
|
}
|
|
866
|
-
|
|
1034
|
+
function trimSlashes(path) {
|
|
1035
|
+
let trimmed = path;
|
|
1036
|
+
if (trimmed.startsWith("/")) trimmed = trimmed.substring(1);
|
|
1037
|
+
if (trimmed.endsWith("/")) trimmed = trimmed.substring(0, trimmed.length - 1);
|
|
1038
|
+
return trimmed;
|
|
1039
|
+
}
|
|
1040
|
+
async function createDot(dir, flags) {
|
|
1041
|
+
const templateFlag = flags.template;
|
|
867
1042
|
if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
|
|
868
1043
|
console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
|
|
869
1044
|
return 1;
|
|
870
1045
|
}
|
|
1046
|
+
const packageManagerFlag = flags.packageManager;
|
|
871
1047
|
if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
|
|
872
1048
|
console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
|
|
873
1049
|
return 1;
|
|
874
1050
|
}
|
|
875
|
-
name
|
|
1051
|
+
let name = flags.name;
|
|
1052
|
+
if (name === void 0) name = await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
|
|
876
1053
|
if (!name) {
|
|
877
|
-
console.error("
|
|
1054
|
+
console.error("The dot needs a name. Pass one with: dotctl create [dir] --name \"My Dot\"");
|
|
878
1055
|
return 1;
|
|
879
1056
|
}
|
|
880
|
-
const
|
|
1057
|
+
const dotPackageFlag = flags.dotPackage;
|
|
1058
|
+
if (dotPackageFlag !== void 0 && !isDotPackage(dotPackageFlag)) {
|
|
1059
|
+
console.error(`--package "${dotPackageFlag}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
1060
|
+
return 1;
|
|
1061
|
+
}
|
|
1062
|
+
let dotPackage = dotPackageFlag;
|
|
1063
|
+
if (dotPackage === void 0) dotPackage = await askDotPackage(suggestedDotPackage(toPackageName(name)));
|
|
1064
|
+
if (!dotPackage) return 1;
|
|
1065
|
+
let template = templateFlag;
|
|
1066
|
+
if (template === void 0) template = await chooseTemplate();
|
|
881
1067
|
const { root, workspace } = await create({
|
|
882
1068
|
name,
|
|
883
1069
|
dir: dir ?? toPackageName(name),
|
|
1070
|
+
dotPackage,
|
|
884
1071
|
template
|
|
885
1072
|
});
|
|
886
|
-
|
|
1073
|
+
let path = relative(process.cwd(), root);
|
|
1074
|
+
if (path === "") path = ".";
|
|
887
1075
|
console.log(`\nCreated ${name} in ${path}`);
|
|
888
1076
|
if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
|
|
889
|
-
let packageManager
|
|
1077
|
+
let packageManager;
|
|
1078
|
+
if (workspace) packageManager = "pnpm";
|
|
1079
|
+
else packageManager = packageManagerFlag ?? launchedBy();
|
|
890
1080
|
console.log();
|
|
891
1081
|
if (await confirm("Install and start it now?", true)) {
|
|
892
1082
|
if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
|
|
893
|
-
if (await
|
|
894
|
-
console.error(`\n${packageManager} install failed.`);
|
|
1083
|
+
if (await runCommand(packageManager, ["install"], root) === 0) return runCommand(packageManager, ["run", "dev"], root);
|
|
1084
|
+
console.error(`\n${packageManager} install failed. Fix the problem above, then run the steps below.`);
|
|
895
1085
|
}
|
|
896
1086
|
console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
|
|
897
1087
|
return 0;
|
|
898
1088
|
}
|
|
899
|
-
function
|
|
900
|
-
return new Promise((
|
|
1089
|
+
function runCommand(command, args, cwd) {
|
|
1090
|
+
return new Promise((resolve, reject) => {
|
|
901
1091
|
const child = spawn(command, args, {
|
|
902
1092
|
cwd,
|
|
903
1093
|
stdio: "inherit",
|
|
904
1094
|
shell: process.platform === "win32"
|
|
905
1095
|
});
|
|
906
1096
|
child.on("error", reject);
|
|
907
|
-
child.on("exit", (
|
|
1097
|
+
child.on("exit", (exitCode) => resolve(exitCode ?? 1));
|
|
908
1098
|
});
|
|
909
1099
|
}
|
|
1100
|
+
async function askDotPackage(suggested) {
|
|
1101
|
+
while (true) {
|
|
1102
|
+
const answer = await ask("Package name", suggested, "a reverse domain name you own");
|
|
1103
|
+
if (answer === void 0 || isDotPackage(answer)) return answer;
|
|
1104
|
+
console.error(`"${answer}" isn't a package name. ${DOT_PACKAGE_RULE}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
910
1107
|
function chooseTemplate() {
|
|
911
1108
|
console.log();
|
|
912
1109
|
return select("Template?", TEMPLATES.map((template) => ({
|
|
@@ -917,10 +1114,11 @@ function chooseTemplate() {
|
|
|
917
1114
|
async function choosePackageManager(suggested) {
|
|
918
1115
|
const installed = installedManagers();
|
|
919
1116
|
if (installed.length < 2) return installed[0] ?? suggested;
|
|
920
|
-
|
|
1117
|
+
let fallback = installed[0];
|
|
1118
|
+
if (installed.includes(suggested)) fallback = suggested;
|
|
921
1119
|
return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
|
|
922
1120
|
}
|
|
923
|
-
main(process.argv.slice(2)).then((
|
|
1121
|
+
main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode), (error) => {
|
|
924
1122
|
console.error(`error: ${describeError(error)}`);
|
|
925
1123
|
process.exit(1);
|
|
926
1124
|
});
|