@botfather/units-tools 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Units Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/format-ui.mjs ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatUnits } from "@botfather/units/print";
5
+
6
+ async function collectUiFiles(entry, isRoot = false) {
7
+ const base = path.basename(entry);
8
+ if (base === "node_modules" || base === ".git" || base === "dist" || base === "build" || base === ".vite") {
9
+ return [];
10
+ }
11
+ let stat = await fs.lstat(entry);
12
+ if (stat.isSymbolicLink()) {
13
+ if (!isRoot) return [];
14
+ stat = await fs.stat(entry);
15
+ }
16
+ if (stat.isFile()) return entry.endsWith(".ui") ? [entry] : [];
17
+ const out = [];
18
+ const items = await fs.readdir(entry);
19
+ for (const item of items) {
20
+ const full = path.join(entry, item);
21
+ const sub = await collectUiFiles(full, false);
22
+ out.push(...sub);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ async function formatFile(file) {
28
+ const src = await fs.readFile(file, "utf-8");
29
+ const formatted = formatUnits(src);
30
+ if (formatted !== src) {
31
+ await fs.writeFile(file, formatted, "utf-8");
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ const targets = process.argv.slice(2);
38
+ if (targets.length === 0) {
39
+ console.error("Usage: node format-ui.mjs <file-or-dir>...");
40
+ process.exit(1);
41
+ }
42
+
43
+ let changed = 0;
44
+ for (const target of targets) {
45
+ const abs = path.resolve(process.cwd(), target);
46
+ const files = await collectUiFiles(abs, true);
47
+ for (const file of files) {
48
+ const didChange = await formatFile(file);
49
+ if (didChange) changed++;
50
+ }
51
+ }
52
+
53
+ console.log(`Formatted ${changed} file(s).`);
package/lint-ui.mjs ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ const lintScript = path.join(here, "units-lint.mjs");
8
+ const targets = process.argv.slice(2);
9
+ const args = [lintScript, ...(targets.length ? targets : ["examples", "packages/units-uikit-shadcn"])];
10
+
11
+ const child = spawn(process.execPath, args, { stdio: "inherit" });
12
+ child.on("exit", (code) => process.exit(code ?? 1));
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@botfather/units-tools",
3
+ "version": "1.0.0",
4
+ "description": "CLI tools for Units (.ui) formatting, linting, manifests, and watch.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Botfather/units.git",
10
+ "directory": "packages/units-tools"
11
+ },
12
+ "homepage": "https://github.com/Botfather/units/tree/main/packages/units-tools",
13
+ "bugs": {
14
+ "url": "https://github.com/Botfather/units/issues"
15
+ },
16
+ "bin": {
17
+ "units-format": "./units-format.mjs",
18
+ "units-lint": "./units-lint.mjs",
19
+ "units-emit": "./units-emit.mjs",
20
+ "units-manifest": "./units-manifest.mjs",
21
+ "units-watch": "./units-watch.mjs",
22
+ "format-ui": "./format-ui.mjs",
23
+ "lint-ui": "./lint-ui.mjs"
24
+ },
25
+ "files": [
26
+ "*.mjs"
27
+ ],
28
+ "dependencies": {
29
+ "@botfather/units": "1.0.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
package/units-emit.mjs ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parseUnits } from "@botfather/units/parser";
5
+
6
+ async function collectUiFiles(entry, isRoot = false) {
7
+ const base = path.basename(entry);
8
+ if (base === "node_modules" || base === ".git" || base === "dist" || base === "build" || base === ".vite" || base === "vite-app") {
9
+ return [];
10
+ }
11
+ let stat = await fs.lstat(entry);
12
+ if (stat.isSymbolicLink()) {
13
+ if (!isRoot) return [];
14
+ stat = await fs.stat(entry);
15
+ }
16
+ if (stat.isFile()) return entry.endsWith(".ui") ? [entry] : [];
17
+ const out = [];
18
+ const items = await fs.readdir(entry);
19
+ for (const item of items) {
20
+ const full = path.join(entry, item);
21
+ const sub = await collectUiFiles(full, false);
22
+ out.push(...sub);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ async function emitAst(file) {
28
+ const src = await fs.readFile(file, "utf-8");
29
+ const ast = parseUnits(src);
30
+ const outFile = `${file}.ast.json`;
31
+ await fs.writeFile(outFile, JSON.stringify(ast), "utf-8");
32
+ return outFile;
33
+ }
34
+
35
+ const targets = process.argv.slice(2);
36
+ if (targets.length === 0) {
37
+ console.error("Usage: node units-emit.mjs <file-or-dir>...");
38
+ process.exit(1);
39
+ }
40
+
41
+ let count = 0;
42
+ for (const target of targets) {
43
+ const abs = path.resolve(process.cwd(), target);
44
+ const files = await collectUiFiles(abs, true);
45
+ for (const file of files) {
46
+ await emitAst(file);
47
+ count++;
48
+ }
49
+ }
50
+
51
+ console.log(`Emitted AST for ${count} file(s).`);
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatUnits } from "@botfather/units/print";
5
+
6
+ async function collectUiFiles(entry, isRoot = false) {
7
+ const base = path.basename(entry);
8
+ if (base === "node_modules" || base === ".git" || base === "dist" || base === "build" || base === ".vite" || base === "vite-app") {
9
+ return [];
10
+ }
11
+ let stat = await fs.lstat(entry);
12
+ if (stat.isSymbolicLink()) {
13
+ if (!isRoot) return [];
14
+ stat = await fs.stat(entry);
15
+ }
16
+ if (stat.isFile()) return entry.endsWith(".ui") ? [entry] : [];
17
+ const out = [];
18
+ const items = await fs.readdir(entry);
19
+ for (const item of items) {
20
+ const full = path.join(entry, item);
21
+ const sub = await collectUiFiles(full, false);
22
+ out.push(...sub);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ async function formatFile(file) {
28
+ const src = await fs.readFile(file, "utf-8");
29
+ const formatted = formatUnits(src);
30
+ if (formatted !== src) {
31
+ await fs.writeFile(file, formatted, "utf-8");
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ const targets = process.argv.slice(2);
38
+ if (targets.length === 0) {
39
+ console.error("Usage: node units-format.mjs <file-or-dir>...");
40
+ process.exit(1);
41
+ }
42
+
43
+ let changed = 0;
44
+ for (const target of targets) {
45
+ const abs = path.resolve(process.cwd(), target);
46
+ const files = await collectUiFiles(abs, true);
47
+ for (const file of files) {
48
+ const didChange = await formatFile(file);
49
+ if (didChange) changed++;
50
+ }
51
+ }
52
+
53
+ console.log(`Formatted ${changed} file(s).`);
package/units-lint.mjs ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatUnits } from "@botfather/units/print";
5
+
6
+ async function collectUiFiles(entry, isRoot = false) {
7
+ const base = path.basename(entry);
8
+ if (base === "node_modules" || base === ".git" || base === "dist" || base === "build" || base === ".vite" || base === "vite-app") {
9
+ return [];
10
+ }
11
+ let stat = await fs.lstat(entry);
12
+ if (stat.isSymbolicLink()) {
13
+ if (!isRoot) return [];
14
+ stat = await fs.stat(entry);
15
+ }
16
+ if (stat.isFile()) return entry.endsWith(".ui") ? [entry] : [];
17
+ const out = [];
18
+ const items = await fs.readdir(entry);
19
+ for (const item of items) {
20
+ const full = path.join(entry, item);
21
+ const sub = await collectUiFiles(full, false);
22
+ out.push(...sub);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ async function lintFile(file) {
28
+ const src = await fs.readFile(file, "utf-8");
29
+ const formatted = formatUnits(src);
30
+ if (formatted !== src) {
31
+ console.error(`Not formatted: ${file}`);
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+
37
+ const targets = process.argv.slice(2);
38
+ if (targets.length === 0) {
39
+ console.error("Usage: node units-lint.mjs <file-or-dir>...");
40
+ process.exit(1);
41
+ }
42
+
43
+ let ok = true;
44
+ for (const target of targets) {
45
+ const abs = path.resolve(process.cwd(), target);
46
+ const files = await collectUiFiles(abs, true);
47
+ for (const file of files) {
48
+ const pass = await lintFile(file);
49
+ if (!pass) ok = false;
50
+ }
51
+ }
52
+
53
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ async function collectUiFiles(entry, isRoot = false) {
6
+ const base = path.basename(entry);
7
+ if (base === "node_modules" || base === ".git" || base === "dist" || base === "build" || base === ".vite" || base === "vite-app") {
8
+ return [];
9
+ }
10
+ let stat = await fs.lstat(entry);
11
+ if (stat.isSymbolicLink()) {
12
+ if (!isRoot) return [];
13
+ stat = await fs.stat(entry);
14
+ }
15
+ if (stat.isFile()) return entry.endsWith(".ui") ? [entry] : [];
16
+ const out = [];
17
+ const items = await fs.readdir(entry);
18
+ for (const item of items) {
19
+ const full = path.join(entry, item);
20
+ const sub = await collectUiFiles(full, false);
21
+ out.push(...sub);
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function toComponentName(file) {
27
+ return path.basename(file, ".ui");
28
+ }
29
+
30
+ function toRel(from, file) {
31
+ const rel = path.relative(from, file);
32
+ return rel.startsWith(".") ? rel : `./${rel}`;
33
+ }
34
+
35
+ const [rootDir, outFile] = process.argv.slice(2);
36
+ if (!rootDir || !outFile) {
37
+ console.error("Usage: node units-manifest.mjs <rootDir> <outFile>");
38
+ process.exit(1);
39
+ }
40
+
41
+ const rootAbs = path.resolve(process.cwd(), rootDir);
42
+ const outAbs = path.resolve(process.cwd(), outFile);
43
+ const outDir = path.dirname(outAbs);
44
+
45
+ const files = await collectUiFiles(rootAbs, true);
46
+ files.sort();
47
+
48
+ let imports = "";
49
+ let entries = "";
50
+
51
+ files.forEach((file, idx) => {
52
+ const name = toComponentName(file);
53
+ const rel = toRel(outDir, file).replace(/\\/g, "/");
54
+ const varName = `Ast_${idx}`;
55
+ imports += `import ${varName} from \"${rel}\";\n`;
56
+ entries += ` \"${name}\": ${varName},\n`;
57
+ });
58
+
59
+ const content = `${imports}\nexport const uiManifest = {\n${entries}};\n`;
60
+ await fs.writeFile(outAbs, content, "utf-8");
61
+
62
+ console.log(`Wrote manifest with ${files.length} entries -> ${outAbs}`);
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+
6
+ const [rootDir, outFile] = process.argv.slice(2);
7
+ if (!rootDir || !outFile) {
8
+ console.error("Usage: node units-watch.mjs <rootDir> <outFile>");
9
+ process.exit(1);
10
+ }
11
+
12
+ const rootAbs = path.resolve(process.cwd(), rootDir);
13
+ const outAbs = path.resolve(process.cwd(), outFile);
14
+
15
+ const ignoreDirs = new Set(["node_modules", ".git", "dist", "build", ".vite", "vite-app"]);
16
+
17
+ function shouldIgnore(filePath) {
18
+ const parts = filePath.split(path.sep);
19
+ return parts.some((p) => ignoreDirs.has(p));
20
+ }
21
+
22
+ function isUi(filePath) {
23
+ return filePath.endsWith(".ui");
24
+ }
25
+
26
+ function runTool(script, args) {
27
+ return new Promise((resolve, reject) => {
28
+ const proc = spawn(process.execPath, [script, ...args], { stdio: "inherit" });
29
+ proc.on("exit", (code) => {
30
+ if (code === 0) resolve();
31
+ else reject(new Error(`${script} failed with code ${code}`));
32
+ });
33
+ });
34
+ }
35
+
36
+ let timer = null;
37
+ let running = false;
38
+ let pending = false;
39
+
40
+ async function rebuild() {
41
+ if (running) {
42
+ pending = true;
43
+ return;
44
+ }
45
+ running = true;
46
+ try {
47
+ const toolsDir = path.resolve(path.dirname(new URL(import.meta.url).pathname));
48
+ const manifestTool = path.resolve(toolsDir, "units-manifest.mjs");
49
+ const emitTool = path.resolve(toolsDir, "units-emit.mjs");
50
+ await runTool(manifestTool, [rootAbs, outAbs]);
51
+ await runTool(emitTool, [rootAbs]);
52
+ } catch (err) {
53
+ console.error(err.message || err);
54
+ } finally {
55
+ running = false;
56
+ if (pending) {
57
+ pending = false;
58
+ rebuild();
59
+ }
60
+ }
61
+ }
62
+
63
+ function schedule() {
64
+ if (timer) clearTimeout(timer);
65
+ timer = setTimeout(rebuild, 200);
66
+ }
67
+
68
+ console.log(`Watching ${rootAbs} for .ui changes...`);
69
+ rebuild();
70
+
71
+ const watcher = fs.watch(rootAbs, { recursive: true }, (event, filename) => {
72
+ if (!filename) return;
73
+ const full = path.join(rootAbs, filename);
74
+ if (shouldIgnore(full)) return;
75
+ if (!isUi(full)) return;
76
+ schedule();
77
+ });
78
+
79
+ process.on("SIGINT", () => {
80
+ watcher.close();
81
+ process.exit(0);
82
+ });