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