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