@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
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
@@ -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,3 @@
1
+ # Public configuration for the dot. Only DOT_PUBLIC_ variables reach it, and anyone with the dot
2
+ # can read them: never put a secret here. Read them in the service as `env.DOT_PUBLIC_…`.
3
+ DOT_PUBLIC_GREETING=Dash draws this alert, not the dot.
@@ -0,0 +1,6 @@
1
+ import { logger } from '@monimesl/dot-runtime';
2
+
3
+ export default defineDot({
4
+ pages: ['index', 'help/tips'],
5
+ plugins: [logger()],
6
+ });
@@ -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
+ }