@monimesl/dot-cli 0.1.0-alpha.2
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 +26 -0
- package/bin/dotctl.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +424 -0
- package/dist/cli.js.map +1 -0
- package/dist/template/.env +3 -0
- package/dist/template/dot.ts +6 -0
- package/dist/template/pages/help/tips/controller.ts +30 -0
- package/dist/template/pages/help/tips/state.ts +11 -0
- package/dist/template/pages/help/tips/view.dml +15 -0
- package/dist/template/pages/index/controller.ts +67 -0
- package/dist/template/pages/index/state.ts +17 -0
- package/dist/template/pages/index/view.dml +66 -0
- package/dist/template/statics/logo.png +0 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @monimesl/dot-cli
|
|
2
|
+
|
|
3
|
+
The `dotctl` command: build mini apps and run them in the simulator. Platform tasks such as
|
|
4
|
+
packaging, signing and publishing mini apps will live here too.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
dotctl create [dir] # scaffold a dot (asks for its name), with a sample home page
|
|
8
|
+
dotctl build [root] # compile the dot into dist/ui and dist/logic, minified
|
|
9
|
+
dotctl dev [root] # build, open it in the browser simulator, rebuild and reload on change
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Option | Applies to | What it does |
|
|
13
|
+
| --------------------- | ---------- | ------------------------------------------------------- |
|
|
14
|
+
| `--out-dir <dir>` | both | Output directory (default: `<root>/dist`) |
|
|
15
|
+
| `--conditions <list>` | both | Extra export conditions, such as `@monimesl/dot-source` |
|
|
16
|
+
| `--port <port>` | `dev` | Simulator port (default: `3680`, or the next free one) |
|
|
17
|
+
|
|
18
|
+
## `dotctl create`
|
|
19
|
+
|
|
20
|
+
Asks for the dot's name (`Dot` by default, or pass `--name`) and creates a self-contained project
|
|
21
|
+
in `dir` (by default, the name in kebab case) with one page, `home`: a short welcome, localized for
|
|
22
|
+
Sierra Leone, to a dot running inside Dash. The dot depends on `@monimesl/dot-runtime` and `@monimesl/dot-cli`; inside
|
|
23
|
+
the Dot repository it uses the workspace packages instead. It then offers to install the
|
|
24
|
+
dependencies and start the simulator inside the new dot, asking which of the installed package
|
|
25
|
+
managers to use (npm, pnpm, yarn or bun; the one that launched `dotctl`, or npm, by default).
|
|
26
|
+
`--pm <name>` answers that up front.
|
package/bin/dotctl.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { CompileError, build, installedRuntime, loadProject } from "@monimesl/dot-compiler";
|
|
7
|
+
import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
9
|
+
import { cp, mkdir, writeFile } from "node:fs/promises";
|
|
10
|
+
import { DmlParseError } from "@monimesl/dot-dml-parser";
|
|
11
|
+
import { ScriptParseError } from "@monimesl/dot-script-parser";
|
|
12
|
+
//#region src/create/template.ts
|
|
13
|
+
const TEMPLATE_FILES = [
|
|
14
|
+
"dot.ts",
|
|
15
|
+
"pages",
|
|
16
|
+
"statics",
|
|
17
|
+
".env"
|
|
18
|
+
];
|
|
19
|
+
const DOT_JSON_SCHEMA = "./node_modules/@monimesl/dot-runtime/dot.schema.json";
|
|
20
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
21
|
+
function projectFiles(options) {
|
|
22
|
+
return {
|
|
23
|
+
"package.json": packageJson(options),
|
|
24
|
+
"tsconfig.json": tsconfig(options),
|
|
25
|
+
".gitignore": "node_modules/\ndist/\n# Types generated by dotctl\n.dot/\n",
|
|
26
|
+
"dot.json": json({
|
|
27
|
+
$schema: DOT_JSON_SCHEMA,
|
|
28
|
+
id: `com.example.${idWord(options.packageName)}`,
|
|
29
|
+
name: options.name,
|
|
30
|
+
version: "0.1.0"
|
|
31
|
+
}),
|
|
32
|
+
"README.md": readme(options.name)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function packageJson({ packageName, dotVersion, workspace }) {
|
|
36
|
+
const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
|
|
37
|
+
return json({
|
|
38
|
+
name: packageName,
|
|
39
|
+
private: true,
|
|
40
|
+
type: "module",
|
|
41
|
+
scripts: {
|
|
42
|
+
dev: `dotctl dev${conditions}`,
|
|
43
|
+
build: `dotctl build${conditions}`,
|
|
44
|
+
typecheck: "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
dependencies: { "@monimesl/dot-runtime": dotVersion },
|
|
47
|
+
devDependencies: {
|
|
48
|
+
"@monimesl/dot-cli": dotVersion,
|
|
49
|
+
typescript: "^5.9.3"
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function tsconfig({ workspace }) {
|
|
54
|
+
const compilerOptions = {
|
|
55
|
+
target: "ES2022",
|
|
56
|
+
module: "ESNext",
|
|
57
|
+
moduleResolution: "bundler",
|
|
58
|
+
lib: ["ES2022"],
|
|
59
|
+
types: ["@monimesl/dot-runtime/globals"],
|
|
60
|
+
strict: true,
|
|
61
|
+
noEmit: true,
|
|
62
|
+
skipLibCheck: true,
|
|
63
|
+
verbatimModuleSyntax: true
|
|
64
|
+
};
|
|
65
|
+
if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
|
|
66
|
+
return json({
|
|
67
|
+
compilerOptions,
|
|
68
|
+
include: [
|
|
69
|
+
"dot.ts",
|
|
70
|
+
"pages",
|
|
71
|
+
".dot/pages.d.ts",
|
|
72
|
+
".dot/env.d.ts"
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function readme(name) {
|
|
77
|
+
return `# ${name}
|
|
78
|
+
|
|
79
|
+
A dot: a mini app that runs inside Dash.
|
|
80
|
+
|
|
81
|
+
\`\`\`sh
|
|
82
|
+
npm install
|
|
83
|
+
npm run dev # run it in the simulator; it reloads on every change
|
|
84
|
+
npm run build # build it for Dash
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
Each page is a folder under \`pages/\` with three files: \`view.dml\` (what it shows),
|
|
88
|
+
\`state.ts\` (what it starts with) and \`controller.ts\` (how it changes). List new pages in
|
|
89
|
+
\`dot.ts\`; the first one is where the dot starts.
|
|
90
|
+
|
|
91
|
+
\`dot.json\` describes the dot to Dash. Change its \`id\` from \`com.example.…\` to a reverse domain
|
|
92
|
+
name you own before you publish: it can't change afterwards. Your editor completes the other fields
|
|
93
|
+
from the schema it points to.
|
|
94
|
+
|
|
95
|
+
\`.env\` holds public configuration: variables named \`DOT_PUBLIC_…\`, which the service reads as
|
|
96
|
+
\`env.DOT_PUBLIC_…\`. They are built into the dot, so anyone who has it can read them: never put a
|
|
97
|
+
secret there. \`.env.development\` and \`.env.production\` override it for \`dev\` and \`build\`.
|
|
98
|
+
|
|
99
|
+
It starts as a counter with two pages. \`index\` counts taps with mutations. \`help/tips\` shows
|
|
100
|
+
navigation: each page you open goes on top of the stack, Back returns to the one below, and the
|
|
101
|
+
trail at the top shows where you are.
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
function idWord(packageName) {
|
|
105
|
+
return packageName.slice(0, 63).replace(/-+$/, "");
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/create/create.ts
|
|
109
|
+
async function create({ name, dir }) {
|
|
110
|
+
const root = resolve(dir);
|
|
111
|
+
if (existsSync(root) && readdirSync(root).length > 0) throw new Error(`${dir} already exists and is not empty`);
|
|
112
|
+
const packageName = toPackageName(name);
|
|
113
|
+
if (!packageName) throw new Error(`"${name}" needs at least one letter or digit`);
|
|
114
|
+
const workspace = isInDotWorkspace(root);
|
|
115
|
+
const template = templateDir();
|
|
116
|
+
await mkdir(root, { recursive: true });
|
|
117
|
+
for (const path of TEMPLATE_FILES) await cp(join(template, path), join(root, path), { recursive: true });
|
|
118
|
+
const generated = projectFiles({
|
|
119
|
+
name,
|
|
120
|
+
packageName,
|
|
121
|
+
dotVersion: workspace ? "workspace:*" : `^${cliVersion()}`,
|
|
122
|
+
workspace
|
|
123
|
+
});
|
|
124
|
+
for (const [path, content] of Object.entries(generated)) await writeFile(join(root, path), content);
|
|
125
|
+
return {
|
|
126
|
+
root,
|
|
127
|
+
workspace
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function toPackageName(name) {
|
|
131
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
132
|
+
}
|
|
133
|
+
function toDisplayName(dir) {
|
|
134
|
+
return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
135
|
+
}
|
|
136
|
+
function isInDotWorkspace(dir) {
|
|
137
|
+
let current = dirname(dir);
|
|
138
|
+
while (true) {
|
|
139
|
+
if (existsSync(join(current, "pnpm-workspace.yaml"))) {
|
|
140
|
+
const runtimeManifest = join(current, "packages/runtime/package.json");
|
|
141
|
+
if (!existsSync(runtimeManifest)) return false;
|
|
142
|
+
return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
|
|
143
|
+
}
|
|
144
|
+
const parent = dirname(current);
|
|
145
|
+
if (parent === current) return false;
|
|
146
|
+
current = parent;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function templateDir() {
|
|
150
|
+
const example = join(cliRoot(), "../../examples/counter");
|
|
151
|
+
return existsSync(example) ? example : join(cliRoot(), "dist/template");
|
|
152
|
+
}
|
|
153
|
+
function cliVersion() {
|
|
154
|
+
return readPackageJson(join(cliRoot(), "package.json")).version;
|
|
155
|
+
}
|
|
156
|
+
function readPackageJson(path) {
|
|
157
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
158
|
+
}
|
|
159
|
+
function cliRoot() {
|
|
160
|
+
return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/create/package-manager.ts
|
|
164
|
+
const PACKAGE_MANAGERS = [
|
|
165
|
+
"npm",
|
|
166
|
+
"pnpm",
|
|
167
|
+
"yarn",
|
|
168
|
+
"bun"
|
|
169
|
+
];
|
|
170
|
+
function isPackageManager(name) {
|
|
171
|
+
return PACKAGE_MANAGERS.includes(name);
|
|
172
|
+
}
|
|
173
|
+
function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
|
|
174
|
+
return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
|
|
175
|
+
}
|
|
176
|
+
function installedManagers(isInstalled = onPath) {
|
|
177
|
+
return PACKAGE_MANAGERS.filter(isInstalled);
|
|
178
|
+
}
|
|
179
|
+
function onPath(name) {
|
|
180
|
+
return spawnSync(name, ["--version"], {
|
|
181
|
+
stdio: "ignore",
|
|
182
|
+
shell: process.platform === "win32"
|
|
183
|
+
}).status === 0;
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/report.ts
|
|
187
|
+
function describeError(error) {
|
|
188
|
+
if (error instanceof CompileError || error instanceof DmlParseError || error instanceof ScriptParseError) return error.message;
|
|
189
|
+
if (error.code?.startsWith("ERR_PARSE_ARGS")) {
|
|
190
|
+
const [firstSentence] = error.message.split(". ");
|
|
191
|
+
return `${firstSentence}. See dotctl --help.`;
|
|
192
|
+
}
|
|
193
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
194
|
+
}
|
|
195
|
+
function printWarnings({ warnings }) {
|
|
196
|
+
for (const warning of warnings) console.warn(`warning: ${warning.message}`);
|
|
197
|
+
}
|
|
198
|
+
function printBuildTime(startedAt) {
|
|
199
|
+
console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/dev.ts
|
|
203
|
+
const CHANGE_DEBOUNCE_MS = 50;
|
|
204
|
+
async function dev({ port, ...options }) {
|
|
205
|
+
const project = await loadProject(options.root);
|
|
206
|
+
const outDir = resolve(project.root, options.outDir ?? "dist");
|
|
207
|
+
const buildOptions = {
|
|
208
|
+
mode: "development",
|
|
209
|
+
...options,
|
|
210
|
+
outDir
|
|
211
|
+
};
|
|
212
|
+
const buildApp = async () => {
|
|
213
|
+
const started = performance.now();
|
|
214
|
+
try {
|
|
215
|
+
printWarnings(await build(buildOptions));
|
|
216
|
+
printBuildTime(started);
|
|
217
|
+
return null;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message = describeError(error).replaceAll(project.root + sep, "");
|
|
220
|
+
console.error(`error: ${message}`);
|
|
221
|
+
return message;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const firstError = await buildApp();
|
|
225
|
+
const simulator = await startSimulator({
|
|
226
|
+
dotDir: outDir,
|
|
227
|
+
reactHostDir: installedRuntime(project.root).react.dir,
|
|
228
|
+
name: project.dotJson.name,
|
|
229
|
+
port
|
|
230
|
+
});
|
|
231
|
+
if (firstError) simulator.error(firstError);
|
|
232
|
+
console.log(`\n Simulator running at: ${simulator.url}\n`);
|
|
233
|
+
const rebuilds = new RebuildQueue(async () => {
|
|
234
|
+
const error = await buildApp();
|
|
235
|
+
if (error) simulator.error(error);
|
|
236
|
+
else simulator.reload();
|
|
237
|
+
});
|
|
238
|
+
let debounceTimer;
|
|
239
|
+
const watcher = watch(project.root, { recursive: true }, (_event, file) => {
|
|
240
|
+
if (!file || isIgnored(file)) return;
|
|
241
|
+
const path = resolve(project.root, file);
|
|
242
|
+
if (path === outDir || path.startsWith(outDir + sep)) return;
|
|
243
|
+
clearTimeout(debounceTimer);
|
|
244
|
+
debounceTimer = setTimeout(() => {
|
|
245
|
+
console.log(`Changed ${relative(project.root, path)}`);
|
|
246
|
+
rebuilds.request();
|
|
247
|
+
}, CHANGE_DEBOUNCE_MS);
|
|
248
|
+
});
|
|
249
|
+
return async () => {
|
|
250
|
+
clearTimeout(debounceTimer);
|
|
251
|
+
watcher.close();
|
|
252
|
+
await simulator.close();
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
var RebuildQueue = class {
|
|
256
|
+
#rebuild;
|
|
257
|
+
#building = false;
|
|
258
|
+
#changedWhileBuilding = false;
|
|
259
|
+
constructor(rebuild) {
|
|
260
|
+
this.#rebuild = rebuild;
|
|
261
|
+
}
|
|
262
|
+
request() {
|
|
263
|
+
if (this.#building) {
|
|
264
|
+
this.#changedWhileBuilding = true;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.#build();
|
|
268
|
+
}
|
|
269
|
+
async #build() {
|
|
270
|
+
this.#building = true;
|
|
271
|
+
this.#changedWhileBuilding = false;
|
|
272
|
+
try {
|
|
273
|
+
await this.#rebuild();
|
|
274
|
+
} finally {
|
|
275
|
+
this.#building = false;
|
|
276
|
+
}
|
|
277
|
+
if (this.#changedWhileBuilding) this.request();
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
function isIgnored(file) {
|
|
281
|
+
return file.split(/[\\/]/).some((segment) => {
|
|
282
|
+
if (segment === ".env" || segment.startsWith(".env.")) return false;
|
|
283
|
+
return segment === "node_modules" || segment.startsWith(".");
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/cli.ts
|
|
288
|
+
const USAGE = `Usage:
|
|
289
|
+
dotctl create [dir] [--name <name>] [--pm <npm|pnpm|yarn|bun>]
|
|
290
|
+
Create a dot (asks for its name)
|
|
291
|
+
dotctl build [root] [options] Build the minified bundles the host app runs
|
|
292
|
+
dotctl dev [root] [options] Run in the browser simulator, rebuilding on change
|
|
293
|
+
|
|
294
|
+
Options:
|
|
295
|
+
--out-dir <dir> Output directory (default: <root>/dist)
|
|
296
|
+
--conditions <list> Extra export conditions, comma separated
|
|
297
|
+
--port <port> Simulator port for \`dev\`; the next free one is used if taken
|
|
298
|
+
(default: ${DEFAULT_PORT})
|
|
299
|
+
-h, --help Show this message`;
|
|
300
|
+
async function main(argv) {
|
|
301
|
+
const { values, positionals } = parseArgs({
|
|
302
|
+
args: argv,
|
|
303
|
+
allowPositionals: true,
|
|
304
|
+
options: {
|
|
305
|
+
"out-dir": { type: "string" },
|
|
306
|
+
conditions: { type: "string" },
|
|
307
|
+
port: { type: "string" },
|
|
308
|
+
name: { type: "string" },
|
|
309
|
+
pm: { type: "string" },
|
|
310
|
+
help: {
|
|
311
|
+
type: "boolean",
|
|
312
|
+
short: "h"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
const [command, target] = positionals;
|
|
317
|
+
if (command === "create" && !values.help) return createApp(target, values.name, values.pm);
|
|
318
|
+
if (values.help || command !== "build" && command !== "dev") {
|
|
319
|
+
console.log(USAGE);
|
|
320
|
+
return values.help ? 0 : 1;
|
|
321
|
+
}
|
|
322
|
+
const port = values.port === void 0 ? void 0 : Number(values.port);
|
|
323
|
+
if (port !== void 0 && !Number.isInteger(port)) {
|
|
324
|
+
console.error(`Invalid --port "${values.port}"`);
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
const options = { root: target ?? "." };
|
|
328
|
+
if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
|
|
329
|
+
if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
|
|
330
|
+
if (command === "dev") {
|
|
331
|
+
const stop = await dev({
|
|
332
|
+
...options,
|
|
333
|
+
port
|
|
334
|
+
});
|
|
335
|
+
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
336
|
+
await stop();
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
const started = performance.now();
|
|
340
|
+
const result = await build(options);
|
|
341
|
+
for (const file of result.files.filter((file) => !file.endsWith(".map"))) console.log(` ${relative(process.cwd(), file)}`);
|
|
342
|
+
printWarnings(result);
|
|
343
|
+
printBuildTime(started);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
async function createApp(dir, name, packageManagerFlag) {
|
|
347
|
+
if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
|
|
348
|
+
console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
name ??= await ask("Dot name:", dir ? toDisplayName(dir) : "Dot");
|
|
352
|
+
if (!name) {
|
|
353
|
+
console.error("A dot needs a name: dotctl create [dir] --name \"My Dot\"");
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
const { root, workspace } = await create({
|
|
357
|
+
name,
|
|
358
|
+
dir: dir ?? toPackageName(name)
|
|
359
|
+
});
|
|
360
|
+
const path = relative(process.cwd(), root) || ".";
|
|
361
|
+
console.log(`\nCreated ${name} in ${path}`);
|
|
362
|
+
if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
|
|
363
|
+
let packageManager = workspace ? "pnpm" : packageManagerFlag ?? launchedBy();
|
|
364
|
+
if (await confirm("\nInstall and start it now?")) {
|
|
365
|
+
if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
|
|
366
|
+
if (await run(packageManager, ["install"], root) === 0) return run(packageManager, ["run", "dev"], root);
|
|
367
|
+
console.error(`\n${packageManager} install failed.`);
|
|
368
|
+
}
|
|
369
|
+
console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
function run(command, args, cwd) {
|
|
373
|
+
return new Promise((resolvePromise, reject) => {
|
|
374
|
+
const child = spawn(command, args, {
|
|
375
|
+
cwd,
|
|
376
|
+
stdio: "inherit",
|
|
377
|
+
shell: process.platform === "win32"
|
|
378
|
+
});
|
|
379
|
+
child.on("error", reject);
|
|
380
|
+
child.on("exit", (code) => resolvePromise(code ?? 1));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
async function confirm(question) {
|
|
384
|
+
if (!process.stdin.isTTY) return false;
|
|
385
|
+
const answer = await readAnswer(`${question} (Y/n)`, "y");
|
|
386
|
+
return answer !== void 0 && answer.toLowerCase().startsWith("y");
|
|
387
|
+
}
|
|
388
|
+
async function ask(question, fallback) {
|
|
389
|
+
return readAnswer(`${question}${fallback ? ` (${fallback})` : ""}`, fallback);
|
|
390
|
+
}
|
|
391
|
+
async function readAnswer(prompt, fallback) {
|
|
392
|
+
if (!process.stdin.isTTY) return fallback;
|
|
393
|
+
const readline = createInterface({
|
|
394
|
+
input: process.stdin,
|
|
395
|
+
output: process.stdout
|
|
396
|
+
});
|
|
397
|
+
try {
|
|
398
|
+
return (await readline.question(`${prompt} `)).trim() || fallback;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error.name === "AbortError") return;
|
|
401
|
+
throw error;
|
|
402
|
+
} finally {
|
|
403
|
+
readline.close();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function choosePackageManager(suggested) {
|
|
407
|
+
const installed = installedManagers();
|
|
408
|
+
if (installed.length < 2) return installed[0] ?? suggested;
|
|
409
|
+
const fallback = installed.includes(suggested) ? suggested : installed[0];
|
|
410
|
+
while (true) {
|
|
411
|
+
const answer = await ask(`Package manager? ${installed.join(" / ")}`, fallback);
|
|
412
|
+
if (answer === void 0) return fallback;
|
|
413
|
+
if (isPackageManager(answer) && installed.includes(answer)) return answer;
|
|
414
|
+
console.log(`Choose one of ${installed.join(", ")}.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
418
|
+
console.error(`error: ${describeError(error)}`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
});
|
|
421
|
+
//#endregion
|
|
422
|
+
export {};
|
|
423
|
+
|
|
424
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/create/template.ts","../src/create/create.ts","../src/create/package-manager.ts","../src/report.ts","../src/dev.ts","../src/cli.ts"],"sourcesContent":["export interface TemplateOptions {\n name: string;\n packageName: string;\n dotVersion: string;\n workspace: boolean;\n}\n\n// The dot itself is copied from the counter example; these are the files around it.\nexport const TEMPLATE_FILES = ['dot.ts', 'pages', 'statics', '.env'] as const;\n\n// Editors that follow `$schema` check dot.json and complete its fields.\nconst DOT_JSON_SCHEMA = './node_modules/@monimesl/dot-runtime/dot.schema.json';\n\nconst json = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`;\n\nexport function projectFiles(options: TemplateOptions): Record<string, string> {\n return {\n 'package.json': packageJson(options),\n 'tsconfig.json': tsconfig(options),\n '.gitignore': 'node_modules/\\ndist/\\n# Types generated by dotctl\\n.dot/\\n',\n 'dot.json': json({\n $schema: DOT_JSON_SCHEMA,\n id: `com.example.${idWord(options.packageName)}`,\n name: options.name,\n version: '0.1.0',\n }),\n 'README.md': readme(options.name),\n };\n}\n\nfunction packageJson({ packageName, dotVersion, workspace }: TemplateOptions): string {\n const conditions = workspace ? ' --conditions @monimesl/dot-source' : '';\n return json({\n name: packageName,\n private: true,\n type: 'module',\n scripts: {\n dev: `dotctl dev${conditions}`,\n build: `dotctl build${conditions}`,\n typecheck: 'tsc --noEmit',\n },\n dependencies: { '@monimesl/dot-runtime': dotVersion },\n devDependencies: { '@monimesl/dot-cli': dotVersion, typescript: '^5.9.3' },\n });\n}\n\nfunction tsconfig({ workspace }: TemplateOptions): string {\n const compilerOptions: Record<string, unknown> = {\n target: 'ES2022',\n module: 'ESNext',\n moduleResolution: 'bundler',\n lib: ['ES2022'],\n types: ['@monimesl/dot-runtime/globals'],\n strict: true,\n noEmit: true,\n skipLibCheck: true,\n verbatimModuleSyntax: true,\n };\n if (workspace) {\n compilerOptions.customConditions = ['@monimesl/dot-source'];\n }\n return json({\n compilerOptions,\n include: ['dot.ts', 'pages', '.dot/pages.d.ts', '.dot/env.d.ts'],\n });\n}\n\nfunction readme(name: string): string {\n return `# ${name}\n\nA dot: a mini app that runs inside Dash.\n\n\\`\\`\\`sh\nnpm install\nnpm run dev # run it in the simulator; it reloads on every change\nnpm run build # build it for Dash\n\\`\\`\\`\n\nEach page is a folder under \\`pages/\\` with three files: \\`view.dml\\` (what it shows),\n\\`state.ts\\` (what it starts with) and \\`controller.ts\\` (how it changes). List new pages in\n\\`dot.ts\\`; the first one is where the dot starts.\n\n\\`dot.json\\` describes the dot to Dash. Change its \\`id\\` from \\`com.example.…\\` to a reverse domain\nname you own before you publish: it can't change afterwards. Your editor completes the other fields\nfrom the schema it points to.\n\n\\`.env\\` holds public configuration: variables named \\`DOT_PUBLIC_…\\`, which the service reads as\n\\`env.DOT_PUBLIC_…\\`. They are built into the dot, so anyone who has it can read them: never put a\nsecret there. \\`.env.development\\` and \\`.env.production\\` override it for \\`dev\\` and \\`build\\`.\n\nIt starts as a counter with two pages. \\`index\\` counts taps with mutations. \\`help/tips\\` shows\nnavigation: each page you open goes on top of the stack, Back returns to the one below, and the\ntrail at the top shows where you are.\n`;\n}\n\n// A word of the dot's id: the package name, which is already lowercase letters, digits and inner\n// hyphens, cut to the 63 characters a host name allows.\nfunction idWord(packageName: string): string {\n return packageName.slice(0, 63).replace(/-+$/, '');\n}\n","import { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { cp, mkdir, writeFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { basename, dirname, join, resolve } from 'node:path';\n\nimport { projectFiles, TEMPLATE_FILES } from './template';\n\nexport interface CreateOptions {\n name: string;\n dir: string;\n}\n\nexport interface CreateResult {\n readonly root: string;\n readonly workspace: boolean;\n}\n\ninterface PackageJson {\n name: string;\n version: string;\n}\n\nexport async function create({ name, dir }: CreateOptions): Promise<CreateResult> {\n const root = resolve(dir);\n if (existsSync(root) && readdirSync(root).length > 0) {\n throw new Error(`${dir} already exists and is not empty`);\n }\n const packageName = toPackageName(name);\n if (!packageName) {\n throw new Error(`\"${name}\" needs at least one letter or digit`);\n }\n\n const workspace = isInDotWorkspace(root);\n const template = templateDir();\n await mkdir(root, { recursive: true });\n for (const path of TEMPLATE_FILES) {\n await cp(join(template, path), join(root, path), { recursive: true });\n }\n const generated = projectFiles({\n name,\n packageName,\n dotVersion: workspace ? 'workspace:*' : `^${cliVersion()}`,\n workspace,\n });\n for (const [path, content] of Object.entries(generated)) {\n await writeFile(join(root, path), content);\n }\n return { root, workspace };\n}\n\nexport function toPackageName(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nexport function toDisplayName(dir: string): string {\n return basename(resolve(dir))\n .split(/[-_\\s]+/)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n// True when the nearest pnpm workspace above `dir` is the Dot repository.\nexport function isInDotWorkspace(dir: string): boolean {\n let current = dirname(dir);\n while (true) {\n if (existsSync(join(current, 'pnpm-workspace.yaml'))) {\n const runtimeManifest = join(current, 'packages/runtime/package.json');\n if (!existsSync(runtimeManifest)) {\n return false;\n }\n return readPackageJson(runtimeManifest).name === '@monimesl/dot-runtime';\n }\n const parent = dirname(current);\n if (parent === current) {\n return false;\n }\n current = parent;\n }\n}\n\n// The counter example: read from the repository, or from the copy the published CLI ships.\nfunction templateDir(): string {\n const example = join(cliRoot(), '../../examples/counter');\n return existsSync(example) ? example : join(cliRoot(), 'dist/template');\n}\n\nfunction cliVersion(): string {\n return readPackageJson(join(cliRoot(), 'package.json')).version;\n}\n\nfunction readPackageJson(path: string): PackageJson {\n return JSON.parse(readFileSync(path, 'utf8')) as PackageJson;\n}\n\nfunction cliRoot(): string {\n return dirname(createRequire(import.meta.url).resolve('@monimesl/dot-cli/package.json'));\n}\n","import { spawnSync } from 'node:child_process';\n\nexport const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'] as const;\nexport type PackageManager = (typeof PACKAGE_MANAGERS)[number];\n\nexport function isPackageManager(name: string): name is PackageManager {\n return (PACKAGE_MANAGERS as readonly string[]).includes(name);\n}\n\nexport function launchedBy(userAgent = process.env.npm_config_user_agent ?? ''): PackageManager {\n return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? 'npm';\n}\n\nexport function installedManagers(\n isInstalled: (name: PackageManager) => boolean = onPath,\n): PackageManager[] {\n return PACKAGE_MANAGERS.filter(isInstalled);\n}\n\nfunction onPath(name: PackageManager): boolean {\n return (\n spawnSync(name, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' })\n .status === 0\n );\n}\n","import { DmlParseError } from '@monimesl/dot-dml-parser';\nimport type { BuildResult } from '@monimesl/dot-compiler';\nimport { CompileError } from '@monimesl/dot-compiler';\nimport { ScriptParseError } from '@monimesl/dot-script-parser';\n\nexport function describeError(error: unknown): string {\n const known =\n error instanceof CompileError ||\n error instanceof DmlParseError ||\n error instanceof ScriptParseError;\n if (known) {\n return error.message;\n }\n if ((error as { code?: string }).code?.startsWith('ERR_PARSE_ARGS')) {\n // Node's message goes on to explain `--` for positional arguments, which is noise here.\n const [firstSentence] = (error as Error).message.split('. ');\n return `${firstSentence}. See dotctl --help.`;\n }\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\nexport function printWarnings({ warnings }: BuildResult): void {\n for (const warning of warnings) {\n console.warn(`warning: ${warning.message}`);\n }\n}\n\nexport function printBuildTime(startedAt: number): void {\n console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);\n}\n","import { watch } from 'node:fs';\nimport { relative, resolve, sep } from 'node:path';\n\nimport type { BuildOptions } from '@monimesl/dot-compiler';\nimport { build, installedRuntime, loadProject } from '@monimesl/dot-compiler';\nimport { startSimulator } from '@monimesl/dot-simulator';\n\nimport { describeError, printBuildTime, printWarnings } from './report';\n\nexport interface DevOptions extends BuildOptions {\n port?: number | undefined;\n}\n\n// Editors save a file in several writes; waiting this long turns them into one rebuild.\nconst CHANGE_DEBOUNCE_MS = 50;\n\nexport async function dev({ port, ...options }: DevOptions): Promise<() => Promise<void>> {\n const project = await loadProject(options.root);\n const outDir = resolve(project.root, options.outDir ?? 'dist');\n const buildOptions: BuildOptions = { mode: 'development', ...options, outDir };\n\n // Returns the error to show, with paths relative to the project.\n const buildApp = async (): Promise<string | null> => {\n const started = performance.now();\n try {\n printWarnings(await build(buildOptions));\n printBuildTime(started);\n return null;\n } catch (error) {\n const message = describeError(error).replaceAll(project.root + sep, '');\n console.error(`error: ${message}`);\n return message;\n }\n };\n\n const firstError = await buildApp();\n const simulator = await startSimulator({\n dotDir: outDir,\n reactHostDir: installedRuntime(project.root).react.dir,\n name: project.dotJson.name,\n port,\n });\n if (firstError) {\n simulator.error(firstError);\n }\n console.log(`\\n Simulator running at: ${simulator.url}\\n`);\n\n const rebuilds = new RebuildQueue(async () => {\n const error = await buildApp();\n if (error) {\n simulator.error(error);\n } else {\n simulator.reload();\n }\n });\n\n let debounceTimer: NodeJS.Timeout | undefined;\n const watcher = watch(project.root, { recursive: true }, (_event, file) => {\n if (!file || isIgnored(file)) {\n return;\n }\n const path = resolve(project.root, file);\n if (path === outDir || path.startsWith(outDir + sep)) {\n return;\n }\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n console.log(`Changed ${relative(project.root, path)}`);\n rebuilds.request();\n }, CHANGE_DEBOUNCE_MS);\n });\n\n return async () => {\n clearTimeout(debounceTimer);\n watcher.close();\n await simulator.close();\n };\n}\n\n// Runs one rebuild at a time; changes that arrive mid-build trigger exactly one more once it ends.\nclass RebuildQueue {\n readonly #rebuild: () => Promise<void>;\n #building = false;\n #changedWhileBuilding = false;\n\n constructor(rebuild: () => Promise<void>) {\n this.#rebuild = rebuild;\n }\n\n request(): void {\n if (this.#building) {\n this.#changedWhileBuilding = true;\n return;\n }\n void this.#build();\n }\n\n async #build(): Promise<void> {\n this.#building = true;\n this.#changedWhileBuilding = false;\n try {\n await this.#rebuild();\n } finally {\n this.#building = false;\n }\n if (this.#changedWhileBuilding) {\n this.request();\n }\n }\n}\n\n// Dependencies and hidden folders such as .git or .dot, but not .env files, which the build reads.\nfunction isIgnored(file: string): boolean {\n return file.split(/[\\\\/]/).some((segment) => {\n if (segment === '.env' || segment.startsWith('.env.')) {\n return false;\n }\n return segment === 'node_modules' || segment.startsWith('.');\n });\n}\n","import { spawn } from 'node:child_process';\nimport { relative } from 'node:path';\nimport { createInterface } from 'node:readline/promises';\nimport { parseArgs } from 'node:util';\n\nimport type { BuildOptions } from '@monimesl/dot-compiler';\nimport { build } from '@monimesl/dot-compiler';\nimport { DEFAULT_PORT } from '@monimesl/dot-simulator';\n\nimport { create, toDisplayName, toPackageName } from './create/create';\nimport type { PackageManager } from './create/package-manager';\nimport {\n installedManagers,\n isPackageManager,\n launchedBy,\n PACKAGE_MANAGERS,\n} from './create/package-manager';\nimport { dev } from './dev';\nimport { describeError, printBuildTime, printWarnings } from './report';\n\nconst USAGE = `Usage:\n dotctl create [dir] [--name <name>] [--pm <npm|pnpm|yarn|bun>]\n Create a dot (asks for its name)\n dotctl build [root] [options] Build the minified bundles the host app runs\n dotctl dev [root] [options] Run in the browser simulator, rebuilding on change\n\nOptions:\n --out-dir <dir> Output directory (default: <root>/dist)\n --conditions <list> Extra export conditions, comma separated\n --port <port> Simulator port for \\`dev\\`; the next free one is used if taken\n (default: ${DEFAULT_PORT})\n -h, --help Show this message`;\n\nasync function main(argv: string[]): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n 'out-dir': { type: 'string' },\n conditions: { type: 'string' },\n port: { type: 'string' },\n name: { type: 'string' },\n pm: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n });\n\n const [command, target] = positionals;\n if (command === 'create' && !values.help) {\n return createApp(target, values.name, values.pm);\n }\n if (values.help || (command !== 'build' && command !== 'dev')) {\n console.log(USAGE);\n return values.help ? 0 : 1;\n }\n const port = values.port === undefined ? undefined : Number(values.port);\n if (port !== undefined && !Number.isInteger(port)) {\n console.error(`Invalid --port \"${values.port}\"`);\n return 1;\n }\n\n const options: BuildOptions = { root: target ?? '.' };\n if (values['out-dir'] !== undefined) {\n options.outDir = values['out-dir'];\n }\n if (values.conditions !== undefined) {\n options.conditions = values.conditions.split(',');\n }\n\n if (command === 'dev') {\n const stop = await dev({ ...options, port });\n await new Promise((resolve) => process.once('SIGINT', resolve));\n await stop();\n return 0;\n }\n\n const started = performance.now();\n const result = await build(options);\n for (const file of result.files.filter((file) => !file.endsWith('.map'))) {\n console.log(` ${relative(process.cwd(), file)}`);\n }\n printWarnings(result);\n printBuildTime(started);\n return 0;\n}\n\nasync function createApp(\n dir: string | undefined,\n name: string | undefined,\n packageManagerFlag: string | undefined,\n): Promise<number> {\n if (packageManagerFlag !== undefined && !isPackageManager(packageManagerFlag)) {\n console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(', ')}`);\n return 1;\n }\n name ??= await ask('Dot name:', dir ? toDisplayName(dir) : 'Dot');\n if (!name) {\n console.error('A dot needs a name: dotctl create [dir] --name \"My Dot\"');\n return 1;\n }\n const { root, workspace } = await create({ name, dir: dir ?? toPackageName(name) });\n const path = relative(process.cwd(), root) || '.';\n console.log(`\\nCreated ${name} in ${path}`);\n if (workspace) {\n console.log('It is inside the Dot repository, so it uses the workspace packages.');\n }\n\n // A command cannot change its shell's directory, so it offers to carry on inside the new dot instead.\n let packageManager: PackageManager = workspace ? 'pnpm' : (packageManagerFlag ?? launchedBy());\n if (await confirm('\\nInstall and start it now?')) {\n if (!workspace && !packageManagerFlag) {\n packageManager = await choosePackageManager(packageManager);\n }\n const installed = await run(packageManager, ['install'], root);\n if (installed === 0) {\n return run(packageManager, ['run', 'dev'], root);\n }\n console.error(`\\n${packageManager} install failed.`);\n }\n console.log(\n `\\nNext:\\n\\n cd ${path}\\n ${packageManager} install\\n ${packageManager} run dev\\n`,\n );\n return 0;\n}\n\nfunction run(command: string, args: string[], cwd: string): Promise<number> {\n return new Promise((resolvePromise, reject) => {\n const child = spawn(command, args, {\n cwd,\n stdio: 'inherit',\n shell: process.platform === 'win32',\n });\n child.on('error', reject);\n child.on('exit', (code) => resolvePromise(code ?? 1));\n });\n}\n\nasync function confirm(question: string): Promise<boolean> {\n if (!process.stdin.isTTY) {\n return false;\n }\n const answer = await readAnswer(`${question} (Y/n)`, 'y');\n return answer !== undefined && answer.toLowerCase().startsWith('y');\n}\n\n// Shows the fallback after the question, as the answer an empty reply gives.\nasync function ask(question: string, fallback?: string): Promise<string | undefined> {\n const hint = fallback ? ` (${fallback})` : '';\n return readAnswer(`${question}${hint}`, fallback);\n}\n\nasync function readAnswer(prompt: string, fallback?: string): Promise<string | undefined> {\n if (!process.stdin.isTTY) {\n return fallback;\n }\n const readline = createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer = await readline.question(`${prompt} `);\n return answer.trim() || fallback;\n } catch (error) {\n // Ctrl+C or Ctrl+D\n if ((error as Error).name === 'AbortError') {\n return undefined;\n }\n throw error;\n } finally {\n readline.close();\n }\n}\n\nasync function choosePackageManager(suggested: PackageManager): Promise<PackageManager> {\n const installed = installedManagers();\n if (installed.length < 2) {\n return installed[0] ?? suggested;\n }\n const fallback = installed.includes(suggested) ? suggested : installed[0]!;\n while (true) {\n const answer = await ask(`Package manager? ${installed.join(' / ')}`, fallback);\n if (answer === undefined) {\n return fallback;\n }\n if (isPackageManager(answer) && installed.includes(answer)) {\n return answer;\n }\n console.log(`Choose one of ${installed.join(', ')}.`);\n }\n}\n\nmain(process.argv.slice(2)).then(\n (code) => process.exit(code),\n (error: unknown) => {\n console.error(`error: ${describeError(error)}`);\n process.exit(1);\n },\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
export default definePage({
|
|
4
|
+
initialState: state,
|
|
5
|
+
hooks: {
|
|
6
|
+
// The page that opens this one passes its own trail and depth.
|
|
7
|
+
onLoad(options) {
|
|
8
|
+
this.setState({
|
|
9
|
+
count: Number(options.count ?? 0),
|
|
10
|
+
trail: `${options.trail ?? 'Home'} › Tips`,
|
|
11
|
+
depth: Number(options.depth ?? 1) + 1,
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
actions: {
|
|
16
|
+
back() {
|
|
17
|
+
this.navigation.back();
|
|
18
|
+
},
|
|
19
|
+
openAnother(state) {
|
|
20
|
+
this.navigation.push('help/tips', {
|
|
21
|
+
count: String(state.count),
|
|
22
|
+
trail: state.trail,
|
|
23
|
+
depth: String(state.depth),
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
startOver() {
|
|
27
|
+
this.navigation.reset('index');
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
count: 0,
|
|
3
|
+
trail: '',
|
|
4
|
+
depth: 0,
|
|
5
|
+
maxPages: 8,
|
|
6
|
+
tips: [
|
|
7
|
+
'Every tap is handled by controller.ts in a Worker',
|
|
8
|
+
'The view only renders the state it receives',
|
|
9
|
+
'Pages open on top of each other; Back returns to the one below',
|
|
10
|
+
],
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<Text variant="caption">{{ trail }}</Text>
|
|
2
|
+
<Text variant="title">Tips</Text>
|
|
3
|
+
<Text>Page {{ depth }} of {{ maxPages }}, opened at {{ count }} taps.</Text>
|
|
4
|
+
|
|
5
|
+
<View variant="card" gap="small">
|
|
6
|
+
<For each="{{ tips }}" item="tip" key="*this">
|
|
7
|
+
<Text>{{ tip }}</Text>
|
|
8
|
+
</For>
|
|
9
|
+
</View>
|
|
10
|
+
|
|
11
|
+
<View direction="row">
|
|
12
|
+
<Button onClick="back">Back</Button>
|
|
13
|
+
<Button variant="secondary" onClick="openAnother" disabled="{{ depth >= maxPages }}">Open another</Button>
|
|
14
|
+
<Button variant="secondary" onClick="startOver">Start over</Button>
|
|
15
|
+
</View>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
export default definePage({
|
|
4
|
+
initialState: state,
|
|
5
|
+
hooks: {
|
|
6
|
+
onShow() {
|
|
7
|
+
this.setState((current) => ({ visits: current.visits + 1 }));
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
mutations: {
|
|
11
|
+
increment: (state) => {
|
|
12
|
+
const count = state.count + 1;
|
|
13
|
+
return { count, taps: [{ id: count }, ...state.taps] };
|
|
14
|
+
},
|
|
15
|
+
reset: () => ({ count: 0, taps: [] }),
|
|
16
|
+
rename: (_state, event: DotEvent<{ value: string }>) => ({ title: event.detail.value }),
|
|
17
|
+
logoLoaded: (_state, event: DotEvent<{ width: number; height: number }>) => ({
|
|
18
|
+
logoStatus: 'loaded' as const,
|
|
19
|
+
logoSize: `${event.detail.width}x${event.detail.height}`,
|
|
20
|
+
}),
|
|
21
|
+
logoFailed: () => ({ logoStatus: 'failed' as const }),
|
|
22
|
+
},
|
|
23
|
+
actions: {
|
|
24
|
+
fillUp() {
|
|
25
|
+
while (this.state.count < this.state.limit) {
|
|
26
|
+
this.mutations.increment();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async sayHello(state) {
|
|
30
|
+
try {
|
|
31
|
+
const { action } = await dash.ui.alert(
|
|
32
|
+
{
|
|
33
|
+
title: `Hello from ${state.title}`,
|
|
34
|
+
message: env.DOT_PUBLIC_GREETING,
|
|
35
|
+
actions: [
|
|
36
|
+
{ id: 'nice', label: 'Nice' },
|
|
37
|
+
{ id: 'close', label: 'Close', role: 'cancel' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{ timeout: 30_000 },
|
|
41
|
+
);
|
|
42
|
+
this.setState({ dashNote: `You chose "${action}"` });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.setState({ dashNote: errorMessage(error) });
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async vibrate() {
|
|
48
|
+
await dash.system.vibrate({ style: 'success' });
|
|
49
|
+
},
|
|
50
|
+
async toggleTorch(state) {
|
|
51
|
+
const next = state.torch === 'on' ? 'off' : 'on';
|
|
52
|
+
try {
|
|
53
|
+
await dash.system.torch(next);
|
|
54
|
+
this.setState({ torch: next, dashNote: `Torch ${next}` });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this.setState({ dashNote: errorMessage(error) });
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
openTips(state) {
|
|
60
|
+
this.navigation.push('help/tips', { count: String(state.count), trail: 'Home', depth: '1' });
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function errorMessage(error: unknown): string {
|
|
66
|
+
return error instanceof Error ? error.message : String(error);
|
|
67
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Tap {
|
|
2
|
+
id: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type LogoStatus = 'loading' | 'loaded' | 'failed';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Dot Counter',
|
|
9
|
+
logoStatus: 'loading' as LogoStatus,
|
|
10
|
+
logoSize: '',
|
|
11
|
+
count: 0,
|
|
12
|
+
limit: 5,
|
|
13
|
+
taps: [] as Tap[],
|
|
14
|
+
visits: 0,
|
|
15
|
+
torch: 'off' as 'on' | 'off',
|
|
16
|
+
dashNote: '',
|
|
17
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<Text variant="caption">Home</Text>
|
|
2
|
+
|
|
3
|
+
<View direction="row" align="center" gap="large">
|
|
4
|
+
<Image src="statics/logo.png" alt="Dot logo" width="48" height="48" onLoad="logoLoaded" onError="logoFailed" />
|
|
5
|
+
<View gap="none">
|
|
6
|
+
<Text variant="title">{{ title }}</Text>
|
|
7
|
+
<If condition="{{ logoStatus === 'loaded' }}">
|
|
8
|
+
<Text variant="caption">Logo loaded at {{ logoSize }}</Text>
|
|
9
|
+
</If>
|
|
10
|
+
<ElseIf condition="{{ logoStatus === 'failed' }}">
|
|
11
|
+
<Text variant="caption">Logo failed to load</Text>
|
|
12
|
+
</ElseIf>
|
|
13
|
+
<Else>
|
|
14
|
+
<Text variant="caption">Loading logo…</Text>
|
|
15
|
+
</Else>
|
|
16
|
+
</View>
|
|
17
|
+
</View>
|
|
18
|
+
|
|
19
|
+
<View variant="card">
|
|
20
|
+
<Text variant="title">{{ count }} / {{ limit }}</Text>
|
|
21
|
+
<If condition="{{ count >= limit }}">
|
|
22
|
+
<Text>Limit reached</Text>
|
|
23
|
+
</If>
|
|
24
|
+
<ElseIf condition="{{ count > 0 }}">
|
|
25
|
+
<Text>Keep tapping</Text>
|
|
26
|
+
</ElseIf>
|
|
27
|
+
<Else>
|
|
28
|
+
<Text>Tap to start 🚀</Text>
|
|
29
|
+
</Else>
|
|
30
|
+
<View direction="row">
|
|
31
|
+
<Button onClick="increment" disabled="{{ count >= limit }}">Add one</Button>
|
|
32
|
+
<Button variant="secondary" onClick="reset" disabled="{{ count === 0 }}">Reset</Button>
|
|
33
|
+
<Button variant="secondary" onClick="fillUp" disabled="{{ count >= limit }}">Fill up</Button>
|
|
34
|
+
</View>
|
|
35
|
+
</View>
|
|
36
|
+
|
|
37
|
+
<View variant="card" gap="small">
|
|
38
|
+
<Text variant="caption">Name your counter</Text>
|
|
39
|
+
<Input label="Counter name" placeholder="Dot Counter" value="{{ title }}" maxLength="24" onChange="rename" />
|
|
40
|
+
</View>
|
|
41
|
+
|
|
42
|
+
<View variant="card" gap="small">
|
|
43
|
+
<Text variant="caption">Dash</Text>
|
|
44
|
+
<View direction="row">
|
|
45
|
+
<Button variant="secondary" onClick="sayHello">Say hello</Button>
|
|
46
|
+
<Button variant="secondary" onClick="vibrate">Vibrate</Button>
|
|
47
|
+
<Button variant="secondary" onClick="toggleTorch">{{ torch === 'on' ? 'Torch off' : 'Torch on' }}</Button>
|
|
48
|
+
</View>
|
|
49
|
+
<If condition="{{ dashNote }}">
|
|
50
|
+
<Text variant="caption">{{ dashNote }}</Text>
|
|
51
|
+
</If>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
<View variant="card" onTap="openTips">
|
|
55
|
+
<Text>Tips -> </Text>
|
|
56
|
+
<Text variant="caption">Shown {{ visits }} {{ visits === 1 ? 'time' : 'times' }}</Text>
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
<If condition="{{ taps.length > 0 }}">
|
|
60
|
+
<View variant="card" gap="small">
|
|
61
|
+
<Text variant="caption">History</Text>
|
|
62
|
+
<For each="{{ taps }}" index="i" item="tap" key="id">
|
|
63
|
+
<Text>{{ i + 1 }}. Tap #{{ tap.id }}</Text>
|
|
64
|
+
</For>
|
|
65
|
+
</View>
|
|
66
|
+
</If>
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monimesl/dot-cli",
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
|
+
"description": "The dotctl command: build mini apps and run them in the simulator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/monime-lab/dot.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.12.0"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"dotctl": "./bin/dotctl.js"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@monimesl/dot-dml-parser": "0.1.0-alpha.2",
|
|
26
|
+
"@monimesl/dot-script-parser": "0.1.0-alpha.2",
|
|
27
|
+
"@monimesl/dot-compiler": "0.1.0-alpha.2",
|
|
28
|
+
"@monimesl/dot-simulator": "0.1.0-alpha.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.20.4",
|
|
32
|
+
"publint": "^0.3.24",
|
|
33
|
+
"rimraf": "^6.1.3",
|
|
34
|
+
"tsdown": "^0.23.0",
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"vitest": "^5.0.1"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"dev": "tsdown --watch",
|
|
44
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
45
|
+
"clean": "rimraf dist *.tsbuildinfo",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
}
|
|
48
|
+
}
|