@ergonui/cli 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +52 -2
- package/dist/index.js +619 -0
- package/dist/template/gitignore +4 -0
- package/dist/template/index.html +12 -0
- package/dist/template/package.json +26 -0
- package/dist/template/src/App.tsx +13 -0
- package/dist/template/src/index.css +1 -0
- package/dist/template/src/main.tsx +10 -0
- package/dist/template/tsconfig.app.json +19 -0
- package/dist/template/tsconfig.json +5 -0
- package/dist/template/tsconfig.node.json +12 -0
- package/dist/template/vite.config.ts +9 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ErgonUI contributors
|
|
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/README.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ergonui/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create Bun + Vite + React 19 + Tailwind 4 applications and install editable ErgonUI component source. No ErgonUI repository clone is needed by consumers.
|
|
4
|
+
|
|
5
|
+
This package is prepared for its first npm release. The `bunx @ergonui/cli` commands below become publicly available after publication. Until then, use the built executable or install its packed tarball locally.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bunx @ergonui/cli create my-app --components line-chart,pie-chart
|
|
9
|
+
cd my-app
|
|
10
|
+
bun run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Selected components are copied to `src/components/ui`; import them using the examples in the [handbook](https://ergonui.netlify.app/#/components). The starter configures aliases, Tailwind bindings, light/dark tokens, animation utilities, and `components.json`. Canvas and zoom helpers retain their dynamic import boundaries.
|
|
14
|
+
|
|
15
|
+
For an existing Vite + React 19 TypeScript application:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
bunx @ergonui/cli init
|
|
19
|
+
bunx @ergonui/cli add button table-plus
|
|
20
|
+
bunx @ergonui/cli add line-chart --dry-run
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Initialization merges TypeScript aliases and a Vite configuration adapter while preserving existing plugins/settings, JSONC comments, CSS rules and existing token values. Version 1 supports a `@/*` source alias, aliases rooted at `@/`, a `src/main.tsx` entry (or its configured source-directory equivalent), and a TypeScript Vite configuration with one default export. Unsupported configurations fail before writing; use the manual handbook for other stacks/configurations.
|
|
24
|
+
|
|
25
|
+
`add` refuses differing existing files before writing any files. Review your changes, then explicitly use `--overwrite` to replace customized components. Select `theme` with `--overwrite` to replace theme token values. Initialization preserves existing tokens by default. `--keep-theme` also prevents explicitly selected theme values from being replaced. Licenses shipped by the registry are retained.
|
|
26
|
+
|
|
27
|
+
Options: `--cwd <path>`, `--components <comma-separated-names>`, `--registry <origin>`, `--overwrite`, `--keep-theme`, `--dry-run`, `--no-install`, `--help`, and `--version`. With `--no-install`, dependencies are recorded in package.json; run `bun install` before running your application.
|
|
28
|
+
|
|
29
|
+
The default registry is `https://ergonui.netlify.app`. For unpublished component work:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bunx @ergonui/cli create local-app --components line-chart,pie-chart --registry http://localhost:5174
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`init` saves that registry in `components.json`. Future `add` calls reuse it; a one-off `--registry` on `add` does not change the saved configuration. A missing entry reports its URL/status and exits before writing source. The CLI supports ErgonUI's generated UI/theme registry items, bundled local source and exact npm versions; it is not a general-purpose installer for every third-party registry format.
|
|
36
|
+
|
|
37
|
+
Dependency installation happens after files are written. If Bun installation fails, resolve the reported error and retry the same command to install the declared dependencies. Registry source is copied at installation time; applications run independently of the registry afterward.
|
|
38
|
+
|
|
39
|
+
## Development and publication
|
|
40
|
+
|
|
41
|
+
From this directory:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
bun install --frozen-lockfile
|
|
45
|
+
bun run check
|
|
46
|
+
bun run test
|
|
47
|
+
bun run build
|
|
48
|
+
npm pack --dry-run
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
See [the publishing guide](../../docs/PUBLISHING.md) for npm scope ownership, first-release validation and publishing. The repository root remains private; publish from `packages/cli` only.
|
|
52
|
+
|
|
53
|
+
ErgonUI is built on shadcn/ui component source and community work, including copied/adapted MIT-licensed components alongside original work. This CLI is original ErgonUI code. Registry payloads preserve the applicable source licenses; retain them in consuming applications. See the repository's `THIRD-PARTY-NOTICES.md` for provenance.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { parseArgs } from "util";
|
|
6
|
+
|
|
7
|
+
// src/engine.ts
|
|
8
|
+
import { readdir, realpath } from "fs/promises";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { basename, resolve as resolve3 } from "path";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
|
|
13
|
+
// src/files.ts
|
|
14
|
+
import { lstat, mkdir, readFile, writeFile } from "fs/promises";
|
|
15
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
16
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
17
|
+
async function read(path) {
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(path, "utf8");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT")
|
|
22
|
+
return;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function json(content, path) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const value = parse(content, errors, { allowTrailingComma: true });
|
|
29
|
+
if (errors.length || !value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
throw new Error(`Invalid JSON configuration: ${path}`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function setJSON(content, path, value) {
|
|
34
|
+
return applyEdits(content, modify(content, path, value, {
|
|
35
|
+
formattingOptions: { insertSpaces: true, tabSize: 2, eol: `
|
|
36
|
+
` }
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
async function destination(root, path) {
|
|
40
|
+
if (!path || path.includes("\\") || path.includes(":") || /[\x00-\x1f]/.test(path) || isAbsolute(path) || path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
41
|
+
throw new Error(`Unsafe destination: ${path}`);
|
|
42
|
+
const target = resolve(root, path);
|
|
43
|
+
const inside = relative(root, target);
|
|
44
|
+
if (inside.startsWith(`..${sep}`) || inside === ".." || isAbsolute(inside))
|
|
45
|
+
throw new Error(`Destination escapes application: ${path}`);
|
|
46
|
+
let current = target;
|
|
47
|
+
while (current !== dirname(root)) {
|
|
48
|
+
try {
|
|
49
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
50
|
+
throw new Error(`Refusing symlink destination: ${path}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code !== "ENOENT")
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (current === root)
|
|
56
|
+
break;
|
|
57
|
+
current = dirname(current);
|
|
58
|
+
}
|
|
59
|
+
return target;
|
|
60
|
+
}
|
|
61
|
+
async function apply(root, changes, overwrite, dryRun) {
|
|
62
|
+
const seen = new Set;
|
|
63
|
+
const prepared = [];
|
|
64
|
+
for (const change of changes) {
|
|
65
|
+
const key = change.path.toLowerCase();
|
|
66
|
+
if (seen.has(key))
|
|
67
|
+
throw new Error(`Duplicate destination: ${change.path}`);
|
|
68
|
+
seen.add(key);
|
|
69
|
+
const target = await destination(root, change.path);
|
|
70
|
+
const before = await read(target);
|
|
71
|
+
if (before === change.content)
|
|
72
|
+
continue;
|
|
73
|
+
const conflict = before !== undefined && !change.replace && !overwrite;
|
|
74
|
+
prepared.push({ ...change, target, conflict, before });
|
|
75
|
+
}
|
|
76
|
+
const conflicts = prepared.filter((change) => change.conflict);
|
|
77
|
+
if (conflicts.length && !dryRun)
|
|
78
|
+
throw new Error(`Existing files differ: ${conflicts.map((change) => change.path).join(", ")}. Review them, then use --overwrite to replace them. No files were written.`);
|
|
79
|
+
for (const change of prepared) {
|
|
80
|
+
console.log(`${change.conflict ? "conflict" : change.before === undefined ? "create" : "update"} ${change.path}`);
|
|
81
|
+
if (!dryRun) {
|
|
82
|
+
await destination(root, change.path);
|
|
83
|
+
await mkdir(dirname(change.target), { recursive: true });
|
|
84
|
+
await writeFile(change.target, change.content);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return prepared;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/config.ts
|
|
91
|
+
import ts from "typescript";
|
|
92
|
+
import postcss from "postcss";
|
|
93
|
+
import { dirname as dirname2, posix, resolve as resolve2 } from "path";
|
|
94
|
+
var DEFAULT_REGISTRY = "https://ergonui.netlify.app";
|
|
95
|
+
var tooling = ["tailwindcss@4.3.3", "@tailwindcss/vite@4.3.3", "@types/node@26.6.6"];
|
|
96
|
+
function registryOrigin(value) {
|
|
97
|
+
const url = new URL(value);
|
|
98
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
99
|
+
throw new Error("Registry must be an HTTP(S) origin/base path without credentials, query or hash.");
|
|
100
|
+
return url.toString().replace(/\/$/, "");
|
|
101
|
+
}
|
|
102
|
+
function viteConfiguration(content, source) {
|
|
103
|
+
const file = ts.createSourceFile("vite.config.ts", content, ts.ScriptTarget.Latest, true);
|
|
104
|
+
const exports = file.statements.filter(ts.isExportAssignment);
|
|
105
|
+
if (exports.length !== 1 || exports[0].isExportEquals)
|
|
106
|
+
throw new Error("Vite configuration must have one default export. Configure Tailwind and the @ alias manually before retrying.");
|
|
107
|
+
if (content.includes("// ErgonUI configuration adapter"))
|
|
108
|
+
return content;
|
|
109
|
+
let hasAlias = false;
|
|
110
|
+
let hasTailwind = false;
|
|
111
|
+
const tailwindImports = new Set;
|
|
112
|
+
for (const node of file.statements) {
|
|
113
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tailwindcss/vite" && node.importClause?.name)
|
|
114
|
+
tailwindImports.add(node.importClause.name.text);
|
|
115
|
+
}
|
|
116
|
+
function visit(node) {
|
|
117
|
+
if (ts.isPropertyAssignment(node) && (ts.isStringLiteral(node.name) || ts.isIdentifier(node.name)) && node.name.text === "@")
|
|
118
|
+
hasAlias = true;
|
|
119
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && tailwindImports.has(node.expression.text))
|
|
120
|
+
hasTailwind = true;
|
|
121
|
+
ts.forEachChild(node, visit);
|
|
122
|
+
}
|
|
123
|
+
visit(file);
|
|
124
|
+
if (hasAlias && hasTailwind)
|
|
125
|
+
return content;
|
|
126
|
+
if (content.includes("__ergon"))
|
|
127
|
+
throw new Error("Reserved __ergon identifiers already exist in Vite config; configure it manually.");
|
|
128
|
+
const declaration = exports[0];
|
|
129
|
+
const imports = [
|
|
130
|
+
"import { defineConfig as __ergonDefineConfig, mergeConfig as __ergonMergeConfig } from 'vite'"
|
|
131
|
+
];
|
|
132
|
+
if (!hasTailwind)
|
|
133
|
+
imports.push("import __ergonTailwind from '@tailwindcss/vite'");
|
|
134
|
+
if (!hasAlias)
|
|
135
|
+
imports.push("import { fileURLToPath as __ergonPath } from 'node:url'");
|
|
136
|
+
const additions = [
|
|
137
|
+
...!hasTailwind ? ["plugins: [__ergonTailwind()]"] : [],
|
|
138
|
+
...!hasAlias ? [
|
|
139
|
+
`resolve: { alias: { '@': __ergonPath(new URL(${JSON.stringify(`./${source}`)}, import.meta.url)) } }`
|
|
140
|
+
] : []
|
|
141
|
+
];
|
|
142
|
+
const replacement = `// ErgonUI configuration adapter
|
|
143
|
+
const __ergonOriginal = ${declaration.expression.getText(file)}
|
|
144
|
+
async function __ergonResolve(__ergonConfig: import('vite').UserConfigExport, __ergonEnv: import('vite').ConfigEnv) {
|
|
145
|
+
return typeof __ergonConfig === 'function' ? await __ergonConfig(__ergonEnv) : await __ergonConfig
|
|
146
|
+
}
|
|
147
|
+
export default __ergonDefineConfig(async (__ergonEnv) => __ergonMergeConfig(
|
|
148
|
+
await __ergonResolve(__ergonOriginal, __ergonEnv),
|
|
149
|
+
{ ${additions.join(", ")} },
|
|
150
|
+
))`;
|
|
151
|
+
return `${imports.join(`
|
|
152
|
+
`)}
|
|
153
|
+
${content.slice(0, declaration.getStart(file))}${replacement}${content.slice(declaration.end)}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
async function configure(root, registry, initialize) {
|
|
157
|
+
const changes = [];
|
|
158
|
+
const pkgContent = await read(resolve2(root, "package.json"));
|
|
159
|
+
if (!pkgContent)
|
|
160
|
+
throw new Error("Run inside a Vite + React application, or use ergonui create <directory>.");
|
|
161
|
+
const pkg = json(pkgContent, "package.json");
|
|
162
|
+
if (!pkg.dependencies?.react?.match(/(?:19|\*)/) || !(pkg.devDependencies?.vite || pkg.dependencies?.vite))
|
|
163
|
+
throw new Error("Version 1 supports Vite + React 19 applications. Keep other stacks on manual installation.");
|
|
164
|
+
const existing = await read(resolve2(root, "components.json"));
|
|
165
|
+
if (!existing && !initialize)
|
|
166
|
+
throw new Error("Run ergonui init before adding components.");
|
|
167
|
+
const config = existing ? json(existing, "components.json") : {};
|
|
168
|
+
const configPath = config.registries?.["@ergonui"];
|
|
169
|
+
const savedRegistry = typeof configPath === "string" && configPath.endsWith("/r/{name}.json") ? configPath.slice(0, -"/r/{name}.json".length) : undefined;
|
|
170
|
+
const origin = registryOrigin(registry ?? savedRegistry ?? DEFAULT_REGISTRY);
|
|
171
|
+
let source = "src";
|
|
172
|
+
const aliasesFound = [];
|
|
173
|
+
for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
|
|
174
|
+
const before = await read(resolve2(root, name));
|
|
175
|
+
if (!before) {
|
|
176
|
+
if (name === "tsconfig.json")
|
|
177
|
+
throw new Error("A TypeScript Vite project with tsconfig.json is required.");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const current = json(before, name);
|
|
181
|
+
const alias = current.compilerOptions?.paths?.["@/*"];
|
|
182
|
+
if (alias) {
|
|
183
|
+
if (!Array.isArray(alias) || alias.length !== 1 || !alias[0].endsWith("/*"))
|
|
184
|
+
throw new Error(`Unsupported @/* alias in ${name}`);
|
|
185
|
+
aliasesFound.push(alias[0].replace(/^\.\//, "").slice(0, -2));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (new Set(aliasesFound).size > 1)
|
|
189
|
+
throw new Error("The @/* aliases in your TypeScript configurations disagree.");
|
|
190
|
+
source = aliasesFound[0] ?? source;
|
|
191
|
+
await destination(root, `${source}/main.tsx`);
|
|
192
|
+
const aliases = {
|
|
193
|
+
ui: config.aliases?.ui ?? "@/components/ui",
|
|
194
|
+
lib: config.aliases?.lib ?? "@/lib",
|
|
195
|
+
utils: config.aliases?.utils ?? "@/lib/utils"
|
|
196
|
+
};
|
|
197
|
+
for (const alias of Object.values(aliases)) {
|
|
198
|
+
if (typeof alias !== "string" || !alias.startsWith("@/"))
|
|
199
|
+
throw new Error("Version 1 supports aliases rooted at @/; preserve your setup using manual installation for other alias prefixes.");
|
|
200
|
+
await destination(root, `${source}/${alias.slice(2)}`);
|
|
201
|
+
}
|
|
202
|
+
let css = config.tailwind?.css;
|
|
203
|
+
const mainPath = `${source}/main.tsx`;
|
|
204
|
+
const main = await read(resolve2(root, mainPath));
|
|
205
|
+
if (initialize && !main)
|
|
206
|
+
throw new Error(`Expected ${mainPath}. Version 1 supports the standard Vite React TypeScript entry; configure other entries manually.`);
|
|
207
|
+
if (!css && main) {
|
|
208
|
+
const file = ts.createSourceFile(mainPath, main, ts.ScriptTarget.Latest, true);
|
|
209
|
+
const imported = file.statements.find((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".") && node.moduleSpecifier.text.endsWith(".css"));
|
|
210
|
+
if (imported)
|
|
211
|
+
css = posix.normalize(posix.join(source, imported.moduleSpecifier.text));
|
|
212
|
+
}
|
|
213
|
+
css ??= `${source}/index.css`;
|
|
214
|
+
await destination(root, css);
|
|
215
|
+
if (initialize) {
|
|
216
|
+
for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
|
|
217
|
+
const before = await read(resolve2(root, name));
|
|
218
|
+
if (before)
|
|
219
|
+
changes.push({
|
|
220
|
+
path: name,
|
|
221
|
+
content: setJSON(before, ["compilerOptions", "paths", "@/*"], [`./${source}/*`]),
|
|
222
|
+
replace: true
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const vitePath = await read(resolve2(root, "vite.config.ts")) !== undefined ? "vite.config.ts" : "vite.config.mts";
|
|
226
|
+
const vite = await read(resolve2(root, vitePath));
|
|
227
|
+
if (!vite)
|
|
228
|
+
throw new Error("A vite.config.ts or vite.config.mts default export is required.");
|
|
229
|
+
changes.push({ path: vitePath, content: viteConfiguration(vite, source), replace: true });
|
|
230
|
+
if (main && !main.includes(`./${posix.relative(source, css)}`)) {
|
|
231
|
+
changes.push({
|
|
232
|
+
path: mainPath,
|
|
233
|
+
content: `import ${JSON.stringify(`./${posix.relative(dirname2(mainPath), css)}`)}
|
|
234
|
+
${main}`,
|
|
235
|
+
replace: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
let updated = existing ?? `{}
|
|
239
|
+
`;
|
|
240
|
+
const defaults = {
|
|
241
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
242
|
+
style: "new-york",
|
|
243
|
+
rsc: false,
|
|
244
|
+
tsx: true,
|
|
245
|
+
tailwind: { config: "", css, baseColor: "neutral", cssVariables: true },
|
|
246
|
+
aliases: {
|
|
247
|
+
components: "@/components",
|
|
248
|
+
ui: aliases.ui,
|
|
249
|
+
lib: aliases.lib,
|
|
250
|
+
utils: aliases.utils,
|
|
251
|
+
hooks: "@/hooks"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
255
|
+
if (config[key] === undefined)
|
|
256
|
+
updated = setJSON(updated, [key], value);
|
|
257
|
+
else if (typeof value === "object" && value !== null) {
|
|
258
|
+
for (const [nested, v] of Object.entries(value))
|
|
259
|
+
if (config[key]?.[nested] === undefined)
|
|
260
|
+
updated = setJSON(updated, [key, nested], v);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
updated = setJSON(updated, ["registries", "@ergonui"], `${origin}/r/{name}.json`);
|
|
264
|
+
changes.push({ path: "components.json", content: updated, replace: true });
|
|
265
|
+
}
|
|
266
|
+
return { changes, source, css, aliases, registry: origin, config };
|
|
267
|
+
}
|
|
268
|
+
function stylesheet(content, vars, overwrite) {
|
|
269
|
+
const root = postcss.parse(content);
|
|
270
|
+
for (const name of ["tailwindcss", "tw-animate-css"]) {
|
|
271
|
+
let exists = false;
|
|
272
|
+
root.walkAtRules("import", (node) => {
|
|
273
|
+
if (node.params.match(/^['"]([^'"]+)['"]/)?.[1] === name)
|
|
274
|
+
exists = true;
|
|
275
|
+
});
|
|
276
|
+
if (!exists)
|
|
277
|
+
root.prepend(postcss.atRule({ name: "import", params: JSON.stringify(name) }));
|
|
278
|
+
}
|
|
279
|
+
let dark = false;
|
|
280
|
+
root.walkAtRules("custom-variant", (node) => {
|
|
281
|
+
if (node.params.startsWith("dark "))
|
|
282
|
+
dark = true;
|
|
283
|
+
});
|
|
284
|
+
if (!dark)
|
|
285
|
+
root.append(postcss.atRule({ name: "custom-variant", params: "dark (&:is(.dark *))" }));
|
|
286
|
+
let theme;
|
|
287
|
+
root.walkAtRules("theme", (node) => {
|
|
288
|
+
if (node.params === "inline")
|
|
289
|
+
theme = node;
|
|
290
|
+
});
|
|
291
|
+
if (!theme) {
|
|
292
|
+
theme = postcss.atRule({ name: "theme", params: "inline" });
|
|
293
|
+
root.append(theme);
|
|
294
|
+
}
|
|
295
|
+
const bound = new Set;
|
|
296
|
+
root.walkAtRules("theme", (node) => {
|
|
297
|
+
if (node.params === "inline")
|
|
298
|
+
node.walkDecls((declaration) => {
|
|
299
|
+
bound.add(declaration.prop);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
for (const key of Object.keys(vars.light ?? {})) {
|
|
303
|
+
if (key === "radius")
|
|
304
|
+
continue;
|
|
305
|
+
const prop = `--color-${key}`;
|
|
306
|
+
if (!bound.has(prop))
|
|
307
|
+
theme.append({ prop, value: `var(--${key})` });
|
|
308
|
+
}
|
|
309
|
+
for (const [size, value] of Object.entries({
|
|
310
|
+
sm: "calc(var(--radius) - 4px)",
|
|
311
|
+
md: "calc(var(--radius) - 2px)",
|
|
312
|
+
lg: "var(--radius)",
|
|
313
|
+
xl: "calc(var(--radius) + 4px)"
|
|
314
|
+
}))
|
|
315
|
+
if (!bound.has(`--radius-${size}`))
|
|
316
|
+
theme.append({ prop: `--radius-${size}`, value });
|
|
317
|
+
for (const [mode, values] of Object.entries(vars)) {
|
|
318
|
+
if (!["light", "dark"].includes(mode))
|
|
319
|
+
continue;
|
|
320
|
+
const selector = mode === "light" ? ":root" : ".dark";
|
|
321
|
+
let rule;
|
|
322
|
+
root.each((node) => {
|
|
323
|
+
if (node.type === "rule" && node.selector === selector)
|
|
324
|
+
rule = node;
|
|
325
|
+
});
|
|
326
|
+
if (!rule) {
|
|
327
|
+
rule = postcss.rule({ selector });
|
|
328
|
+
root.append(rule);
|
|
329
|
+
}
|
|
330
|
+
for (const [key, value] of Object.entries(values)) {
|
|
331
|
+
let exists = false;
|
|
332
|
+
root.walkRules(selector, (candidate) => {
|
|
333
|
+
candidate.walkDecls(`--${key}`, (declaration) => {
|
|
334
|
+
exists = true;
|
|
335
|
+
if (overwrite)
|
|
336
|
+
declaration.value = value;
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
if (!exists)
|
|
340
|
+
rule.append({ prop: `--${key}`, value });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!content.includes("/* ErgonUI base styles */"))
|
|
344
|
+
root.append(postcss.comment({ text: "ErgonUI base styles" }), postcss.parse("@layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; } }"));
|
|
345
|
+
return root.toString().trimEnd() + `
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/engine.ts
|
|
350
|
+
function componentNames(names) {
|
|
351
|
+
return [
|
|
352
|
+
...new Set(names.flatMap((name) => name.split(",")).filter(Boolean).map((name) => {
|
|
353
|
+
const normalized = name.toLowerCase().replace(/\+$/, "-plus");
|
|
354
|
+
if (!/^[a-z][a-z0-9-]*$/.test(normalized))
|
|
355
|
+
throw new Error(`Invalid component name: ${name}`);
|
|
356
|
+
return normalized;
|
|
357
|
+
}))
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
async function registryItem(origin, name) {
|
|
361
|
+
const url = `${registryOrigin(origin)}/r/${name}.json`;
|
|
362
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(20000) });
|
|
363
|
+
if (!response.ok)
|
|
364
|
+
throw new Error(`Registry returned ${response.status} for ${name}: ${url}. Check that this component is deployed.`);
|
|
365
|
+
let item;
|
|
366
|
+
try {
|
|
367
|
+
item = await response.json();
|
|
368
|
+
} catch {
|
|
369
|
+
throw new Error(`Registry did not return JSON for ${name}: ${url}`);
|
|
370
|
+
}
|
|
371
|
+
if (item.name !== name || !["registry:ui", "registry:theme"].includes(item.type))
|
|
372
|
+
throw new Error(`Invalid registry entry: ${name}`);
|
|
373
|
+
if (item.type === "registry:ui" && (!Array.isArray(item.files) || !item.files.length))
|
|
374
|
+
throw new Error(`Registry source is missing for ${name}`);
|
|
375
|
+
if (item.type === "registry:theme" && (!item.cssVars?.light || !item.cssVars?.dark))
|
|
376
|
+
throw new Error(`Theme tokens are missing for ${name}`);
|
|
377
|
+
for (const dependency of item.dependencies ?? []) {
|
|
378
|
+
if (typeof dependency !== "string" || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*@\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(dependency))
|
|
379
|
+
throw new Error(`Registry dependency must be a pinned npm version: ${dependency}`);
|
|
380
|
+
}
|
|
381
|
+
return item;
|
|
382
|
+
}
|
|
383
|
+
function sourceFile(file, setup) {
|
|
384
|
+
if (typeof file.path !== "string" || typeof file.content !== "string" || file.path.includes("\\") || file.path.split("/").some((p) => p === ".." || p === "." || !p))
|
|
385
|
+
throw new Error("Invalid registry source file");
|
|
386
|
+
const { source, aliases } = setup;
|
|
387
|
+
let path;
|
|
388
|
+
if (/^src\/components\/ui\/[a-zA-Z0-9-]+\.tsx?$/.test(file.path))
|
|
389
|
+
path = `${source}/${aliases.ui.slice(2)}/${file.path.split("/").at(-1)}`;
|
|
390
|
+
else if (file.path === "src/lib/utils.ts")
|
|
391
|
+
path = `${source}/${aliases.utils.slice(2)}.ts`;
|
|
392
|
+
else if (/^src\/lib\/[a-zA-Z0-9-]+\.tsx?$/.test(file.path))
|
|
393
|
+
path = `${source}/${aliases.lib.slice(2)}/${file.path.split("/").at(-1)}`;
|
|
394
|
+
else if (/^LICENSES\/[a-zA-Z0-9._-]+$/.test(file.path))
|
|
395
|
+
path = file.path;
|
|
396
|
+
else
|
|
397
|
+
throw new Error(`Unsupported registry destination: ${file.path}`);
|
|
398
|
+
const content = file.path.startsWith("src/") ? file.content.replace(/(['"])@\/components\/ui\//g, `$1${aliases.ui}/`).replace(/(['"])@\/lib\/utils(['"])/g, `$1${aliases.utils}$2`).replace(/(['"])@\/lib\//g, `$1${aliases.lib}/`) : file.content;
|
|
399
|
+
return { path, content };
|
|
400
|
+
}
|
|
401
|
+
async function planEntries(root, setup, names, overwrite) {
|
|
402
|
+
const items = await Promise.all(names.map((name) => registryItem(setup.registry, name)));
|
|
403
|
+
const changes = new Map;
|
|
404
|
+
const dependencies = new Set;
|
|
405
|
+
for (const item of items) {
|
|
406
|
+
for (const dependency of item.dependencies ?? [])
|
|
407
|
+
dependencies.add(dependency);
|
|
408
|
+
for (const file of item.files ?? []) {
|
|
409
|
+
const change = sourceFile(file, setup);
|
|
410
|
+
await destination(root, change.path);
|
|
411
|
+
const key = change.path.toLowerCase();
|
|
412
|
+
const previous = changes.get(key);
|
|
413
|
+
if (previous && (previous.path !== change.path || previous.content !== change.content))
|
|
414
|
+
throw new Error(`Registry entries disagree about ${change.path}`);
|
|
415
|
+
changes.set(key, change);
|
|
416
|
+
}
|
|
417
|
+
if (item.cssVars) {
|
|
418
|
+
const before = await read(resolve3(root, setup.css)) ?? "";
|
|
419
|
+
changes.set(setup.css.toLowerCase(), {
|
|
420
|
+
path: setup.css,
|
|
421
|
+
content: stylesheet(before, item.cssVars, overwrite),
|
|
422
|
+
replace: true
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const versions = new Map;
|
|
427
|
+
for (const dependency of dependencies) {
|
|
428
|
+
const name = dependency.slice(0, dependency.lastIndexOf("@"));
|
|
429
|
+
if (versions.has(name) && versions.get(name) !== dependency)
|
|
430
|
+
throw new Error(`Registry dependencies disagree on the version of ${name}`);
|
|
431
|
+
versions.set(name, dependency);
|
|
432
|
+
}
|
|
433
|
+
return { changes: [...changes.values()], dependencies: [...dependencies] };
|
|
434
|
+
}
|
|
435
|
+
function command(root, args) {
|
|
436
|
+
console.log(`bun ${args.join(" ")}`);
|
|
437
|
+
const result = spawnSync(process.execPath, args, { cwd: root, stdio: "inherit", shell: false });
|
|
438
|
+
if (result.error)
|
|
439
|
+
throw result.error;
|
|
440
|
+
if (result.status !== 0)
|
|
441
|
+
throw new Error(`bun ${args[0]} failed. Files are in place; resolve the dependency error and run bun install.`);
|
|
442
|
+
}
|
|
443
|
+
function declareDependencies(content, runtime, dev = []) {
|
|
444
|
+
for (const [section, dependencies] of [
|
|
445
|
+
["dependencies", runtime],
|
|
446
|
+
["devDependencies", dev]
|
|
447
|
+
]) {
|
|
448
|
+
for (const dependency of dependencies) {
|
|
449
|
+
const separator = dependency.lastIndexOf("@");
|
|
450
|
+
const name = dependency.slice(0, separator);
|
|
451
|
+
content = setJSON(content, [section, name], dependency.slice(separator + 1));
|
|
452
|
+
const other = section === "dependencies" ? "devDependencies" : "dependencies";
|
|
453
|
+
if (json(content, "package.json")[other]?.[name] !== undefined)
|
|
454
|
+
content = setJSON(content, [other, name], undefined);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return content;
|
|
458
|
+
}
|
|
459
|
+
async function add(root, options, initialize) {
|
|
460
|
+
const setup = await configure(root, options.registry, initialize);
|
|
461
|
+
const names = componentNames(options.components ?? []);
|
|
462
|
+
const includeTheme = names.includes("theme") || initialize;
|
|
463
|
+
const entries = await planEntries(root, setup, [...new Set([...names, ...includeTheme ? ["theme"] : []])], !!options.overwrite && names.includes("theme") && !options.keepTheme);
|
|
464
|
+
const deps = [
|
|
465
|
+
...new Set([...entries.dependencies, ...initialize ? ["tw-animate-css@1.4.0"] : []])
|
|
466
|
+
];
|
|
467
|
+
const manifest = declareDependencies(await read(resolve3(root, "package.json")), deps, initialize ? tooling : []);
|
|
468
|
+
await apply(root, [
|
|
469
|
+
...setup.changes,
|
|
470
|
+
...entries.changes,
|
|
471
|
+
{ path: "package.json", content: manifest, replace: true }
|
|
472
|
+
], !!options.overwrite, !!options.dryRun);
|
|
473
|
+
if (options.dryRun)
|
|
474
|
+
console.log(`Dependencies: ${[...deps, ...initialize ? tooling : []].join(", ") || "none"}`);
|
|
475
|
+
else if (options.install !== false)
|
|
476
|
+
command(root, ["install"]);
|
|
477
|
+
else
|
|
478
|
+
console.log("Dependencies recorded in package.json. Run bun install before using the components.");
|
|
479
|
+
return { root, names };
|
|
480
|
+
}
|
|
481
|
+
async function run(commandName, positionals, options = {}) {
|
|
482
|
+
if (commandName === "create") {
|
|
483
|
+
if (positionals.length !== 1)
|
|
484
|
+
throw new Error("Usage: ergonui create <directory> [--components line-chart,pie-chart]");
|
|
485
|
+
const parent = options.cwd ?? process.cwd();
|
|
486
|
+
const root = resolve3(parent, positionals[0]);
|
|
487
|
+
if (await read(resolve3(root, "package.json")) !== undefined)
|
|
488
|
+
throw new Error("Target already contains an application. Use init inside it.");
|
|
489
|
+
try {
|
|
490
|
+
await realpath(root);
|
|
491
|
+
throw new Error("Create requires a new directory; existing directories are preserved.");
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (error.code !== "ENOENT")
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
const origin = registryOrigin(options.registry ?? DEFAULT_REGISTRY);
|
|
497
|
+
const names = componentNames(options.components?.length ? options.components : ["button"]);
|
|
498
|
+
const setup = {
|
|
499
|
+
changes: [],
|
|
500
|
+
source: "src",
|
|
501
|
+
css: "src/index.css",
|
|
502
|
+
aliases: { ui: "@/components/ui", lib: "@/lib", utils: "@/lib/utils" },
|
|
503
|
+
registry: origin,
|
|
504
|
+
config: {}
|
|
505
|
+
};
|
|
506
|
+
const entries = await planEntries(root, setup, [...new Set(["theme", ...names])], false);
|
|
507
|
+
const template = fileURLToPath(new URL("./template/", import.meta.url));
|
|
508
|
+
const changes = [];
|
|
509
|
+
async function walk(path = "") {
|
|
510
|
+
for (const entry of await readdir(resolve3(template, path), { withFileTypes: true })) {
|
|
511
|
+
const name = path ? `${path}/${entry.name}` : entry.name;
|
|
512
|
+
if (entry.isDirectory())
|
|
513
|
+
await walk(name);
|
|
514
|
+
else {
|
|
515
|
+
let content = await read(resolve3(template, name));
|
|
516
|
+
if (name === "package.json") {
|
|
517
|
+
const pkg = json(content, name);
|
|
518
|
+
pkg.name = basename(root).toLowerCase().replace(/[^a-z0-9-]/g, "-") || "ergonui-app";
|
|
519
|
+
content = JSON.stringify(pkg, null, 2) + `
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
changes.push({ path: name === "gitignore" ? ".gitignore" : name, content });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
await walk();
|
|
527
|
+
const configured = {
|
|
528
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
529
|
+
style: "new-york",
|
|
530
|
+
rsc: false,
|
|
531
|
+
tsx: true,
|
|
532
|
+
tailwind: { config: "", css: setup.css, baseColor: "neutral", cssVariables: true },
|
|
533
|
+
aliases: { components: "@/components", ...setup.aliases, hooks: "@/hooks" },
|
|
534
|
+
registries: { "@ergonui": `${origin}/r/{name}.json` }
|
|
535
|
+
};
|
|
536
|
+
changes.push({ path: "components.json", content: JSON.stringify(configured, null, 2) + `
|
|
537
|
+
` });
|
|
538
|
+
const all = new Map(changes.map((change) => [change.path, change]));
|
|
539
|
+
for (const change of entries.changes)
|
|
540
|
+
all.set(change.path, change);
|
|
541
|
+
all.get("package.json").content = declareDependencies(all.get("package.json").content, entries.dependencies);
|
|
542
|
+
await apply(root, [...all.values()], false, !!options.dryRun);
|
|
543
|
+
if (!options.dryRun && options.install !== false) {
|
|
544
|
+
command(root, ["install"]);
|
|
545
|
+
} else if (!options.dryRun)
|
|
546
|
+
console.log("Dependencies recorded in package.json. Run bun install before starting the application.");
|
|
547
|
+
console.log(`${options.dryRun ? "Dry run: no files written." : "Created application."}
|
|
548
|
+
${root}
|
|
549
|
+
Next: cd ${JSON.stringify(positionals[0])} and bun run dev`);
|
|
550
|
+
return { root, names };
|
|
551
|
+
}
|
|
552
|
+
if (!["init", "add"].includes(commandName))
|
|
553
|
+
throw new Error(`Unknown command: ${commandName}`);
|
|
554
|
+
if (commandName === "init" && positionals.length)
|
|
555
|
+
throw new Error("Pass initial components with --components, not positional arguments, to init.");
|
|
556
|
+
const root = await realpath(resolve3(options.cwd ?? process.cwd()));
|
|
557
|
+
const components = [...options.components ?? [], ...commandName === "add" ? positionals : []];
|
|
558
|
+
if (commandName === "add" && !components.length)
|
|
559
|
+
throw new Error("Usage: ergonui add <component...>");
|
|
560
|
+
const result = await add(root, { ...options, components }, commandName === "init");
|
|
561
|
+
console.log(options.dryRun ? "Dry run: no files or dependencies changed." : "ErgonUI source is ready.");
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/index.ts
|
|
566
|
+
var help = `ErgonUI CLI 0.1.0 \u2014 Bun + Vite + React 19 + Tailwind 4
|
|
567
|
+
|
|
568
|
+
Usage:
|
|
569
|
+
ergonui create my-app --components line-chart,pie-chart
|
|
570
|
+
ergonui init
|
|
571
|
+
ergonui add button table-plus
|
|
572
|
+
|
|
573
|
+
Options:
|
|
574
|
+
--components <names> Comma-separated component names
|
|
575
|
+
--cwd, -c <path> Application/parent directory
|
|
576
|
+
--registry <origin> Override the hosted ErgonUI registry (e.g. local preview)
|
|
577
|
+
--overwrite Replace differing component files and explicitly selected theme values
|
|
578
|
+
--keep-theme On init, preserve existing tokens while filling missing bindings/tokens
|
|
579
|
+
--dry-run Preview changes without writing or installing
|
|
580
|
+
--no-install Write files without installing npm dependencies
|
|
581
|
+
--help, -h Show help
|
|
582
|
+
--version, -v Show version
|
|
583
|
+
|
|
584
|
+
Components are editable source with retained shadcn/ui and community attribution.
|
|
585
|
+
`;
|
|
586
|
+
try {
|
|
587
|
+
const { values, positionals } = parseArgs({
|
|
588
|
+
args: process.argv.slice(2),
|
|
589
|
+
allowPositionals: true,
|
|
590
|
+
options: {
|
|
591
|
+
components: { type: "string" },
|
|
592
|
+
cwd: { type: "string", short: "c" },
|
|
593
|
+
registry: { type: "string" },
|
|
594
|
+
overwrite: { type: "boolean" },
|
|
595
|
+
"keep-theme": { type: "boolean" },
|
|
596
|
+
"dry-run": { type: "boolean" },
|
|
597
|
+
"no-install": { type: "boolean" },
|
|
598
|
+
help: { type: "boolean", short: "h" },
|
|
599
|
+
version: { type: "boolean", short: "v" }
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
if (values.version)
|
|
603
|
+
console.log("0.1.0");
|
|
604
|
+
else if (values.help || !positionals.length)
|
|
605
|
+
console.log(help);
|
|
606
|
+
else
|
|
607
|
+
await run(positionals[0], positionals.slice(1), {
|
|
608
|
+
cwd: values.cwd,
|
|
609
|
+
registry: values.registry,
|
|
610
|
+
components: values.components?.split(","),
|
|
611
|
+
overwrite: values.overwrite,
|
|
612
|
+
dryRun: values["dry-run"],
|
|
613
|
+
install: !values["no-install"],
|
|
614
|
+
keepTheme: values["keep-theme"]
|
|
615
|
+
});
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error(`ErgonUI: ${error instanceof Error ? error.message : String(error)}`);
|
|
618
|
+
process.exitCode = 1;
|
|
619
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ErgonUI application</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ergonui-app",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "19.3.1",
|
|
13
|
+
"react-dom": "19.3.1"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@tailwindcss/vite": "4.3.3",
|
|
17
|
+
"@types/node": "26.6.6",
|
|
18
|
+
"@types/react": "19.3.0",
|
|
19
|
+
"@types/react-dom": "19.3.0",
|
|
20
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
21
|
+
"tailwindcss": "4.3.3",
|
|
22
|
+
"tw-animate-css": "1.4.0",
|
|
23
|
+
"typescript": "7.0.2",
|
|
24
|
+
"vite": "8.3.5"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function App() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="mx-auto max-w-3xl space-y-6 p-8">
|
|
4
|
+
<h1 className="text-3xl font-semibold">Make yourself at home.</h1>
|
|
5
|
+
<p className="text-muted-foreground">
|
|
6
|
+
Your ErgonUI application is ready. Import your selected components from @/components/ui.
|
|
7
|
+
</p>
|
|
8
|
+
<a className="text-primary underline" href="https://ergonui.netlify.app/#/components">
|
|
9
|
+
Explore components and usage examples
|
|
10
|
+
</a>
|
|
11
|
+
</main>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"paths": { "@/*": ["./src/*"] }
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
|
|
9
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ergonui/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create Bun + Vite + React applications and install editable ErgonUI components.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ergonui": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ganonym741/ergon-shadcn-ui.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://ergonui.netlify.app",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.4.4"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "bun build.ts",
|
|
30
|
+
"test": "bun run build && bun test test",
|
|
31
|
+
"check": "tsc -p tsconfig.json",
|
|
32
|
+
"prepack": "bun run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"jsonc-parser": "3.3.1",
|
|
36
|
+
"postcss": "8.5.6",
|
|
37
|
+
"typescript": "5.9.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "1.4.4"
|
|
41
|
+
}
|
|
42
|
+
}
|