stera-ui 0.8.0 → 0.9.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/dist/add-JTKZDMNI.js +2 -0
- package/dist/chunk-42DL2YAQ.js +2 -0
- package/dist/chunk-FZGIVCWE.js +42175 -0
- package/dist/chunk-LUUYMQIF.js +97 -0
- package/dist/chunk-MBBLZJIK.js +2 -0
- package/dist/chunk-OPVLMYW5.js +2 -0
- package/dist/chunk-OSMC63WJ.js +2 -0
- package/dist/chunk-P2O7D5BG.js +2 -0
- package/dist/chunk-UVHQPGZ3.js +7 -0
- package/dist/chunk-XJ6VM67K.js +2 -0
- package/dist/cli.js +29 -32
- package/dist/index.js +2 -86
- package/dist/init-XAKB27HW.js +12 -0
- package/dist/list-W7ECLPKW.js +5 -0
- package/dist/registry/index.d.ts +3 -1
- package/dist/registry/index.js +2 -17
- package/dist/schema/index.js +2 -18
- package/dist/utils/index.d.ts +26 -3
- package/dist/utils/index.js +2 -58
- package/package.json +6 -8
- package/dist/add-WUTTJN25.js +0 -215
- package/dist/chunk-4E46BULJ.js +0 -79
- package/dist/chunk-5NLD52O5.js +0 -376
- package/dist/chunk-BJNLGEZJ.js +0 -100
- package/dist/chunk-KGMF6UW4.js +0 -21
- package/dist/chunk-KKUJRJ2N.js +0 -209
- package/dist/chunk-V4TMOT6I.js +0 -880
- package/dist/init-QYYCGWYV.js +0 -763
- package/dist/list-M73QTIWR.js +0 -44
package/dist/init-QYYCGWYV.js
DELETED
|
@@ -1,763 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
CONFIG_FILE,
|
|
4
|
-
LEGACY_CONFIG_FILE,
|
|
5
|
-
TYPOGRAPHY_TARGET,
|
|
6
|
-
findConfigPath,
|
|
7
|
-
insertImportLine,
|
|
8
|
-
installDependencies,
|
|
9
|
-
migrateLegacyStyles,
|
|
10
|
-
patchCssVariables,
|
|
11
|
-
writeStyles
|
|
12
|
-
} from "./chunk-5NLD52O5.js";
|
|
13
|
-
import {
|
|
14
|
-
createSpinner,
|
|
15
|
-
fetchRegistryItem
|
|
16
|
-
} from "./chunk-KKUJRJ2N.js";
|
|
17
|
-
import {
|
|
18
|
-
CHECK,
|
|
19
|
-
LOGO,
|
|
20
|
-
dim
|
|
21
|
-
} from "./chunk-KGMF6UW4.js";
|
|
22
|
-
|
|
23
|
-
// src/commands/init.ts
|
|
24
|
-
import fs4 from "fs";
|
|
25
|
-
import fsp from "fs/promises";
|
|
26
|
-
import path4 from "path";
|
|
27
|
-
import { confirm, select } from "@inquirer/prompts";
|
|
28
|
-
|
|
29
|
-
// src/utils/detect-project.ts
|
|
30
|
-
import fs from "fs";
|
|
31
|
-
import path from "path";
|
|
32
|
-
import { loadConfig } from "tsconfig-paths";
|
|
33
|
-
function hasFile(cwd, filenames) {
|
|
34
|
-
return filenames.some((f) => fs.existsSync(path.join(cwd, f)));
|
|
35
|
-
}
|
|
36
|
-
function findTailwindCssFile(cwd) {
|
|
37
|
-
const candidates = [
|
|
38
|
-
"src/app/globals.css",
|
|
39
|
-
"src/styles/globals.css",
|
|
40
|
-
"src/index.css",
|
|
41
|
-
"app/globals.css",
|
|
42
|
-
"styles/globals.css",
|
|
43
|
-
"index.css"
|
|
44
|
-
];
|
|
45
|
-
for (const candidate of candidates) {
|
|
46
|
-
const fullPath = path.join(cwd, candidate);
|
|
47
|
-
if (!fs.existsSync(fullPath)) continue;
|
|
48
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
49
|
-
if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'") || content.includes("@tailwind base")) {
|
|
50
|
-
return candidate;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
function detectProject(cwd) {
|
|
56
|
-
const hasSrc = fs.existsSync(path.join(cwd, "src"));
|
|
57
|
-
let framework = "other";
|
|
58
|
-
if (hasFile(cwd, ["next.config.ts", "next.config.js", "next.config.mjs"])) {
|
|
59
|
-
framework = "next";
|
|
60
|
-
} else if (hasFile(cwd, ["vite.config.ts", "vite.config.js", "vite.config.mjs"])) {
|
|
61
|
-
framework = "vite";
|
|
62
|
-
}
|
|
63
|
-
let rsc = false;
|
|
64
|
-
if (framework === "next") {
|
|
65
|
-
const appDir = hasSrc ? "src/app" : "app";
|
|
66
|
-
rsc = fs.existsSync(path.join(cwd, appDir));
|
|
67
|
-
}
|
|
68
|
-
const existingCssFile = findTailwindCssFile(cwd);
|
|
69
|
-
let layoutFile = null;
|
|
70
|
-
if (framework === "next") {
|
|
71
|
-
const appDir = hasSrc ? "src/app" : "app";
|
|
72
|
-
for (const ext of ["tsx", "jsx", "ts", "js"]) {
|
|
73
|
-
const candidate = `${appDir}/layout.${ext}`;
|
|
74
|
-
if (fs.existsSync(path.join(cwd, candidate))) {
|
|
75
|
-
layoutFile = candidate;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} else if (framework === "vite") {
|
|
80
|
-
if (fs.existsSync(path.join(cwd, "index.html"))) {
|
|
81
|
-
layoutFile = "index.html";
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return { hasSrc, framework, rsc, existingCssFile, layoutFile };
|
|
85
|
-
}
|
|
86
|
-
function detectAliasPrefix(cwd) {
|
|
87
|
-
let tsConfig;
|
|
88
|
-
try {
|
|
89
|
-
tsConfig = loadConfig(cwd);
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
if (tsConfig.resultType !== "success" || Object.keys(tsConfig.paths).length === 0) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
for (const [alias, targets] of Object.entries(tsConfig.paths)) {
|
|
97
|
-
if (targets.includes("./*") || targets.includes("./src/*") || targets.includes("./app/*")) {
|
|
98
|
-
return alias.replace(/\/\*$/, "");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const first = Object.keys(tsConfig.paths)[0];
|
|
102
|
-
return first ? first.replace(/\/\*$/, "") : null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// src/utils/detect-fonts.ts
|
|
106
|
-
import fs2 from "fs";
|
|
107
|
-
import path2 from "path";
|
|
108
|
-
function readIfExists(filePath) {
|
|
109
|
-
try {
|
|
110
|
-
return fs2.readFileSync(filePath, "utf-8");
|
|
111
|
-
} catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function detectNextFont(layoutContent) {
|
|
116
|
-
const hasNextFont = /import.*from\s+['"]next\/font/.test(layoutContent) || /import.*from\s+['"]@next\/font/.test(layoutContent) || /import.*from\s+['"]geist\/font/.test(layoutContent);
|
|
117
|
-
let nextFontVariable = null;
|
|
118
|
-
const varMatch = layoutContent.match(/variable:\s*['"](--.+?)['"]/);
|
|
119
|
-
if (varMatch) {
|
|
120
|
-
nextFontVariable = varMatch[1];
|
|
121
|
-
}
|
|
122
|
-
return { hasNextFont, nextFontVariable };
|
|
123
|
-
}
|
|
124
|
-
function detectFontFace(cssContent) {
|
|
125
|
-
const hasFontFace = /@font-face\s*\{/.test(cssContent);
|
|
126
|
-
const fontFamilies = [];
|
|
127
|
-
const regex = /font-family:\s*['"]?([^'";}\n]+?)['"]?\s*;/g;
|
|
128
|
-
let match;
|
|
129
|
-
while ((match = regex.exec(cssContent)) !== null) {
|
|
130
|
-
const family = match[1].trim();
|
|
131
|
-
if (family && !fontFamilies.includes(family)) {
|
|
132
|
-
fontFamilies.push(family);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return { hasFontFace, fontFamilies };
|
|
136
|
-
}
|
|
137
|
-
function detectGoogleFonts(content) {
|
|
138
|
-
return content.includes("fonts.googleapis.com") || content.includes("fonts.gstatic.com");
|
|
139
|
-
}
|
|
140
|
-
function detectFonts(cwd, project) {
|
|
141
|
-
const result = {
|
|
142
|
-
hasNextFont: false,
|
|
143
|
-
hasFontFace: false,
|
|
144
|
-
hasGoogleFontsLink: false,
|
|
145
|
-
fontFamilies: [],
|
|
146
|
-
nextFontVariable: null
|
|
147
|
-
};
|
|
148
|
-
if (project.layoutFile) {
|
|
149
|
-
const layoutContent = readIfExists(path2.join(cwd, project.layoutFile));
|
|
150
|
-
if (layoutContent) {
|
|
151
|
-
const nextFontResult = detectNextFont(layoutContent);
|
|
152
|
-
result.hasNextFont = nextFontResult.hasNextFont;
|
|
153
|
-
result.nextFontVariable = nextFontResult.nextFontVariable;
|
|
154
|
-
if (detectGoogleFonts(layoutContent)) {
|
|
155
|
-
result.hasGoogleFontsLink = true;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (project.existingCssFile) {
|
|
160
|
-
const cssContent = readIfExists(path2.join(cwd, project.existingCssFile));
|
|
161
|
-
if (cssContent) {
|
|
162
|
-
const fontFaceResult = detectFontFace(cssContent);
|
|
163
|
-
result.hasFontFace = fontFaceResult.hasFontFace;
|
|
164
|
-
result.fontFamilies.push(...fontFaceResult.fontFamilies);
|
|
165
|
-
if (detectGoogleFonts(cssContent)) {
|
|
166
|
-
result.hasGoogleFontsLink = true;
|
|
167
|
-
}
|
|
168
|
-
const importRegex = /@import\s+['"](.+?)['"]/g;
|
|
169
|
-
let importMatch;
|
|
170
|
-
while ((importMatch = importRegex.exec(cssContent)) !== null) {
|
|
171
|
-
const importPath = importMatch[1];
|
|
172
|
-
const resolvedPath = path2.join(
|
|
173
|
-
cwd,
|
|
174
|
-
path2.dirname(project.existingCssFile),
|
|
175
|
-
importPath
|
|
176
|
-
);
|
|
177
|
-
const importedContent = readIfExists(resolvedPath);
|
|
178
|
-
if (importedContent) {
|
|
179
|
-
const imported = detectFontFace(importedContent);
|
|
180
|
-
if (imported.hasFontFace) result.hasFontFace = true;
|
|
181
|
-
for (const family of imported.fontFamilies) {
|
|
182
|
-
if (!result.fontFamilies.includes(family)) {
|
|
183
|
-
result.fontFamilies.push(family);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
for (const htmlFile of ["index.html", "src/index.html"]) {
|
|
191
|
-
const htmlContent = readIfExists(path2.join(cwd, htmlFile));
|
|
192
|
-
if (htmlContent && detectGoogleFonts(htmlContent)) {
|
|
193
|
-
result.hasGoogleFontsLink = true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// src/utils/update-fonts.ts
|
|
200
|
-
import { existsSync, promises as fs3 } from "fs";
|
|
201
|
-
import path3 from "path";
|
|
202
|
-
import {
|
|
203
|
-
Project,
|
|
204
|
-
ScriptKind,
|
|
205
|
-
SyntaxKind,
|
|
206
|
-
VariableDeclarationKind
|
|
207
|
-
} from "ts-morph";
|
|
208
|
-
var ROOT_FONT_VARIABLES = /* @__PURE__ */ new Set([
|
|
209
|
-
"--font-sans",
|
|
210
|
-
"--font-serif",
|
|
211
|
-
"--font-mono"
|
|
212
|
-
]);
|
|
213
|
-
function massageTreeForFonts(fonts, project) {
|
|
214
|
-
const result = {
|
|
215
|
-
cssVars: {},
|
|
216
|
-
dependencies: [],
|
|
217
|
-
cssImports: []
|
|
218
|
-
};
|
|
219
|
-
if (fonts.length === 0) return result;
|
|
220
|
-
const isNext = project.framework === "next";
|
|
221
|
-
for (const font of fonts) {
|
|
222
|
-
if (isNext) {
|
|
223
|
-
result.cssVars[font.font.variable] = `var(${font.font.variable})`;
|
|
224
|
-
} else {
|
|
225
|
-
const fontName = font.name.replace(/^font-/, "");
|
|
226
|
-
const fontSourceDep = font.font.dependency ?? `@fontsource-variable/${fontName}`;
|
|
227
|
-
result.dependencies.push(fontSourceDep);
|
|
228
|
-
result.cssImports.push(`@import "${fontSourceDep}";`);
|
|
229
|
-
result.cssVars[font.font.variable] = font.font.family;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
|
-
async function updateFonts(fonts, project, cwd, options = {}) {
|
|
235
|
-
if (fonts.length === 0) return;
|
|
236
|
-
if (project.framework !== "next") return;
|
|
237
|
-
const layoutPath = findLayoutFile(project, cwd);
|
|
238
|
-
if (!layoutPath) return;
|
|
239
|
-
if (options.silent) {
|
|
240
|
-
try {
|
|
241
|
-
const layoutContent = await fs3.readFile(layoutPath, "utf-8");
|
|
242
|
-
const updatedContent = transformLayoutFonts(layoutContent, fonts);
|
|
243
|
-
if (updatedContent !== layoutContent) {
|
|
244
|
-
await fs3.writeFile(layoutPath, updatedContent, "utf-8");
|
|
245
|
-
}
|
|
246
|
-
} catch {
|
|
247
|
-
}
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const spinner = createSpinner("Updating fonts");
|
|
251
|
-
try {
|
|
252
|
-
const layoutContent = await fs3.readFile(layoutPath, "utf-8");
|
|
253
|
-
const updatedContent = transformLayoutFonts(layoutContent, fonts);
|
|
254
|
-
if (updatedContent !== layoutContent) {
|
|
255
|
-
await fs3.writeFile(layoutPath, updatedContent, "utf-8");
|
|
256
|
-
}
|
|
257
|
-
spinner.succeed("Updated layout with font imports");
|
|
258
|
-
} catch {
|
|
259
|
-
spinner.fail("Failed to update fonts");
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function findLayoutFile(project, cwd) {
|
|
263
|
-
if (!project.layoutFile) return null;
|
|
264
|
-
const fullPath = path3.join(cwd, project.layoutFile);
|
|
265
|
-
return existsSync(fullPath) ? fullPath : null;
|
|
266
|
-
}
|
|
267
|
-
function transformLayoutFonts(input, fonts) {
|
|
268
|
-
const project = new Project({ compilerOptions: {} });
|
|
269
|
-
const sourceFile = project.createSourceFile("layout.tsx", input, {
|
|
270
|
-
scriptKind: ScriptKind.TSX
|
|
271
|
-
});
|
|
272
|
-
const googleFonts = fonts.filter((f) => f.font.provider === "google");
|
|
273
|
-
const fontVariableNames = [];
|
|
274
|
-
const fontUtilityClasses = [];
|
|
275
|
-
for (const font of googleFonts) {
|
|
276
|
-
const importName = font.font.import;
|
|
277
|
-
if (!importName) continue;
|
|
278
|
-
const existingImport = sourceFile.getImportDeclaration(
|
|
279
|
-
(decl) => decl.getModuleSpecifierValue() === "next/font/google"
|
|
280
|
-
);
|
|
281
|
-
let hasExistingImport = false;
|
|
282
|
-
if (existingImport) {
|
|
283
|
-
const namedImports = existingImport.getNamedImports();
|
|
284
|
-
hasExistingImport = namedImports.some(
|
|
285
|
-
(imp) => imp.getName() === importName
|
|
286
|
-
);
|
|
287
|
-
if (!hasExistingImport) {
|
|
288
|
-
existingImport.addNamedImport(importName);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
sourceFile.addImportDeclaration({
|
|
292
|
-
moduleSpecifier: "next/font/google",
|
|
293
|
-
namedImports: [importName]
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
const varName = getFontVariableName(importName, font.font.variable);
|
|
297
|
-
const fontOptions = buildFontOptions(font);
|
|
298
|
-
const existingVarDecl = findFontVariableDeclaration(
|
|
299
|
-
sourceFile,
|
|
300
|
-
font.font.variable
|
|
301
|
-
);
|
|
302
|
-
if (hasExistingImport && !existingVarDecl && isRootFontVariable(font.font.variable)) {
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (existingVarDecl) {
|
|
306
|
-
existingVarDecl.setInitializer(`${importName}(${fontOptions})`);
|
|
307
|
-
if (existingVarDecl.getName() !== varName) {
|
|
308
|
-
existingVarDecl.rename(varName);
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
const insertPosition = findInsertPosition(sourceFile);
|
|
312
|
-
const statement = sourceFile.insertVariableStatement(insertPosition, {
|
|
313
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
314
|
-
declarations: [
|
|
315
|
-
{
|
|
316
|
-
name: varName,
|
|
317
|
-
initializer: `${importName}(${fontOptions})`
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
});
|
|
321
|
-
statement.appendWhitespace("\n");
|
|
322
|
-
}
|
|
323
|
-
fontVariableNames.push(varName);
|
|
324
|
-
if (shouldApplyFontUtilityToHtml(font)) {
|
|
325
|
-
fontUtilityClasses.push(font.font.variable.replace("--", ""));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
const fontFamilyClasses = /* @__PURE__ */ new Set(["font-sans", "font-serif", "font-mono"]);
|
|
329
|
-
const lastFontFamilyClass = [...fontUtilityClasses].reverse().find((cls) => fontFamilyClasses.has(cls));
|
|
330
|
-
const filteredUtilityClasses = fontUtilityClasses.filter(
|
|
331
|
-
(cls) => !fontFamilyClasses.has(cls)
|
|
332
|
-
);
|
|
333
|
-
if (lastFontFamilyClass) {
|
|
334
|
-
filteredUtilityClasses.unshift(lastFontFamilyClass);
|
|
335
|
-
}
|
|
336
|
-
if (fontVariableNames.length > 0) {
|
|
337
|
-
updateHtmlClassName(sourceFile, fontVariableNames, filteredUtilityClasses);
|
|
338
|
-
}
|
|
339
|
-
return sourceFile.getFullText();
|
|
340
|
-
}
|
|
341
|
-
function buildFontOptions(font) {
|
|
342
|
-
const options = {};
|
|
343
|
-
if (font.font.subsets?.length) options.subsets = font.font.subsets;
|
|
344
|
-
if (font.font.weight?.length) options.weight = font.font.weight;
|
|
345
|
-
options.variable = font.font.variable;
|
|
346
|
-
return JSON.stringify(options).replace(/"([^"]+)":/g, "$1:").replace(/"/g, "'");
|
|
347
|
-
}
|
|
348
|
-
function isRootFontVariable(variable) {
|
|
349
|
-
return ROOT_FONT_VARIABLES.has(variable);
|
|
350
|
-
}
|
|
351
|
-
function shouldApplyFontUtilityToHtml(font) {
|
|
352
|
-
return !font.font.selector && isRootFontVariable(font.font.variable);
|
|
353
|
-
}
|
|
354
|
-
function getFontVariableName(importName, variable) {
|
|
355
|
-
const baseName = toCamelCase(importName);
|
|
356
|
-
if (isRootFontVariable(variable)) return baseName;
|
|
357
|
-
return `${baseName}${toPascalCase(variable.replace(/^--font-/, ""))}`;
|
|
358
|
-
}
|
|
359
|
-
function toCamelCase(str) {
|
|
360
|
-
return str.split("_").map(
|
|
361
|
-
(part, index) => index === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
362
|
-
).join("");
|
|
363
|
-
}
|
|
364
|
-
function toPascalCase(str) {
|
|
365
|
-
return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
366
|
-
}
|
|
367
|
-
function findFontVariableDeclaration(sourceFile, variable) {
|
|
368
|
-
for (const statement of sourceFile.getVariableStatements()) {
|
|
369
|
-
for (const declaration of statement.getDeclarations()) {
|
|
370
|
-
const initializer = declaration.getInitializer();
|
|
371
|
-
if (!initializer || initializer.getKind() !== SyntaxKind.CallExpression)
|
|
372
|
-
continue;
|
|
373
|
-
const args = initializer.getArguments();
|
|
374
|
-
if (args.length === 0) continue;
|
|
375
|
-
const argText = args[0].getText();
|
|
376
|
-
if (argText.includes("variable:") && argText.includes(variable)) {
|
|
377
|
-
return declaration;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
function findInsertPosition(sourceFile) {
|
|
384
|
-
const imports = sourceFile.getImportDeclarations();
|
|
385
|
-
if (imports.length > 0) {
|
|
386
|
-
return imports[imports.length - 1].getChildIndex() + 1;
|
|
387
|
-
}
|
|
388
|
-
return 0;
|
|
389
|
-
}
|
|
390
|
-
function updateHtmlClassName(sourceFile, fontVariableNames, fontUtilityClasses) {
|
|
391
|
-
const jsxElements = sourceFile.getDescendantsOfKind(
|
|
392
|
-
SyntaxKind.JsxOpeningElement
|
|
393
|
-
);
|
|
394
|
-
for (const element of jsxElements) {
|
|
395
|
-
if (element.getTagNameNode().getText() !== "html") continue;
|
|
396
|
-
const newUtilityClasses = fontUtilityClasses.map((cls) => `"${cls}"`);
|
|
397
|
-
const newVarExpressions = fontVariableNames.map(
|
|
398
|
-
(name) => `${name}.variable`
|
|
399
|
-
);
|
|
400
|
-
const allNewArgs = [...newUtilityClasses, ...newVarExpressions];
|
|
401
|
-
const classNameAttr = element.getAttribute("className");
|
|
402
|
-
if (!classNameAttr) {
|
|
403
|
-
element.addAttribute({
|
|
404
|
-
name: "className",
|
|
405
|
-
initializer: `{cn(${allNewArgs.join(", ")})}`
|
|
406
|
-
});
|
|
407
|
-
ensureCnImport(sourceFile);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
if (classNameAttr.getKind() !== SyntaxKind.JsxAttribute) return;
|
|
411
|
-
const jsxAttr = classNameAttr.asKindOrThrow(SyntaxKind.JsxAttribute);
|
|
412
|
-
const initializer = jsxAttr.getInitializer();
|
|
413
|
-
if (!initializer) return;
|
|
414
|
-
if (initializer.getKind() === SyntaxKind.StringLiteral) {
|
|
415
|
-
const currentValue = initializer.getText().slice(1, -1);
|
|
416
|
-
ensureCnImport(sourceFile);
|
|
417
|
-
jsxAttr.setInitializer(
|
|
418
|
-
`{cn("${currentValue}", ${allNewArgs.join(", ")})}`
|
|
419
|
-
);
|
|
420
|
-
} else if (initializer.getKind() === SyntaxKind.JsxExpression) {
|
|
421
|
-
const jsxExpr = initializer.asKindOrThrow(SyntaxKind.JsxExpression);
|
|
422
|
-
const expr = jsxExpr.getExpression();
|
|
423
|
-
if (!expr) return;
|
|
424
|
-
const exprText = expr.getText();
|
|
425
|
-
if (exprText.startsWith("cn(")) {
|
|
426
|
-
const hasAllFontVars = newVarExpressions.every(
|
|
427
|
-
(v) => exprText.includes(v)
|
|
428
|
-
);
|
|
429
|
-
const hasAllUtilityClasses = fontUtilityClasses.every(
|
|
430
|
-
(cls) => exprText.includes(`"${cls}"`)
|
|
431
|
-
);
|
|
432
|
-
if (hasAllFontVars && hasAllUtilityClasses) continue;
|
|
433
|
-
let cleanedExpr = exprText;
|
|
434
|
-
for (const varExpr of newVarExpressions) {
|
|
435
|
-
cleanedExpr = cleanedExpr.replace(
|
|
436
|
-
new RegExp(`,?\\s*${varExpr.replace(".", "\\.")}`, "g"),
|
|
437
|
-
""
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
cleanedExpr = cleanedExpr.replace(/cn\(\s*,/, "cn(");
|
|
441
|
-
const newExpr = cleanedExpr.replace(
|
|
442
|
-
/\)$/,
|
|
443
|
-
`, ${allNewArgs.join(", ")})`
|
|
444
|
-
);
|
|
445
|
-
jsxExpr.replaceWithText(`{${newExpr}}`);
|
|
446
|
-
} else if (/^\w+\.variable$/.test(exprText)) {
|
|
447
|
-
ensureCnImport(sourceFile);
|
|
448
|
-
jsxExpr.replaceWithText(`{cn(${allNewArgs.join(", ")})}`);
|
|
449
|
-
} else if (exprText.startsWith("`") && exprText.endsWith("`")) {
|
|
450
|
-
const cnArgs = parseTemplateLiteralToCnArgs(exprText);
|
|
451
|
-
ensureCnImport(sourceFile);
|
|
452
|
-
const allNewArgsSet = new Set(allNewArgs);
|
|
453
|
-
const fontFamilyLiterals = new Set(
|
|
454
|
-
["font-sans", "font-serif", "font-mono"].map((c) => `"${c}"`)
|
|
455
|
-
);
|
|
456
|
-
const cleanedCnArgs = cnArgs.filter(
|
|
457
|
-
(arg) => !allNewArgsSet.has(arg) && !fontFamilyLiterals.has(arg)
|
|
458
|
-
);
|
|
459
|
-
jsxExpr.replaceWithText(
|
|
460
|
-
`{cn(${[...cleanedCnArgs, ...allNewArgs].join(", ")})}`
|
|
461
|
-
);
|
|
462
|
-
} else {
|
|
463
|
-
ensureCnImport(sourceFile);
|
|
464
|
-
jsxExpr.replaceWithText(`{cn(${exprText}, ${allNewArgs.join(", ")})}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
function ensureCnImport(sourceFile) {
|
|
470
|
-
const existingImport = sourceFile.getImportDeclaration(
|
|
471
|
-
(decl) => decl.getNamedImports().some((imp) => imp.getName() === "cn")
|
|
472
|
-
);
|
|
473
|
-
if (!existingImport) {
|
|
474
|
-
const utilsImport = sourceFile.getImportDeclaration(
|
|
475
|
-
(decl) => decl.getModuleSpecifierValue().includes("/lib/utils")
|
|
476
|
-
);
|
|
477
|
-
if (utilsImport) {
|
|
478
|
-
const namedImports = utilsImport.getNamedImports();
|
|
479
|
-
if (!namedImports.some((imp) => imp.getName() === "cn")) {
|
|
480
|
-
utilsImport.addNamedImport("cn");
|
|
481
|
-
}
|
|
482
|
-
} else {
|
|
483
|
-
sourceFile.addImportDeclaration({
|
|
484
|
-
moduleSpecifier: "@/lib/utils",
|
|
485
|
-
namedImports: ["cn"]
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
function parseTemplateLiteralToCnArgs(templateLiteral) {
|
|
491
|
-
const staticArgs = [];
|
|
492
|
-
const variableArgs = [];
|
|
493
|
-
const content = templateLiteral.slice(1, -1);
|
|
494
|
-
const parts = content.split(/(\$\{[^}]+\})/);
|
|
495
|
-
for (const part of parts) {
|
|
496
|
-
if (!part) continue;
|
|
497
|
-
if (part.startsWith("${") && part.endsWith("}")) {
|
|
498
|
-
const expr = part.slice(2, -1).trim();
|
|
499
|
-
if (expr) variableArgs.push(expr);
|
|
500
|
-
} else {
|
|
501
|
-
const staticParts = part.trim().split(/\s+/).filter(Boolean);
|
|
502
|
-
for (const staticPart of staticParts) {
|
|
503
|
-
staticArgs.push(`"${staticPart}"`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return [...staticArgs, ...variableArgs];
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// src/commands/init.ts
|
|
511
|
-
var DEFAULT_FONT_NAMES = ["font-geist", "font-geist-mono"];
|
|
512
|
-
function resolveCssPath(project) {
|
|
513
|
-
if (project.existingCssFile) return project.existingCssFile;
|
|
514
|
-
return project.hasSrc ? "src/styles/globals.css" : "styles/globals.css";
|
|
515
|
-
}
|
|
516
|
-
function buildConfig(project, strategy, aliasPrefix) {
|
|
517
|
-
const cssPath = resolveCssPath(project);
|
|
518
|
-
return {
|
|
519
|
-
$schema: "https://ui.stera.sh/schema.json",
|
|
520
|
-
version: 1,
|
|
521
|
-
css: cssPath,
|
|
522
|
-
aliases: {
|
|
523
|
-
components: `${aliasPrefix}/components`,
|
|
524
|
-
utils: `${aliasPrefix}/lib/utils`,
|
|
525
|
-
ui: `${aliasPrefix}/components/ui`,
|
|
526
|
-
lib: `${aliasPrefix}/lib`,
|
|
527
|
-
hooks: `${aliasPrefix}/hooks`
|
|
528
|
-
},
|
|
529
|
-
fonts: {
|
|
530
|
-
strategy
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
async function promptFontStrategy(detected, skipPrompts) {
|
|
535
|
-
const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
|
|
536
|
-
const defaultStrategy = hasExistingFonts ? "keep-existing" : "stera-default";
|
|
537
|
-
if (skipPrompts) return defaultStrategy;
|
|
538
|
-
if (hasExistingFonts) {
|
|
539
|
-
const sources = [];
|
|
540
|
-
if (detected.fontFamilies.length > 0) {
|
|
541
|
-
sources.push(detected.fontFamilies.join(", "));
|
|
542
|
-
}
|
|
543
|
-
if (detected.hasNextFont) sources.push("next/font");
|
|
544
|
-
if (detected.hasGoogleFontsLink) sources.push("Google Fonts");
|
|
545
|
-
console.log(` Detected existing fonts: ${sources.join(" via ")}
|
|
546
|
-
`);
|
|
547
|
-
}
|
|
548
|
-
const choices = [];
|
|
549
|
-
if (hasExistingFonts) {
|
|
550
|
-
choices.push({
|
|
551
|
-
value: "keep-existing",
|
|
552
|
-
name: "Keep my current fonts",
|
|
553
|
-
description: "Update CSS variables to use your existing fonts"
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
choices.push({
|
|
557
|
-
value: "stera-default",
|
|
558
|
-
name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
|
|
559
|
-
description: "Optimized variable fonts via next/font or fontsource"
|
|
560
|
-
});
|
|
561
|
-
choices.push({
|
|
562
|
-
value: "skip",
|
|
563
|
-
name: "Skip font setup",
|
|
564
|
-
description: "Configure fonts manually later"
|
|
565
|
-
});
|
|
566
|
-
const strategy = await select({
|
|
567
|
-
message: "How would you like to set up fonts?",
|
|
568
|
-
choices,
|
|
569
|
-
default: defaultStrategy
|
|
570
|
-
});
|
|
571
|
-
console.log("");
|
|
572
|
-
return strategy;
|
|
573
|
-
}
|
|
574
|
-
async function fetchDefaultFonts() {
|
|
575
|
-
const spinner = createSpinner("Fetching fonts");
|
|
576
|
-
const results = await Promise.all(
|
|
577
|
-
DEFAULT_FONT_NAMES.map(async (name) => {
|
|
578
|
-
try {
|
|
579
|
-
const item = await fetchRegistryItem(name);
|
|
580
|
-
return item.type === "registry:font" ? item : null;
|
|
581
|
-
} catch {
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
})
|
|
585
|
-
);
|
|
586
|
-
spinner.succeed("Fonts resolved");
|
|
587
|
-
return results.filter((item) => item !== null);
|
|
588
|
-
}
|
|
589
|
-
async function executeFontStrategy(strategy, project, detected, config, cwd) {
|
|
590
|
-
if (strategy === "skip") return;
|
|
591
|
-
const cssDir = path4.dirname(path4.resolve(cwd, config.css));
|
|
592
|
-
const typographyPath = path4.join(cssDir, TYPOGRAPHY_TARGET);
|
|
593
|
-
if (strategy === "keep-existing") {
|
|
594
|
-
const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
|
|
595
|
-
const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
|
|
596
|
-
const patched = patchCssVariables(typographyPath, {
|
|
597
|
-
"--font-heading": fontValue,
|
|
598
|
-
"--font-sans": fontValue
|
|
599
|
-
});
|
|
600
|
-
if (patched.length > 0) {
|
|
601
|
-
console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
|
|
602
|
-
}
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
const fonts = await fetchDefaultFonts();
|
|
606
|
-
if (fonts.length === 0) return;
|
|
607
|
-
const fontTree = massageTreeForFonts(fonts, project);
|
|
608
|
-
if (fontTree.dependencies.length > 0) {
|
|
609
|
-
await installDependencies(fontTree.dependencies, cwd);
|
|
610
|
-
}
|
|
611
|
-
if (fontTree.cssImports.length > 0) {
|
|
612
|
-
for (const importLine of fontTree.cssImports) {
|
|
613
|
-
await insertImportLine(typographyPath, importLine);
|
|
614
|
-
}
|
|
615
|
-
console.log(
|
|
616
|
-
` ${CHECK} Added font imports to ${path4.relative(cwd, typographyPath)}`
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
if (Object.keys(fontTree.cssVars).length > 0) {
|
|
620
|
-
const patched = patchCssVariables(typographyPath, fontTree.cssVars);
|
|
621
|
-
if (patched.length > 0) {
|
|
622
|
-
console.log(
|
|
623
|
-
` ${CHECK} Updated font variables in ${path4.relative(cwd, typographyPath)}`
|
|
624
|
-
);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
await updateFonts(fonts, project, cwd);
|
|
628
|
-
}
|
|
629
|
-
async function init(options) {
|
|
630
|
-
const cwd = options.cwd ? path4.resolve(options.cwd) : process.cwd();
|
|
631
|
-
console.log(`
|
|
632
|
-
${LOGO} Stera UI
|
|
633
|
-
`);
|
|
634
|
-
const existing = findConfigPath(cwd);
|
|
635
|
-
if (existing) {
|
|
636
|
-
const existingName = path4.basename(existing);
|
|
637
|
-
if (existingName === LEGACY_CONFIG_FILE) {
|
|
638
|
-
console.log(` Found legacy ${LEGACY_CONFIG_FILE} at ${existing}`);
|
|
639
|
-
const shouldRename = options.yes ? true : await confirm({
|
|
640
|
-
message: `Rename it to ${CONFIG_FILE}?`,
|
|
641
|
-
default: true
|
|
642
|
-
});
|
|
643
|
-
if (shouldRename) {
|
|
644
|
-
const renamed = path4.join(path4.dirname(existing), CONFIG_FILE);
|
|
645
|
-
fs4.renameSync(existing, renamed);
|
|
646
|
-
console.log(` ${CHECK} Renamed to ${CONFIG_FILE}`);
|
|
647
|
-
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
648
|
-
} else {
|
|
649
|
-
console.log(
|
|
650
|
-
` Keeping ${LEGACY_CONFIG_FILE} for now. Legacy filename support will be removed in a future release.
|
|
651
|
-
`
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
console.log(` ${CONFIG_FILE} already exists at ${existing}`);
|
|
657
|
-
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
const pkgPath = path4.join(cwd, "package.json");
|
|
661
|
-
if (!fs4.existsSync(pkgPath)) {
|
|
662
|
-
console.error(
|
|
663
|
-
" Error: No package.json found. Please run this in a project directory."
|
|
664
|
-
);
|
|
665
|
-
process.exit(1);
|
|
666
|
-
}
|
|
667
|
-
const project = detectProject(cwd);
|
|
668
|
-
const aliasPrefix = detectAliasPrefix(cwd);
|
|
669
|
-
if (!aliasPrefix) {
|
|
670
|
-
console.error("");
|
|
671
|
-
console.error(" Error: No import alias found in your tsconfig.json file.");
|
|
672
|
-
console.error(
|
|
673
|
-
' Stera UI requires a path alias (e.g. "@/*": ["./src/*"]) in compilerOptions.paths.'
|
|
674
|
-
);
|
|
675
|
-
console.error(
|
|
676
|
-
" See https://ui.stera.sh/docs/installation for setup instructions per framework."
|
|
677
|
-
);
|
|
678
|
-
console.error("");
|
|
679
|
-
process.exit(1);
|
|
680
|
-
}
|
|
681
|
-
const frameworkName = project.framework === "next" ? "Next.js" : project.framework === "vite" ? "Vite" : null;
|
|
682
|
-
const parts = [];
|
|
683
|
-
if (frameworkName) parts.push(frameworkName);
|
|
684
|
-
if (project.hasSrc) parts.push("src/ directory");
|
|
685
|
-
if (parts.length > 0) {
|
|
686
|
-
console.log(` Detected ${parts.join(" project with ")} project
|
|
687
|
-
`);
|
|
688
|
-
}
|
|
689
|
-
const detectedFonts = detectFonts(cwd, project);
|
|
690
|
-
const uiDir = path4.join(path4.dirname(path4.resolve(cwd, resolveCssPath(project))), "ui");
|
|
691
|
-
if (fs4.existsSync(uiDir) && fs4.readdirSync(uiDir).length > 0) {
|
|
692
|
-
const relativeUiDir = path4.relative(cwd, uiDir);
|
|
693
|
-
console.log("");
|
|
694
|
-
console.log(` ${relativeUiDir} already exists and is not empty.`);
|
|
695
|
-
const proceed = options.yes || process.stdin.isTTY && await confirm({
|
|
696
|
-
message: `Write the style partials into ${relativeUiDir}?`,
|
|
697
|
-
default: false
|
|
698
|
-
});
|
|
699
|
-
if (!proceed) {
|
|
700
|
-
console.log("");
|
|
701
|
-
console.log(
|
|
702
|
-
` Aborted. Move ${relativeUiDir} aside, or point "css" in ${CONFIG_FILE} at a different stylesheet, then run init again.`
|
|
703
|
-
);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
const strategy = await promptFontStrategy(detectedFonts, options.yes ?? false);
|
|
708
|
-
const config = buildConfig(project, strategy, aliasPrefix);
|
|
709
|
-
const configPath = path4.join(cwd, CONFIG_FILE);
|
|
710
|
-
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
711
|
-
console.log(` ${CHECK} Created ${CONFIG_FILE}`);
|
|
712
|
-
const fetchSpinner = createSpinner("Fetching base styles");
|
|
713
|
-
let stylesResult;
|
|
714
|
-
try {
|
|
715
|
-
await migrateLegacyStyles(config, cwd);
|
|
716
|
-
stylesResult = await writeStyles(config, cwd);
|
|
717
|
-
} catch (err) {
|
|
718
|
-
fetchSpinner.fail("Failed to fetch base styles");
|
|
719
|
-
throw err;
|
|
720
|
-
}
|
|
721
|
-
fetchSpinner.succeed("Base styles resolved");
|
|
722
|
-
console.log(
|
|
723
|
-
` ${CHECK} Created ${path4.relative(cwd, stylesResult.indexPath)}`
|
|
724
|
-
);
|
|
725
|
-
const globalsPath = project.existingCssFile ? path4.resolve(cwd, project.existingCssFile) : path4.resolve(cwd, config.css);
|
|
726
|
-
if (fs4.existsSync(globalsPath)) {
|
|
727
|
-
for (const imp of stylesResult.extraImports) {
|
|
728
|
-
await insertImportLine(globalsPath, imp);
|
|
729
|
-
}
|
|
730
|
-
await insertImportLine(globalsPath, '@import "./ui/index.css";');
|
|
731
|
-
console.log(
|
|
732
|
-
` ${CHECK} Added @import to ${path4.relative(cwd, globalsPath)}`
|
|
733
|
-
);
|
|
734
|
-
} else {
|
|
735
|
-
await fsp.mkdir(path4.dirname(globalsPath), { recursive: true });
|
|
736
|
-
const scaffold = [
|
|
737
|
-
'@import "tailwindcss";',
|
|
738
|
-
...stylesResult.extraImports,
|
|
739
|
-
'@import "./ui/index.css";'
|
|
740
|
-
].join("\n") + "\n";
|
|
741
|
-
await fsp.writeFile(globalsPath, scaffold, "utf-8");
|
|
742
|
-
console.log(` ${CHECK} Created ${path4.relative(cwd, globalsPath)}`);
|
|
743
|
-
}
|
|
744
|
-
await executeFontStrategy(strategy, project, detectedFonts, config, cwd);
|
|
745
|
-
if (stylesResult.dependencies.length > 0) {
|
|
746
|
-
const deps = [...new Set(stylesResult.dependencies)].sort();
|
|
747
|
-
console.log("");
|
|
748
|
-
await installDependencies(deps, cwd);
|
|
749
|
-
}
|
|
750
|
-
console.log("");
|
|
751
|
-
console.log(" Next steps:");
|
|
752
|
-
console.log(" stera-ui add <component>");
|
|
753
|
-
if (strategy === "skip") {
|
|
754
|
-
console.log("");
|
|
755
|
-
console.log(
|
|
756
|
-
` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
console.log("");
|
|
760
|
-
}
|
|
761
|
-
export {
|
|
762
|
-
init
|
|
763
|
-
};
|