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