stera-ui 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/dist/index.d.ts +1 -0
- package/dist/index.js +665 -0
- package/dist/registry/index.d.ts +32 -0
- package/dist/registry/index.js +59 -0
- package/dist/schema/index.d.ts +191 -0
- package/dist/schema/index.js +45 -0
- package/dist/utils/index.d.ts +103 -0
- package/dist/utils/index.js +361 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chaz Giese
|
|
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/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/add.ts
|
|
8
|
+
import path6 from "path";
|
|
9
|
+
|
|
10
|
+
// src/utils/resolve-config.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var CONFIG_FILE = "components.json";
|
|
14
|
+
function migrateConfig(raw) {
|
|
15
|
+
if (!("version" in raw) && "tailwind" in raw) {
|
|
16
|
+
const legacy = raw;
|
|
17
|
+
return {
|
|
18
|
+
version: 1,
|
|
19
|
+
css: legacy.tailwind.css || "src/styles/globals.css",
|
|
20
|
+
aliases: legacy.aliases
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
function findConfigPath(cwd) {
|
|
26
|
+
let dir = path.resolve(cwd);
|
|
27
|
+
while (true) {
|
|
28
|
+
const configPath = path.join(dir, CONFIG_FILE);
|
|
29
|
+
if (fs.existsSync(configPath)) return configPath;
|
|
30
|
+
const parent = path.dirname(dir);
|
|
31
|
+
if (parent === dir) return null;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function loadConfig(cwd) {
|
|
36
|
+
const configPath = findConfigPath(cwd);
|
|
37
|
+
if (!configPath) return null;
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
40
|
+
return migrateConfig(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/registry/index.ts
|
|
47
|
+
var REGISTRY_URL = process.env.REGISTRY_URL ?? "https://ui.stera.site/r";
|
|
48
|
+
var _indexCache = null;
|
|
49
|
+
async function fetchJSON(url) {
|
|
50
|
+
const res = await fetch(url);
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
53
|
+
}
|
|
54
|
+
return res.json();
|
|
55
|
+
}
|
|
56
|
+
async function fetchRegistryIndex() {
|
|
57
|
+
if (_indexCache) return _indexCache;
|
|
58
|
+
const data = await fetchJSON(
|
|
59
|
+
`${REGISTRY_URL}/index.json`
|
|
60
|
+
);
|
|
61
|
+
_indexCache = data.items;
|
|
62
|
+
return _indexCache;
|
|
63
|
+
}
|
|
64
|
+
async function fetchRegistryItem(name) {
|
|
65
|
+
return fetchJSON(`${REGISTRY_URL}/${name}.json`);
|
|
66
|
+
}
|
|
67
|
+
async function getComponent(name) {
|
|
68
|
+
const index = await fetchRegistryIndex();
|
|
69
|
+
return index.find((item) => item.name === name);
|
|
70
|
+
}
|
|
71
|
+
async function getAllComponents() {
|
|
72
|
+
return fetchRegistryIndex();
|
|
73
|
+
}
|
|
74
|
+
async function resolveDependencies(names) {
|
|
75
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
76
|
+
const queue = [...names];
|
|
77
|
+
while (queue.length > 0) {
|
|
78
|
+
const name = queue.shift();
|
|
79
|
+
if (resolved.has(name)) continue;
|
|
80
|
+
let item;
|
|
81
|
+
try {
|
|
82
|
+
item = await fetchRegistryItem(name);
|
|
83
|
+
} catch {
|
|
84
|
+
console.error(`Component "${name}" not found in registry.`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
resolved.set(name, item);
|
|
88
|
+
if (item.registryDependencies) {
|
|
89
|
+
for (const dep of item.registryDependencies) {
|
|
90
|
+
if (!resolved.has(dep)) {
|
|
91
|
+
queue.push(dep);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return Array.from(resolved.values());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/utils/write-files.ts
|
|
100
|
+
import fs2 from "fs";
|
|
101
|
+
import path3 from "path";
|
|
102
|
+
import readline from "readline/promises";
|
|
103
|
+
|
|
104
|
+
// src/utils/resolve-paths.ts
|
|
105
|
+
import path2 from "path";
|
|
106
|
+
import { loadConfig as loadConfig2, createMatchPath } from "tsconfig-paths";
|
|
107
|
+
var TYPE_TO_ALIAS = {
|
|
108
|
+
"registry:ui": "ui",
|
|
109
|
+
"registry:lib": "lib",
|
|
110
|
+
"registry:hook": "hooks",
|
|
111
|
+
"registry:component": "components"
|
|
112
|
+
};
|
|
113
|
+
function resolveAlias(alias, projectRoot) {
|
|
114
|
+
const tsConfig = loadConfig2(projectRoot);
|
|
115
|
+
if (tsConfig.resultType === "success") {
|
|
116
|
+
const match = createMatchPath(tsConfig.absoluteBaseUrl, tsConfig.paths);
|
|
117
|
+
const resolved = match(alias, void 0, () => true, [".ts", ".tsx"]);
|
|
118
|
+
if (resolved) {
|
|
119
|
+
return path2.relative(projectRoot, resolved);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return alias.replace(/^@\//, "");
|
|
123
|
+
}
|
|
124
|
+
function resolveOutputPath(config, projectRoot, file) {
|
|
125
|
+
const alias = TYPE_TO_ALIAS[file.type];
|
|
126
|
+
if (alias) {
|
|
127
|
+
const dir = resolveAlias(config.aliases[alias], projectRoot);
|
|
128
|
+
const filename = path2.basename(file.path);
|
|
129
|
+
return path2.join(projectRoot, dir, filename);
|
|
130
|
+
}
|
|
131
|
+
if (file.type === "registry:style") {
|
|
132
|
+
const cssDir = path2.dirname(config.css);
|
|
133
|
+
const filename = path2.basename(file.path);
|
|
134
|
+
return path2.join(projectRoot, cssDir, filename);
|
|
135
|
+
}
|
|
136
|
+
return path2.join(projectRoot, file.path);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/utils/write-files.ts
|
|
140
|
+
async function confirmOverwrite(relativePath) {
|
|
141
|
+
if (!process.stdin.isTTY) {
|
|
142
|
+
console.log(` Skipping ${relativePath} (already exists, use --overwrite to replace)`);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const rl = readline.createInterface({
|
|
146
|
+
input: process.stdin,
|
|
147
|
+
output: process.stdout
|
|
148
|
+
});
|
|
149
|
+
const answer = await rl.question(` ${relativePath} already exists. Overwrite? (y/N) `);
|
|
150
|
+
rl.close();
|
|
151
|
+
return answer.toLowerCase() === "y";
|
|
152
|
+
}
|
|
153
|
+
async function writeComponentFiles(items, config, projectRoot, options = {}) {
|
|
154
|
+
const written = [];
|
|
155
|
+
const skipped = [];
|
|
156
|
+
for (const item of items) {
|
|
157
|
+
for (const file of item.files) {
|
|
158
|
+
const outputPath = resolveOutputPath(config, projectRoot, file);
|
|
159
|
+
const relativePath = path3.relative(projectRoot, outputPath);
|
|
160
|
+
if (fs2.existsSync(outputPath)) {
|
|
161
|
+
const existing = fs2.readFileSync(outputPath, "utf-8");
|
|
162
|
+
if (existing === file.content) {
|
|
163
|
+
skipped.push(relativePath);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!options.overwrite) {
|
|
167
|
+
const confirmed = await confirmOverwrite(relativePath);
|
|
168
|
+
if (!confirmed) {
|
|
169
|
+
skipped.push(relativePath);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const dir = path3.dirname(outputPath);
|
|
175
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
176
|
+
fs2.writeFileSync(outputPath, file.content ?? "", "utf-8");
|
|
177
|
+
written.push(relativePath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { written, skipped };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/utils/install-deps.ts
|
|
184
|
+
import fs3 from "fs";
|
|
185
|
+
import path4 from "path";
|
|
186
|
+
import { spawn } from "child_process";
|
|
187
|
+
|
|
188
|
+
// src/utils/format.ts
|
|
189
|
+
var BRAND = "\x1B[38;2;166;244;48m";
|
|
190
|
+
var DIM = "\x1B[2m";
|
|
191
|
+
var RESET = "\x1B[0m";
|
|
192
|
+
var enabled = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
193
|
+
var wrap = (code, text) => enabled ? `${code}${text}${RESET}` : text;
|
|
194
|
+
var brand = (text) => wrap(BRAND, text);
|
|
195
|
+
var dim = (text) => wrap(DIM, text);
|
|
196
|
+
var LOGO = brand("\u273B");
|
|
197
|
+
var CHECK = brand("\u2713");
|
|
198
|
+
var CROSS = "\u2717";
|
|
199
|
+
var WARN = "!";
|
|
200
|
+
|
|
201
|
+
// src/utils/spinner.ts
|
|
202
|
+
var FRAMES = ["\u2022", "\u2726", "\u2736", "\u273B"].map(brand);
|
|
203
|
+
var INTERVAL_MS = 150;
|
|
204
|
+
function createSpinner(text) {
|
|
205
|
+
const isTTY = process.stdout.isTTY;
|
|
206
|
+
if (!isTTY) {
|
|
207
|
+
process.stdout.write(` ${text}...
|
|
208
|
+
`);
|
|
209
|
+
return {
|
|
210
|
+
succeed(msg) {
|
|
211
|
+
process.stdout.write(` ${CHECK} ${msg}
|
|
212
|
+
`);
|
|
213
|
+
},
|
|
214
|
+
fail(msg) {
|
|
215
|
+
process.stdout.write(` ${CROSS} ${msg}
|
|
216
|
+
`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
let frameIndex = 0;
|
|
221
|
+
const interval = setInterval(() => {
|
|
222
|
+
process.stdout.write(`\r ${FRAMES[frameIndex % FRAMES.length]} ${text}`);
|
|
223
|
+
frameIndex++;
|
|
224
|
+
}, INTERVAL_MS);
|
|
225
|
+
const clear = () => {
|
|
226
|
+
clearInterval(interval);
|
|
227
|
+
process.stdout.write("\r" + " ".repeat(text.length + 6) + "\r");
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
succeed(msg) {
|
|
231
|
+
clear();
|
|
232
|
+
process.stdout.write(` ${CHECK} ${msg}
|
|
233
|
+
`);
|
|
234
|
+
},
|
|
235
|
+
fail(msg) {
|
|
236
|
+
clear();
|
|
237
|
+
process.stdout.write(` ${CROSS} ${msg}
|
|
238
|
+
`);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/utils/install-deps.ts
|
|
244
|
+
function detectPackageManager(cwd) {
|
|
245
|
+
if (fs3.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
246
|
+
if (fs3.existsSync(path4.join(cwd, "bun.lockb"))) return "bun";
|
|
247
|
+
if (fs3.existsSync(path4.join(cwd, "yarn.lock"))) return "yarn";
|
|
248
|
+
return "npm";
|
|
249
|
+
}
|
|
250
|
+
function getInstallArgs(pm, deps) {
|
|
251
|
+
switch (pm) {
|
|
252
|
+
case "pnpm":
|
|
253
|
+
return ["pnpm", ["add", ...deps]];
|
|
254
|
+
case "yarn":
|
|
255
|
+
return ["yarn", ["add", ...deps]];
|
|
256
|
+
case "bun":
|
|
257
|
+
return ["bun", ["add", ...deps]];
|
|
258
|
+
default:
|
|
259
|
+
return ["npm", ["install", ...deps]];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function installDependencies(deps, cwd) {
|
|
263
|
+
if (deps.length === 0) return;
|
|
264
|
+
const pkgPath = path4.join(cwd, "package.json");
|
|
265
|
+
let existing = /* @__PURE__ */ new Set();
|
|
266
|
+
if (fs3.existsSync(pkgPath)) {
|
|
267
|
+
try {
|
|
268
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
269
|
+
existing = new Set(Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }));
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const toInstall = deps.filter((dep) => !existing.has(dep));
|
|
274
|
+
if (toInstall.length === 0) return;
|
|
275
|
+
const pm = detectPackageManager(cwd);
|
|
276
|
+
const [cmd, args] = getInstallArgs(pm, toInstall);
|
|
277
|
+
const spinner = createSpinner(`Installing with ${pm}`);
|
|
278
|
+
await new Promise((resolve, reject) => {
|
|
279
|
+
const proc = spawn(cmd, args, { cwd, stdio: "pipe" });
|
|
280
|
+
proc.on("close", (code) => {
|
|
281
|
+
if (code === 0) {
|
|
282
|
+
spinner.succeed(`Installed with ${pm}`);
|
|
283
|
+
resolve();
|
|
284
|
+
} else {
|
|
285
|
+
spinner.fail(`Install failed (exit ${code})`);
|
|
286
|
+
reject(new Error(`${pm} exited with code ${code}`));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
proc.on("error", (err) => {
|
|
290
|
+
spinner.fail(`Install failed`);
|
|
291
|
+
reject(err);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/utils/transform.ts
|
|
297
|
+
function applyTransforms(content, ctx, transforms2) {
|
|
298
|
+
return transforms2.reduce((acc, fn) => fn(acc, ctx), content);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/utils/transform-imports.ts
|
|
302
|
+
var DEFAULT_ALIASES = {
|
|
303
|
+
utils: "@/lib/utils",
|
|
304
|
+
ui: "@/components/ui",
|
|
305
|
+
components: "@/components",
|
|
306
|
+
lib: "@/lib",
|
|
307
|
+
hooks: "@/hooks"
|
|
308
|
+
};
|
|
309
|
+
var transformImports = (content, ctx) => {
|
|
310
|
+
if (!ctx.filename.endsWith(".ts") && !ctx.filename.endsWith(".tsx")) {
|
|
311
|
+
return content;
|
|
312
|
+
}
|
|
313
|
+
const { aliases } = ctx.config;
|
|
314
|
+
const rules = [];
|
|
315
|
+
if (aliases.utils !== DEFAULT_ALIASES.utils) {
|
|
316
|
+
rules.push({ type: "exact", from: DEFAULT_ALIASES.utils, to: aliases.utils });
|
|
317
|
+
}
|
|
318
|
+
if (aliases.ui !== DEFAULT_ALIASES.ui) {
|
|
319
|
+
rules.push({ type: "prefix", from: DEFAULT_ALIASES.ui, to: aliases.ui });
|
|
320
|
+
}
|
|
321
|
+
if (aliases.components !== DEFAULT_ALIASES.components) {
|
|
322
|
+
rules.push({ type: "prefix", from: DEFAULT_ALIASES.components, to: aliases.components });
|
|
323
|
+
}
|
|
324
|
+
if (aliases.lib !== DEFAULT_ALIASES.lib) {
|
|
325
|
+
rules.push({ type: "prefix", from: DEFAULT_ALIASES.lib, to: aliases.lib });
|
|
326
|
+
}
|
|
327
|
+
if (aliases.hooks !== DEFAULT_ALIASES.hooks) {
|
|
328
|
+
rules.push({ type: "prefix", from: DEFAULT_ALIASES.hooks, to: aliases.hooks });
|
|
329
|
+
}
|
|
330
|
+
if (rules.length === 0) {
|
|
331
|
+
return content;
|
|
332
|
+
}
|
|
333
|
+
return content.replace(
|
|
334
|
+
/from\s+(["'])([^"']+)\1/g,
|
|
335
|
+
(match, quote, importPath) => {
|
|
336
|
+
for (const rule of rules) {
|
|
337
|
+
if (rule.type === "exact" && importPath === rule.from) {
|
|
338
|
+
return `from ${quote}${rule.to}${quote}`;
|
|
339
|
+
}
|
|
340
|
+
if (rule.type === "prefix" && importPath.startsWith(rule.from + "/")) {
|
|
341
|
+
const rest = importPath.slice(rule.from.length);
|
|
342
|
+
return `from ${quote}${rule.to}${rest}${quote}`;
|
|
343
|
+
}
|
|
344
|
+
if (rule.type === "prefix" && importPath === rule.from) {
|
|
345
|
+
return `from ${quote}${rule.to}${quote}`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return match;
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/utils/detect-globals.ts
|
|
354
|
+
import fs4 from "fs";
|
|
355
|
+
import path5 from "path";
|
|
356
|
+
function hasGlobalsCss(config, projectRoot) {
|
|
357
|
+
const cssPath = path5.resolve(projectRoot, config.css);
|
|
358
|
+
if (!fs4.existsSync(cssPath)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
const steraPath = path5.join(path5.dirname(cssPath), "stera-ui.css");
|
|
362
|
+
return fs4.existsSync(steraPath);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/commands/add.ts
|
|
366
|
+
var transforms = [transformImports];
|
|
367
|
+
async function add(components, options) {
|
|
368
|
+
const cwd = options.cwd ? path6.resolve(options.cwd) : process.cwd();
|
|
369
|
+
for (const name of components) {
|
|
370
|
+
const item = await getComponent(name);
|
|
371
|
+
if (!item) {
|
|
372
|
+
console.error(` Error: Component "${name}" not found in registry.`);
|
|
373
|
+
console.error(` Run "stera-ui list" to see available components.`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const configPath = findConfigPath(cwd);
|
|
378
|
+
if (!configPath) {
|
|
379
|
+
console.error(
|
|
380
|
+
` Error: ${CONFIG_FILE} not found. Run "stera-ui init" first.`
|
|
381
|
+
);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
const config = loadConfig(cwd);
|
|
385
|
+
if (!config) {
|
|
386
|
+
console.error(` Error: Failed to parse ${CONFIG_FILE}.`);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
const projectRoot = path6.dirname(configPath);
|
|
390
|
+
const isAddingGlobals = components.includes("globals");
|
|
391
|
+
if (!isAddingGlobals && !hasGlobalsCss(config, projectRoot)) {
|
|
392
|
+
console.warn(`
|
|
393
|
+
${WARN} Stera design tokens not found in ${config.css}`);
|
|
394
|
+
console.warn(` Components may not render correctly.`);
|
|
395
|
+
console.warn(` Run "stera-ui add globals" to install base styles.
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
398
|
+
const resolved = await resolveDependencies(components);
|
|
399
|
+
for (const item of resolved) {
|
|
400
|
+
for (const file of item.files) {
|
|
401
|
+
if (file.content) {
|
|
402
|
+
file.content = applyTransforms(
|
|
403
|
+
file.content,
|
|
404
|
+
{ config, filename: file.path },
|
|
405
|
+
transforms
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const requested = new Set(components);
|
|
411
|
+
console.log("\n Installing components\n");
|
|
412
|
+
for (const item of resolved) {
|
|
413
|
+
const tag = requested.has(item.name) ? "" : dim(" (dependency)");
|
|
414
|
+
console.log(` \u2192 ${item.name}${tag}`);
|
|
415
|
+
}
|
|
416
|
+
const allNpmDeps = [];
|
|
417
|
+
for (const item of resolved) {
|
|
418
|
+
if (item.dependencies) {
|
|
419
|
+
allNpmDeps.push(...item.dependencies);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const uniqueNpmDeps = [...new Set(allNpmDeps)].sort();
|
|
423
|
+
if (uniqueNpmDeps.length > 0) {
|
|
424
|
+
console.log("\n npm packages\n");
|
|
425
|
+
for (const dep of uniqueNpmDeps) {
|
|
426
|
+
console.log(` \u2192 ${dep}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
console.log("");
|
|
430
|
+
const { written, skipped } = await writeComponentFiles(
|
|
431
|
+
resolved,
|
|
432
|
+
config,
|
|
433
|
+
projectRoot,
|
|
434
|
+
{ overwrite: options.overwrite }
|
|
435
|
+
);
|
|
436
|
+
for (const file of written) {
|
|
437
|
+
console.log(` ${CHECK} ${file}`);
|
|
438
|
+
}
|
|
439
|
+
for (const file of skipped) {
|
|
440
|
+
console.log(` ${dim(`\xB7 ${file} (unchanged)`)}`);
|
|
441
|
+
}
|
|
442
|
+
if (uniqueNpmDeps.length > 0) {
|
|
443
|
+
console.log("");
|
|
444
|
+
await installDependencies(uniqueNpmDeps, projectRoot);
|
|
445
|
+
}
|
|
446
|
+
console.log("");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/commands/init.ts
|
|
450
|
+
import fs6 from "fs";
|
|
451
|
+
import path8 from "path";
|
|
452
|
+
|
|
453
|
+
// src/utils/detect-project.ts
|
|
454
|
+
import fs5 from "fs";
|
|
455
|
+
import path7 from "path";
|
|
456
|
+
function hasFile(cwd, filenames) {
|
|
457
|
+
return filenames.some((f) => fs5.existsSync(path7.join(cwd, f)));
|
|
458
|
+
}
|
|
459
|
+
function findTailwindCssFile(cwd) {
|
|
460
|
+
const candidates = [
|
|
461
|
+
"src/app/globals.css",
|
|
462
|
+
"src/styles/globals.css",
|
|
463
|
+
"src/index.css",
|
|
464
|
+
"app/globals.css",
|
|
465
|
+
"styles/globals.css",
|
|
466
|
+
"index.css"
|
|
467
|
+
];
|
|
468
|
+
for (const candidate of candidates) {
|
|
469
|
+
const fullPath = path7.join(cwd, candidate);
|
|
470
|
+
if (!fs5.existsSync(fullPath)) continue;
|
|
471
|
+
const content = fs5.readFileSync(fullPath, "utf-8");
|
|
472
|
+
if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'") || content.includes("@tailwind base")) {
|
|
473
|
+
return candidate;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function detectProject(cwd) {
|
|
479
|
+
const hasSrc = fs5.existsSync(path7.join(cwd, "src"));
|
|
480
|
+
let framework = "other";
|
|
481
|
+
if (hasFile(cwd, ["next.config.ts", "next.config.js", "next.config.mjs"])) {
|
|
482
|
+
framework = "next";
|
|
483
|
+
} else if (hasFile(cwd, ["vite.config.ts", "vite.config.js", "vite.config.mjs"])) {
|
|
484
|
+
framework = "vite";
|
|
485
|
+
}
|
|
486
|
+
let rsc = false;
|
|
487
|
+
if (framework === "next") {
|
|
488
|
+
const appDir = hasSrc ? "src/app" : "app";
|
|
489
|
+
rsc = fs5.existsSync(path7.join(cwd, appDir));
|
|
490
|
+
}
|
|
491
|
+
const existingCssFile = findTailwindCssFile(cwd);
|
|
492
|
+
return { hasSrc, framework, rsc, existingCssFile };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/commands/init.ts
|
|
496
|
+
function buildConfig(project) {
|
|
497
|
+
let cssPath;
|
|
498
|
+
if (project.existingCssFile) {
|
|
499
|
+
cssPath = project.existingCssFile;
|
|
500
|
+
} else if (project.hasSrc) {
|
|
501
|
+
cssPath = "src/styles/globals.css";
|
|
502
|
+
} else {
|
|
503
|
+
cssPath = "styles/globals.css";
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
$schema: "https://ui.stera.site/schema.json",
|
|
507
|
+
version: 1,
|
|
508
|
+
css: cssPath,
|
|
509
|
+
aliases: {
|
|
510
|
+
components: "@/components",
|
|
511
|
+
utils: "@/lib/utils",
|
|
512
|
+
ui: "@/components/ui",
|
|
513
|
+
lib: "@/lib",
|
|
514
|
+
hooks: "@/hooks"
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function stripTailwindCoreImport(content) {
|
|
519
|
+
return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
|
|
520
|
+
}
|
|
521
|
+
function insertImportLine(filePath, importStatement) {
|
|
522
|
+
const content = fs6.readFileSync(filePath, "utf-8");
|
|
523
|
+
const lines = content.split("\n");
|
|
524
|
+
let lastImportIndex = -1;
|
|
525
|
+
for (let i = 0; i < lines.length; i++) {
|
|
526
|
+
if (lines[i].trim().startsWith("@import")) {
|
|
527
|
+
lastImportIndex = i;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (lastImportIndex >= 0) {
|
|
531
|
+
lines.splice(lastImportIndex + 1, 0, importStatement);
|
|
532
|
+
} else {
|
|
533
|
+
lines.unshift(importStatement);
|
|
534
|
+
}
|
|
535
|
+
fs6.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
536
|
+
}
|
|
537
|
+
async function init(options) {
|
|
538
|
+
const cwd = options.cwd ? path8.resolve(options.cwd) : process.cwd();
|
|
539
|
+
console.log(`
|
|
540
|
+
${LOGO} Stera UI
|
|
541
|
+
`);
|
|
542
|
+
const existing = findConfigPath(cwd);
|
|
543
|
+
if (existing) {
|
|
544
|
+
console.log(` components.json already exists at ${existing}`);
|
|
545
|
+
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const pkgPath = path8.join(cwd, "package.json");
|
|
549
|
+
if (!fs6.existsSync(pkgPath)) {
|
|
550
|
+
console.error(
|
|
551
|
+
" Error: No package.json found. Please run this in a project directory."
|
|
552
|
+
);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
const project = detectProject(cwd);
|
|
556
|
+
const frameworkName = project.framework === "next" ? "Next.js" : project.framework === "vite" ? "Vite" : null;
|
|
557
|
+
const parts = [];
|
|
558
|
+
if (frameworkName) parts.push(frameworkName);
|
|
559
|
+
if (project.hasSrc) parts.push("src/ directory");
|
|
560
|
+
if (parts.length > 0) {
|
|
561
|
+
console.log(` Detected ${parts.join(" project with ")} project
|
|
562
|
+
`);
|
|
563
|
+
}
|
|
564
|
+
const config = buildConfig(project);
|
|
565
|
+
const configPath = path8.join(cwd, CONFIG_FILE);
|
|
566
|
+
fs6.writeFileSync(
|
|
567
|
+
configPath,
|
|
568
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
569
|
+
"utf-8"
|
|
570
|
+
);
|
|
571
|
+
console.log(` ${CHECK} Created components.json`);
|
|
572
|
+
const cssDir = path8.dirname(path8.resolve(cwd, config.css));
|
|
573
|
+
if (project.existingCssFile) {
|
|
574
|
+
if (hasGlobalsCss(config, cwd)) {
|
|
575
|
+
console.log(
|
|
576
|
+
` ${CHECK} Stera already initialised ${dim(`(${path8.join(path8.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
|
|
577
|
+
);
|
|
578
|
+
} else {
|
|
579
|
+
try {
|
|
580
|
+
const globalsItem = await fetchRegistryItem("globals");
|
|
581
|
+
const globalsContent = globalsItem.files[0].content ?? "";
|
|
582
|
+
const steraUiContent = stripTailwindCoreImport(globalsContent);
|
|
583
|
+
const steraUiPath = path8.join(cssDir, "stera-ui.css");
|
|
584
|
+
fs6.mkdirSync(cssDir, { recursive: true });
|
|
585
|
+
fs6.writeFileSync(steraUiPath, steraUiContent, "utf-8");
|
|
586
|
+
console.log(` ${CHECK} Created ${path8.relative(cwd, steraUiPath)}`);
|
|
587
|
+
const existingCssPath = path8.resolve(cwd, project.existingCssFile);
|
|
588
|
+
insertImportLine(existingCssPath, "@import './stera-ui.css';");
|
|
589
|
+
console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const fontsItem = await fetchRegistryItem("fonts");
|
|
595
|
+
const fontsPath = path8.join(cssDir, "fonts.css");
|
|
596
|
+
fs6.mkdirSync(path8.dirname(fontsPath), { recursive: true });
|
|
597
|
+
if (!fs6.existsSync(fontsPath)) {
|
|
598
|
+
fs6.writeFileSync(fontsPath, fontsItem.files[0].content ?? "", "utf-8");
|
|
599
|
+
console.log(` ${CHECK} Created ${path8.relative(cwd, fontsPath)}`);
|
|
600
|
+
} else {
|
|
601
|
+
console.log(` ${dim(path8.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
const resolved = await resolveDependencies(["globals"]);
|
|
607
|
+
const { written } = await writeComponentFiles(resolved, config, cwd);
|
|
608
|
+
for (const file of written) {
|
|
609
|
+
console.log(` ${CHECK} Created ${file}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const globalsItem = await fetchRegistryItem("globals");
|
|
614
|
+
if (globalsItem.dependencies) {
|
|
615
|
+
const deps = [...new Set(globalsItem.dependencies)].sort();
|
|
616
|
+
console.log("");
|
|
617
|
+
await installDependencies(deps, cwd);
|
|
618
|
+
}
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
console.log("");
|
|
622
|
+
console.log(" Next steps:");
|
|
623
|
+
console.log(" stera-ui add <component>");
|
|
624
|
+
console.log("");
|
|
625
|
+
console.log(` ${dim("See the README for font setup instructions.")}`);
|
|
626
|
+
console.log("");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/commands/list.ts
|
|
630
|
+
async function list() {
|
|
631
|
+
const items = await getAllComponents();
|
|
632
|
+
const groups = /* @__PURE__ */ new Map();
|
|
633
|
+
for (const item of items) {
|
|
634
|
+
const type = item.type.replace("registry:", "");
|
|
635
|
+
if (!groups.has(type)) groups.set(type, []);
|
|
636
|
+
groups.get(type).push(item);
|
|
637
|
+
}
|
|
638
|
+
console.log(`
|
|
639
|
+
${LOGO} Available components
|
|
640
|
+
`);
|
|
641
|
+
for (const [type, groupItems] of groups) {
|
|
642
|
+
console.log(` ${type}`);
|
|
643
|
+
for (const item of groupItems) {
|
|
644
|
+
const deps = item.registryDependencies?.length ? dim(` (depends on: ${item.registryDependencies.join(", ")})`) : "";
|
|
645
|
+
console.log(` ${item.name}${deps}`);
|
|
646
|
+
}
|
|
647
|
+
console.log("");
|
|
648
|
+
}
|
|
649
|
+
console.log(` ${dim(`${items.length} components available`)}
|
|
650
|
+
`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/index.ts
|
|
654
|
+
var program = new Command();
|
|
655
|
+
program.name("stera-ui").description("Add Stera UI components to your project.").version("0.1.0");
|
|
656
|
+
program.command("init").description("Initialize Stera UI in your project.").option("-c, --cwd <path>", "Working directory", process.cwd()).option("-y, --yes", "Skip confirmation prompts").action(async (options) => {
|
|
657
|
+
await init(options);
|
|
658
|
+
});
|
|
659
|
+
program.command("add").description("Add components to your project.").argument("<components...>", "Components to add").option("-c, --cwd <path>", "Working directory", process.cwd()).option("-y, --yes", "Skip confirmation prompts").option("-o, --overwrite", "Overwrite existing files without prompting").action(async (components, options) => {
|
|
660
|
+
await add(components, options);
|
|
661
|
+
});
|
|
662
|
+
program.command("list").description("List all available components.").action(async () => {
|
|
663
|
+
await list();
|
|
664
|
+
});
|
|
665
|
+
program.parse();
|