stera-ui 0.2.0 → 0.3.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/chunk-26HP67ZM.js +695 -0
- package/dist/{chunk-AXIACJ2C.js → chunk-3KADOWKC.js} +26 -4
- package/dist/cli.js +378 -281
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/schema/index.d.ts +291 -31
- package/dist/schema/index.js +3 -1
- package/dist/utils/index.d.ts +28 -2
- package/dist/utils/index.js +5 -1
- package/package.json +2 -1
- package/dist/chunk-MFUZBXO6.js +0 -375
package/dist/cli.js
CHANGED
|
@@ -153,7 +153,7 @@ async function writeComponentFiles(items, config, projectRoot, options = {}) {
|
|
|
153
153
|
const written = [];
|
|
154
154
|
const skipped = [];
|
|
155
155
|
for (const item of items) {
|
|
156
|
-
for (const file of item.files) {
|
|
156
|
+
for (const file of item.files ?? []) {
|
|
157
157
|
const outputPath = resolveOutputPath(config, projectRoot, file);
|
|
158
158
|
const relativePath = path3.relative(projectRoot, outputPath);
|
|
159
159
|
if (fs2.existsSync(outputPath)) {
|
|
@@ -286,6 +286,13 @@ async function installDependencies(deps, cwd) {
|
|
|
286
286
|
spinner.succeed(`Installed with ${pm}`);
|
|
287
287
|
} catch (error) {
|
|
288
288
|
spinner.fail(`Install failed`);
|
|
289
|
+
if (error instanceof Error && "stdout" in error && typeof error.stdout === "string" && error.stdout.includes("ERR_PNPM_ADDING_TO_ROOT")) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Cannot install dependencies at a pnpm workspace root.
|
|
292
|
+
Run stera-ui init from within a specific workspace package instead,
|
|
293
|
+
or use "pnpm add -w <package>" manually to install at the root.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
289
296
|
throw error;
|
|
290
297
|
}
|
|
291
298
|
}
|
|
@@ -394,7 +401,7 @@ async function add(components, options) {
|
|
|
394
401
|
}
|
|
395
402
|
const resolved = await resolveDependencies(components);
|
|
396
403
|
for (const item of resolved) {
|
|
397
|
-
for (const file of item.files) {
|
|
404
|
+
for (const file of item.files ?? []) {
|
|
398
405
|
if (file.content) {
|
|
399
406
|
file.content = applyTransforms(
|
|
400
407
|
file.content,
|
|
@@ -444,9 +451,9 @@ async function add(components, options) {
|
|
|
444
451
|
}
|
|
445
452
|
|
|
446
453
|
// src/commands/init.ts
|
|
447
|
-
import
|
|
448
|
-
import
|
|
449
|
-
import { select
|
|
454
|
+
import fs9 from "fs";
|
|
455
|
+
import path10 from "path";
|
|
456
|
+
import { select } from "@inquirer/prompts";
|
|
450
457
|
|
|
451
458
|
// src/utils/detect-project.ts
|
|
452
459
|
import fs5 from "fs";
|
|
@@ -619,243 +626,324 @@ function patchCssVariables(filePath, patches) {
|
|
|
619
626
|
}
|
|
620
627
|
return patched;
|
|
621
628
|
}
|
|
622
|
-
function removeCssImport(filePath, importPath) {
|
|
623
|
-
const content = fs7.readFileSync(filePath, "utf-8");
|
|
624
|
-
const pattern = new RegExp(
|
|
625
|
-
`@import\\s+['"]${escapeRegex(importPath)}['"]\\s*;?\\n?`
|
|
626
|
-
);
|
|
627
|
-
if (!pattern.test(content)) return false;
|
|
628
|
-
fs7.writeFileSync(filePath, content.replace(pattern, ""), "utf-8");
|
|
629
|
-
return true;
|
|
630
|
-
}
|
|
631
629
|
function escapeRegex(str) {
|
|
632
630
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
633
631
|
}
|
|
634
632
|
|
|
635
|
-
// src/utils/
|
|
636
|
-
import
|
|
637
|
-
import path10 from "path";
|
|
638
|
-
|
|
639
|
-
// src/utils/copy-fonts.ts
|
|
640
|
-
import fs8 from "fs";
|
|
633
|
+
// src/utils/update-fonts.ts
|
|
634
|
+
import { existsSync, promises as fs8 } from "fs";
|
|
641
635
|
import path9 from "path";
|
|
642
|
-
import {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
636
|
+
import {
|
|
637
|
+
Project,
|
|
638
|
+
ScriptKind,
|
|
639
|
+
SyntaxKind,
|
|
640
|
+
VariableDeclarationKind
|
|
641
|
+
} from "ts-morph";
|
|
642
|
+
var ROOT_FONT_VARIABLES = /* @__PURE__ */ new Set([
|
|
643
|
+
"--font-sans",
|
|
644
|
+
"--font-serif",
|
|
645
|
+
"--font-mono"
|
|
646
|
+
]);
|
|
647
|
+
function massageTreeForFonts(fonts, project) {
|
|
648
|
+
const result = {
|
|
649
|
+
cssVars: {},
|
|
650
|
+
dependencies: [],
|
|
651
|
+
cssImports: []
|
|
652
|
+
};
|
|
653
|
+
if (fonts.length === 0) return result;
|
|
654
|
+
const isNext = project.framework === "next";
|
|
655
|
+
for (const font of fonts) {
|
|
656
|
+
if (isNext) {
|
|
657
|
+
result.cssVars[font.font.variable] = `var(${font.font.variable})`;
|
|
658
|
+
} else {
|
|
659
|
+
const fontName = font.name.replace(/^font-/, "");
|
|
660
|
+
const fontSourceDep = font.font.dependency ?? `@fontsource-variable/${fontName}`;
|
|
661
|
+
result.dependencies.push(fontSourceDep);
|
|
662
|
+
result.cssImports.push(`@import "${fontSourceDep}";`);
|
|
663
|
+
result.cssVars[font.font.variable] = font.font.family;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
666
667
|
}
|
|
667
|
-
function
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
668
|
+
async function updateFonts(fonts, project, cwd, options = {}) {
|
|
669
|
+
if (fonts.length === 0) return;
|
|
670
|
+
if (project.framework !== "next") return;
|
|
671
|
+
const layoutPath = findLayoutFile(project, cwd);
|
|
672
|
+
if (!layoutPath) return;
|
|
673
|
+
if (options.silent) {
|
|
674
|
+
try {
|
|
675
|
+
const layoutContent = await fs8.readFile(layoutPath, "utf-8");
|
|
676
|
+
const updatedContent = transformLayoutFonts(layoutContent, fonts);
|
|
677
|
+
if (updatedContent !== layoutContent) {
|
|
678
|
+
await fs8.writeFile(layoutPath, updatedContent, "utf-8");
|
|
679
|
+
}
|
|
680
|
+
} catch {
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const spinner = createSpinner("Updating fonts");
|
|
685
|
+
try {
|
|
686
|
+
const layoutContent = await fs8.readFile(layoutPath, "utf-8");
|
|
687
|
+
const updatedContent = transformLayoutFonts(layoutContent, fonts);
|
|
688
|
+
if (updatedContent !== layoutContent) {
|
|
689
|
+
await fs8.writeFile(layoutPath, updatedContent, "utf-8");
|
|
690
|
+
}
|
|
691
|
+
spinner.succeed("Updated layout with font imports");
|
|
692
|
+
} catch {
|
|
693
|
+
spinner.fail("Failed to update fonts");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function findLayoutFile(project, cwd) {
|
|
697
|
+
if (!project.layoutFile) return null;
|
|
698
|
+
const fullPath = path9.join(cwd, project.layoutFile);
|
|
699
|
+
return existsSync(fullPath) ? fullPath : null;
|
|
700
|
+
}
|
|
701
|
+
function transformLayoutFonts(input, fonts) {
|
|
702
|
+
const project = new Project({ compilerOptions: {} });
|
|
703
|
+
const sourceFile = project.createSourceFile("layout.tsx", input, {
|
|
704
|
+
scriptKind: ScriptKind.TSX
|
|
705
|
+
});
|
|
706
|
+
const googleFonts = fonts.filter((f) => f.font.provider === "google");
|
|
707
|
+
const fontVariableNames = [];
|
|
708
|
+
const fontUtilityClasses = [];
|
|
709
|
+
for (const font of googleFonts) {
|
|
710
|
+
const importName = font.font.import;
|
|
711
|
+
if (!importName) continue;
|
|
712
|
+
const existingImport = sourceFile.getImportDeclaration(
|
|
713
|
+
(decl) => decl.getModuleSpecifierValue() === "next/font/google"
|
|
714
|
+
);
|
|
715
|
+
let hasExistingImport = false;
|
|
716
|
+
if (existingImport) {
|
|
717
|
+
const namedImports = existingImport.getNamedImports();
|
|
718
|
+
hasExistingImport = namedImports.some(
|
|
719
|
+
(imp) => imp.getName() === importName
|
|
680
720
|
);
|
|
721
|
+
if (!hasExistingImport) {
|
|
722
|
+
existingImport.addNamedImport(importName);
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
sourceFile.addImportDeclaration({
|
|
726
|
+
moduleSpecifier: "next/font/google",
|
|
727
|
+
namedImports: [importName]
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
const varName = getFontVariableName(importName, font.font.variable);
|
|
731
|
+
const fontOptions = buildFontOptions(font);
|
|
732
|
+
const existingVarDecl = findFontVariableDeclaration(
|
|
733
|
+
sourceFile,
|
|
734
|
+
font.font.variable
|
|
735
|
+
);
|
|
736
|
+
if (hasExistingImport && !existingVarDecl && isRootFontVariable(font.font.variable)) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (existingVarDecl) {
|
|
740
|
+
existingVarDecl.setInitializer(`${importName}(${fontOptions})`);
|
|
741
|
+
if (existingVarDecl.getName() !== varName) {
|
|
742
|
+
existingVarDecl.rename(varName);
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
const insertPosition = findInsertPosition(sourceFile);
|
|
746
|
+
const statement = sourceFile.insertVariableStatement(insertPosition, {
|
|
747
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
748
|
+
declarations: [
|
|
749
|
+
{
|
|
750
|
+
name: varName,
|
|
751
|
+
initializer: `${importName}(${fontOptions})`
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
});
|
|
755
|
+
statement.appendWhitespace("\n");
|
|
756
|
+
}
|
|
757
|
+
fontVariableNames.push(varName);
|
|
758
|
+
if (shouldApplyFontUtilityToHtml(font)) {
|
|
759
|
+
fontUtilityClasses.push(font.font.variable.replace("--", ""));
|
|
681
760
|
}
|
|
682
|
-
fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
|
|
683
|
-
fs8.copyFileSync(srcPath, destPath);
|
|
684
|
-
written.push(path9.relative(cwd, destPath));
|
|
685
761
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
762
|
+
const fontFamilyClasses = /* @__PURE__ */ new Set(["font-sans", "font-serif", "font-mono"]);
|
|
763
|
+
const lastFontFamilyClass = [...fontUtilityClasses].reverse().find((cls) => fontFamilyClasses.has(cls));
|
|
764
|
+
const filteredUtilityClasses = fontUtilityClasses.filter(
|
|
765
|
+
(cls) => !fontFamilyClasses.has(cls)
|
|
766
|
+
);
|
|
767
|
+
if (lastFontFamilyClass) {
|
|
768
|
+
filteredUtilityClasses.unshift(lastFontFamilyClass);
|
|
769
|
+
}
|
|
770
|
+
if (fontVariableNames.length > 0) {
|
|
771
|
+
updateHtmlClassName(sourceFile, fontVariableNames, filteredUtilityClasses);
|
|
690
772
|
}
|
|
691
|
-
return
|
|
773
|
+
return sourceFile.getFullText();
|
|
692
774
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
2. Add an @font-face block below
|
|
700
|
-
3. Add a --font-* variable in globals.css
|
|
701
|
-
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
702
|
-
|
|
703
|
-
/* \u2500\u2500\u2500 Geist Sans \u2500\u2500\u2500 */
|
|
704
|
-
@font-face {
|
|
705
|
-
font-family: 'Geist';
|
|
706
|
-
src: url('/fonts/geist/Geist-Variable.woff2') format('woff2');
|
|
707
|
-
font-weight: 100 900;
|
|
708
|
-
font-style: normal;
|
|
709
|
-
font-display: swap;
|
|
775
|
+
function buildFontOptions(font) {
|
|
776
|
+
const options = {};
|
|
777
|
+
if (font.font.subsets?.length) options.subsets = font.font.subsets;
|
|
778
|
+
if (font.font.weight?.length) options.weight = font.font.weight;
|
|
779
|
+
options.variable = font.font.variable;
|
|
780
|
+
return JSON.stringify(options).replace(/"([^"]+)":/g, "$1:").replace(/"/g, "'");
|
|
710
781
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
font-family: 'Geist';
|
|
714
|
-
src: url('/fonts/geist/Geist-Italic.woff2') format('woff2');
|
|
715
|
-
font-weight: 100 900;
|
|
716
|
-
font-style: italic;
|
|
717
|
-
font-display: swap;
|
|
782
|
+
function isRootFontVariable(variable) {
|
|
783
|
+
return ROOT_FONT_VARIABLES.has(variable);
|
|
718
784
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
@font-face {
|
|
722
|
-
font-family: 'Geist Mono';
|
|
723
|
-
src: url('/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2');
|
|
724
|
-
font-weight: 100 900;
|
|
725
|
-
font-style: normal;
|
|
726
|
-
font-display: swap;
|
|
785
|
+
function shouldApplyFontUtilityToHtml(font) {
|
|
786
|
+
return !font.font.selector && isRootFontVariable(font.font.variable);
|
|
727
787
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
font-weight: 100 900;
|
|
733
|
-
font-style: italic;
|
|
734
|
-
font-display: swap;
|
|
788
|
+
function getFontVariableName(importName, variable) {
|
|
789
|
+
const baseName = toCamelCase(importName);
|
|
790
|
+
if (isRootFontVariable(variable)) return baseName;
|
|
791
|
+
return `${baseName}${toPascalCase(variable.replace(/^--font-/, ""))}`;
|
|
735
792
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
copyFonts(cwd);
|
|
741
|
-
const fontsPath = path10.join(cssDir, "fonts.css");
|
|
742
|
-
if (!fs9.existsSync(fontsPath)) {
|
|
743
|
-
fs9.mkdirSync(path10.dirname(fontsPath), { recursive: true });
|
|
744
|
-
fs9.writeFileSync(fontsPath, FONTS_CSS_CONTENT, "utf-8");
|
|
745
|
-
console.log(` ${CHECK} Created ${path10.relative(cwd, fontsPath)}`);
|
|
746
|
-
} else {
|
|
747
|
-
console.log(` ${dim(path10.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
|
|
748
|
-
}
|
|
749
|
-
return { preloadTags: PRELOAD_TAGS };
|
|
793
|
+
function toCamelCase(str) {
|
|
794
|
+
return str.split("_").map(
|
|
795
|
+
(part, index) => index === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
796
|
+
).join("");
|
|
750
797
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
import fs10 from "fs";
|
|
754
|
-
import path11 from "path";
|
|
755
|
-
var GEIST_IMPORTS = `import { GeistSans } from "geist/font/sans"
|
|
756
|
-
import { GeistMono } from "geist/font/mono"`;
|
|
757
|
-
function canPatchLayout(content) {
|
|
758
|
-
if (!/<html[\s>]/.test(content)) return false;
|
|
759
|
-
if ((content.match(/<html[\s>]/g) || []).length > 1) return false;
|
|
760
|
-
return true;
|
|
798
|
+
function toPascalCase(str) {
|
|
799
|
+
return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
761
800
|
}
|
|
762
|
-
function
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
801
|
+
function findFontVariableDeclaration(sourceFile, variable) {
|
|
802
|
+
for (const statement of sourceFile.getVariableStatements()) {
|
|
803
|
+
for (const declaration of statement.getDeclarations()) {
|
|
804
|
+
const initializer = declaration.getInitializer();
|
|
805
|
+
if (!initializer || initializer.getKind() !== SyntaxKind.CallExpression)
|
|
806
|
+
continue;
|
|
807
|
+
const args = initializer.getArguments();
|
|
808
|
+
if (args.length === 0) continue;
|
|
809
|
+
const argText = args[0].getText();
|
|
810
|
+
if (argText.includes("variable:") && argText.includes(variable)) {
|
|
811
|
+
return declaration;
|
|
812
|
+
}
|
|
769
813
|
}
|
|
770
814
|
}
|
|
771
|
-
|
|
772
|
-
lines.splice(lastImportIndex + 1, 0, imports);
|
|
773
|
-
} else {
|
|
774
|
-
lines.unshift(imports);
|
|
775
|
-
}
|
|
776
|
-
return lines.join("\n");
|
|
815
|
+
return null;
|
|
777
816
|
}
|
|
778
|
-
function
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
);
|
|
783
|
-
if (templateMatch) {
|
|
784
|
-
const existing = templateMatch[2].trim();
|
|
785
|
-
const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
|
|
786
|
-
return content.replace(
|
|
787
|
-
templateMatch[0],
|
|
788
|
-
`${templateMatch[1]}${newClassName}${templateMatch[3]}`
|
|
789
|
-
);
|
|
817
|
+
function findInsertPosition(sourceFile) {
|
|
818
|
+
const imports = sourceFile.getImportDeclarations();
|
|
819
|
+
if (imports.length > 0) {
|
|
820
|
+
return imports[imports.length - 1].getChildIndex() + 1;
|
|
790
821
|
}
|
|
791
|
-
|
|
792
|
-
|
|
822
|
+
return 0;
|
|
823
|
+
}
|
|
824
|
+
function updateHtmlClassName(sourceFile, fontVariableNames, fontUtilityClasses) {
|
|
825
|
+
const jsxElements = sourceFile.getDescendantsOfKind(
|
|
826
|
+
SyntaxKind.JsxOpeningElement
|
|
793
827
|
);
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
828
|
+
for (const element of jsxElements) {
|
|
829
|
+
if (element.getTagNameNode().getText() !== "html") continue;
|
|
830
|
+
const newUtilityClasses = fontUtilityClasses.map((cls) => `"${cls}"`);
|
|
831
|
+
const newVarExpressions = fontVariableNames.map(
|
|
832
|
+
(name) => `${name}.variable`
|
|
798
833
|
);
|
|
834
|
+
const allNewArgs = [...newUtilityClasses, ...newVarExpressions];
|
|
835
|
+
const classNameAttr = element.getAttribute("className");
|
|
836
|
+
if (!classNameAttr) {
|
|
837
|
+
element.addAttribute({
|
|
838
|
+
name: "className",
|
|
839
|
+
initializer: `{cn(${allNewArgs.join(", ")})}`
|
|
840
|
+
});
|
|
841
|
+
ensureCnImport(sourceFile);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (classNameAttr.getKind() !== SyntaxKind.JsxAttribute) return;
|
|
845
|
+
const jsxAttr = classNameAttr.asKindOrThrow(SyntaxKind.JsxAttribute);
|
|
846
|
+
const initializer = jsxAttr.getInitializer();
|
|
847
|
+
if (!initializer) return;
|
|
848
|
+
if (initializer.getKind() === SyntaxKind.StringLiteral) {
|
|
849
|
+
const currentValue = initializer.getText().slice(1, -1);
|
|
850
|
+
ensureCnImport(sourceFile);
|
|
851
|
+
jsxAttr.setInitializer(
|
|
852
|
+
`{cn("${currentValue}", ${allNewArgs.join(", ")})}`
|
|
853
|
+
);
|
|
854
|
+
} else if (initializer.getKind() === SyntaxKind.JsxExpression) {
|
|
855
|
+
const jsxExpr = initializer.asKindOrThrow(SyntaxKind.JsxExpression);
|
|
856
|
+
const expr = jsxExpr.getExpression();
|
|
857
|
+
if (!expr) return;
|
|
858
|
+
const exprText = expr.getText();
|
|
859
|
+
if (exprText.startsWith("cn(")) {
|
|
860
|
+
const hasAllFontVars = newVarExpressions.every(
|
|
861
|
+
(v) => exprText.includes(v)
|
|
862
|
+
);
|
|
863
|
+
const hasAllUtilityClasses = fontUtilityClasses.every(
|
|
864
|
+
(cls) => exprText.includes(`"${cls}"`)
|
|
865
|
+
);
|
|
866
|
+
if (hasAllFontVars && hasAllUtilityClasses) continue;
|
|
867
|
+
let cleanedExpr = exprText;
|
|
868
|
+
for (const varExpr of newVarExpressions) {
|
|
869
|
+
cleanedExpr = cleanedExpr.replace(
|
|
870
|
+
new RegExp(`,?\\s*${varExpr.replace(".", "\\.")}`, "g"),
|
|
871
|
+
""
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
cleanedExpr = cleanedExpr.replace(/cn\(\s*,/, "cn(");
|
|
875
|
+
const newExpr = cleanedExpr.replace(
|
|
876
|
+
/\)$/,
|
|
877
|
+
`, ${allNewArgs.join(", ")})`
|
|
878
|
+
);
|
|
879
|
+
jsxExpr.replaceWithText(`{${newExpr}}`);
|
|
880
|
+
} else if (/^\w+\.variable$/.test(exprText)) {
|
|
881
|
+
ensureCnImport(sourceFile);
|
|
882
|
+
jsxExpr.replaceWithText(`{cn(${allNewArgs.join(", ")})}`);
|
|
883
|
+
} else if (exprText.startsWith("`") && exprText.endsWith("`")) {
|
|
884
|
+
const cnArgs = parseTemplateLiteralToCnArgs(exprText);
|
|
885
|
+
ensureCnImport(sourceFile);
|
|
886
|
+
const allNewArgsSet = new Set(allNewArgs);
|
|
887
|
+
const fontFamilyLiterals = new Set(
|
|
888
|
+
["font-sans", "font-serif", "font-mono"].map((c) => `"${c}"`)
|
|
889
|
+
);
|
|
890
|
+
const cleanedCnArgs = cnArgs.filter(
|
|
891
|
+
(arg) => !allNewArgsSet.has(arg) && !fontFamilyLiterals.has(arg)
|
|
892
|
+
);
|
|
893
|
+
jsxExpr.replaceWithText(
|
|
894
|
+
`{cn(${[...cleanedCnArgs, ...allNewArgs].join(", ")})}`
|
|
895
|
+
);
|
|
896
|
+
} else {
|
|
897
|
+
ensureCnImport(sourceFile);
|
|
898
|
+
jsxExpr.replaceWithText(`{cn(${exprText}, ${allNewArgs.join(", ")})}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
799
901
|
}
|
|
800
|
-
|
|
801
|
-
|
|
902
|
+
}
|
|
903
|
+
function ensureCnImport(sourceFile) {
|
|
904
|
+
const existingImport = sourceFile.getImportDeclaration(
|
|
905
|
+
(decl) => decl.getNamedImports().some((imp) => imp.getName() === "cn")
|
|
802
906
|
);
|
|
803
|
-
if (
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
return content.replace(
|
|
807
|
-
stringMatch[0],
|
|
808
|
-
`${stringMatch[1]}{\`${newClassName}\`}`
|
|
907
|
+
if (!existingImport) {
|
|
908
|
+
const utilsImport = sourceFile.getImportDeclaration(
|
|
909
|
+
(decl) => decl.getModuleSpecifierValue().includes("/lib/utils")
|
|
809
910
|
);
|
|
911
|
+
if (utilsImport) {
|
|
912
|
+
const namedImports = utilsImport.getNamedImports();
|
|
913
|
+
if (!namedImports.some((imp) => imp.getName() === "cn")) {
|
|
914
|
+
utilsImport.addNamedImport("cn");
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
sourceFile.addImportDeclaration({
|
|
918
|
+
moduleSpecifier: "@/lib/utils",
|
|
919
|
+
namedImports: ["cn"]
|
|
920
|
+
});
|
|
921
|
+
}
|
|
810
922
|
}
|
|
811
|
-
return content.replace(
|
|
812
|
-
/<html(\s)/,
|
|
813
|
-
`<html className={\`${fontClasses}\`}$1`
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
function generateSnippet() {
|
|
817
|
-
return `
|
|
818
|
-
Add these imports to your root layout file:
|
|
819
|
-
|
|
820
|
-
${dim(GEIST_IMPORTS)}
|
|
821
|
-
|
|
822
|
-
Then add the font variables to your <html> tag:
|
|
823
|
-
|
|
824
|
-
${dim("<html className={`${GeistSans.variable} ${GeistMono.variable}`}>")}
|
|
825
|
-
`;
|
|
826
923
|
}
|
|
827
|
-
function
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
fs10.writeFileSync(layoutPath, layoutContent, "utf-8");
|
|
843
|
-
console.log(` ${CHECK} Updated ${layoutFile} with geist/font imports`);
|
|
844
|
-
if (fs10.existsSync(cssPath)) {
|
|
845
|
-
const patched = patchCssVariables(cssPath, {
|
|
846
|
-
"--font-display": "var(--font-geist-sans)",
|
|
847
|
-
"--font-body": "var(--font-geist-sans)"
|
|
848
|
-
});
|
|
849
|
-
if (patched.length > 0) {
|
|
850
|
-
console.log(` ${CHECK} Updated font variables in ${path11.relative(cwd, cssPath)}`);
|
|
924
|
+
function parseTemplateLiteralToCnArgs(templateLiteral) {
|
|
925
|
+
const staticArgs = [];
|
|
926
|
+
const variableArgs = [];
|
|
927
|
+
const content = templateLiteral.slice(1, -1);
|
|
928
|
+
const parts = content.split(/(\$\{[^}]+\})/);
|
|
929
|
+
for (const part of parts) {
|
|
930
|
+
if (!part) continue;
|
|
931
|
+
if (part.startsWith("${") && part.endsWith("}")) {
|
|
932
|
+
const expr = part.slice(2, -1).trim();
|
|
933
|
+
if (expr) variableArgs.push(expr);
|
|
934
|
+
} else {
|
|
935
|
+
const staticParts = part.trim().split(/\s+/).filter(Boolean);
|
|
936
|
+
for (const staticPart of staticParts) {
|
|
937
|
+
staticArgs.push(`"${staticPart}"`);
|
|
938
|
+
}
|
|
851
939
|
}
|
|
852
|
-
removeCssImport(cssPath, "./fonts.css");
|
|
853
940
|
}
|
|
854
|
-
return
|
|
941
|
+
return [...staticArgs, ...variableArgs];
|
|
855
942
|
}
|
|
856
943
|
|
|
857
944
|
// src/commands/init.ts
|
|
858
|
-
|
|
945
|
+
var DEFAULT_FONT_NAMES = ["font-geist", "font-geist-mono"];
|
|
946
|
+
function buildConfig(project, strategy) {
|
|
859
947
|
let cssPath;
|
|
860
948
|
if (project.existingCssFile) {
|
|
861
949
|
cssPath = project.existingCssFile;
|
|
@@ -876,8 +964,7 @@ function buildConfig(project, fontChoice) {
|
|
|
876
964
|
hooks: "@/hooks"
|
|
877
965
|
},
|
|
878
966
|
fonts: {
|
|
879
|
-
strategy
|
|
880
|
-
...fontChoice.useNextFont ? { nextFont: true } : {}
|
|
967
|
+
strategy
|
|
881
968
|
}
|
|
882
969
|
};
|
|
883
970
|
}
|
|
@@ -885,7 +972,7 @@ function stripTailwindCoreImport(content) {
|
|
|
885
972
|
return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
|
|
886
973
|
}
|
|
887
974
|
function insertImportLine(filePath, importStatement) {
|
|
888
|
-
const content =
|
|
975
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
889
976
|
const lines = content.split("\n");
|
|
890
977
|
let lastImportIndex = -1;
|
|
891
978
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -898,17 +985,12 @@ function insertImportLine(filePath, importStatement) {
|
|
|
898
985
|
} else {
|
|
899
986
|
lines.unshift(importStatement);
|
|
900
987
|
}
|
|
901
|
-
|
|
988
|
+
fs9.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
902
989
|
}
|
|
903
|
-
async function
|
|
990
|
+
async function promptFontStrategy(detected, skipPrompts) {
|
|
904
991
|
const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
|
|
905
992
|
const defaultStrategy = hasExistingFonts ? "keep-existing" : "stera-default";
|
|
906
|
-
if (skipPrompts)
|
|
907
|
-
return {
|
|
908
|
-
strategy: defaultStrategy,
|
|
909
|
-
useNextFont: project.framework === "next" && defaultStrategy === "stera-default"
|
|
910
|
-
};
|
|
911
|
-
}
|
|
993
|
+
if (skipPrompts) return defaultStrategy;
|
|
912
994
|
if (hasExistingFonts) {
|
|
913
995
|
const sources = [];
|
|
914
996
|
if (detected.fontFamilies.length > 0) {
|
|
@@ -930,7 +1012,7 @@ async function promptFontChoice(project, detected, skipPrompts) {
|
|
|
930
1012
|
choices.push({
|
|
931
1013
|
value: "stera-default",
|
|
932
1014
|
name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
|
|
933
|
-
description: "
|
|
1015
|
+
description: "Optimized variable fonts via next/font or fontsource"
|
|
934
1016
|
});
|
|
935
1017
|
choices.push({
|
|
936
1018
|
value: "skip",
|
|
@@ -942,56 +1024,79 @@ async function promptFontChoice(project, detected, skipPrompts) {
|
|
|
942
1024
|
choices,
|
|
943
1025
|
default: defaultStrategy
|
|
944
1026
|
});
|
|
945
|
-
let useNextFont = false;
|
|
946
|
-
if (project.framework === "next" && strategy === "stera-default") {
|
|
947
|
-
useNextFont = await confirm({
|
|
948
|
-
message: "Use next/font for automatic optimization? (recommended)",
|
|
949
|
-
default: true
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
1027
|
console.log("");
|
|
953
|
-
return
|
|
1028
|
+
return strategy;
|
|
954
1029
|
}
|
|
955
|
-
async function
|
|
956
|
-
const
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1030
|
+
async function fetchDefaultFonts() {
|
|
1031
|
+
const fonts = [];
|
|
1032
|
+
for (const name of DEFAULT_FONT_NAMES) {
|
|
1033
|
+
try {
|
|
1034
|
+
const item = await fetchRegistryItem(name);
|
|
1035
|
+
if (item.type === "registry:font") {
|
|
1036
|
+
fonts.push(item);
|
|
1037
|
+
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return fonts;
|
|
1042
|
+
}
|
|
1043
|
+
async function executeFontStrategy(strategy, project, detected, config, cwd) {
|
|
1044
|
+
if (strategy === "skip") return;
|
|
1045
|
+
if (strategy === "keep-existing") {
|
|
1046
|
+
const cssPath = path10.resolve(cwd, config.css);
|
|
1047
|
+
const cssDir = path10.dirname(cssPath);
|
|
1048
|
+
if (fs9.existsSync(cssPath)) {
|
|
963
1049
|
const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
|
|
964
1050
|
const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
|
|
965
1051
|
const patched = patchCssVariables(cssPath, {
|
|
966
|
-
"--font-
|
|
967
|
-
"--font-
|
|
1052
|
+
"--font-heading": fontValue,
|
|
1053
|
+
"--font-sans": fontValue
|
|
968
1054
|
});
|
|
969
|
-
const steraUiPath =
|
|
970
|
-
if (
|
|
1055
|
+
const steraUiPath = path10.join(cssDir, "stera-ui.css");
|
|
1056
|
+
if (fs9.existsSync(steraUiPath)) {
|
|
971
1057
|
patchCssVariables(steraUiPath, {
|
|
972
|
-
"--font-
|
|
973
|
-
"--font-
|
|
1058
|
+
"--font-heading": fontValue,
|
|
1059
|
+
"--font-sans": fontValue
|
|
974
1060
|
});
|
|
975
1061
|
}
|
|
976
1062
|
if (patched.length > 0) {
|
|
977
1063
|
console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
|
|
978
1064
|
}
|
|
979
1065
|
}
|
|
980
|
-
return
|
|
1066
|
+
return;
|
|
981
1067
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
1068
|
+
const fonts = await fetchDefaultFonts();
|
|
1069
|
+
if (fonts.length === 0) return;
|
|
1070
|
+
const fontTree = massageTreeForFonts(fonts, project);
|
|
1071
|
+
if (fontTree.dependencies.length > 0) {
|
|
1072
|
+
await installDependencies(fontTree.dependencies, cwd);
|
|
1073
|
+
}
|
|
1074
|
+
if (fontTree.cssImports.length > 0) {
|
|
1075
|
+
const cssPath = path10.resolve(cwd, config.css);
|
|
1076
|
+
const cssDir = path10.dirname(cssPath);
|
|
1077
|
+
const targetCss = fs9.existsSync(path10.join(cssDir, "stera-ui.css")) ? path10.join(cssDir, "stera-ui.css") : cssPath;
|
|
1078
|
+
if (fs9.existsSync(targetCss)) {
|
|
1079
|
+
for (const importLine of fontTree.cssImports) {
|
|
1080
|
+
insertImportLine(targetCss, importLine);
|
|
1081
|
+
}
|
|
1082
|
+
console.log(` ${CHECK} Added font imports to ${path10.relative(cwd, targetCss)}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (Object.keys(fontTree.cssVars).length > 0) {
|
|
1086
|
+
const cssPath = path10.resolve(cwd, config.css);
|
|
1087
|
+
const cssDir = path10.dirname(cssPath);
|
|
1088
|
+
const targetCss = fs9.existsSync(path10.join(cssDir, "stera-ui.css")) ? path10.join(cssDir, "stera-ui.css") : cssPath;
|
|
1089
|
+
if (fs9.existsSync(targetCss)) {
|
|
1090
|
+
const patched = patchCssVariables(targetCss, fontTree.cssVars);
|
|
1091
|
+
if (patched.length > 0) {
|
|
1092
|
+
console.log(` ${CHECK} Updated font variables in ${path10.relative(cwd, targetCss)}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
991
1095
|
}
|
|
1096
|
+
await updateFonts(fonts, project, cwd);
|
|
992
1097
|
}
|
|
993
1098
|
async function init(options) {
|
|
994
|
-
const cwd = options.cwd ?
|
|
1099
|
+
const cwd = options.cwd ? path10.resolve(options.cwd) : process.cwd();
|
|
995
1100
|
console.log(`
|
|
996
1101
|
${LOGO} Stera UI
|
|
997
1102
|
`);
|
|
@@ -1001,8 +1106,8 @@ async function init(options) {
|
|
|
1001
1106
|
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
1002
1107
|
return;
|
|
1003
1108
|
}
|
|
1004
|
-
const pkgPath =
|
|
1005
|
-
if (!
|
|
1109
|
+
const pkgPath = path10.join(cwd, "package.json");
|
|
1110
|
+
if (!fs9.existsSync(pkgPath)) {
|
|
1006
1111
|
console.error(
|
|
1007
1112
|
" Error: No package.json found. Please run this in a project directory."
|
|
1008
1113
|
);
|
|
@@ -1018,35 +1123,34 @@ async function init(options) {
|
|
|
1018
1123
|
`);
|
|
1019
1124
|
}
|
|
1020
1125
|
const detectedFonts = detectFonts(cwd, project);
|
|
1021
|
-
const
|
|
1022
|
-
project,
|
|
1126
|
+
const strategy = await promptFontStrategy(
|
|
1023
1127
|
detectedFonts,
|
|
1024
1128
|
options.yes ?? false
|
|
1025
1129
|
);
|
|
1026
|
-
const config = buildConfig(project,
|
|
1027
|
-
const configPath =
|
|
1028
|
-
|
|
1130
|
+
const config = buildConfig(project, strategy);
|
|
1131
|
+
const configPath = path10.join(cwd, CONFIG_FILE);
|
|
1132
|
+
fs9.writeFileSync(
|
|
1029
1133
|
configPath,
|
|
1030
1134
|
JSON.stringify(config, null, 2) + "\n",
|
|
1031
1135
|
"utf-8"
|
|
1032
1136
|
);
|
|
1033
1137
|
console.log(` ${CHECK} Created components.json`);
|
|
1034
|
-
const cssDir =
|
|
1138
|
+
const cssDir = path10.dirname(path10.resolve(cwd, config.css));
|
|
1035
1139
|
if (project.existingCssFile) {
|
|
1036
1140
|
if (hasGlobalsCss(config, cwd)) {
|
|
1037
1141
|
console.log(
|
|
1038
|
-
` ${CHECK} Stera already initialised ${dim(`(${
|
|
1142
|
+
` ${CHECK} Stera already initialised ${dim(`(${path10.join(path10.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
|
|
1039
1143
|
);
|
|
1040
1144
|
} else {
|
|
1041
1145
|
try {
|
|
1042
1146
|
const globalsItem = await fetchRegistryItem("globals");
|
|
1043
|
-
const globalsContent = globalsItem.files[0]
|
|
1147
|
+
const globalsContent = globalsItem.files?.[0]?.content ?? "";
|
|
1044
1148
|
const steraUiContent = stripTailwindCoreImport(globalsContent);
|
|
1045
|
-
const steraUiPath =
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
console.log(` ${CHECK} Created ${
|
|
1049
|
-
const existingCssPath =
|
|
1149
|
+
const steraUiPath = path10.join(cssDir, "stera-ui.css");
|
|
1150
|
+
fs9.mkdirSync(cssDir, { recursive: true });
|
|
1151
|
+
fs9.writeFileSync(steraUiPath, steraUiContent, "utf-8");
|
|
1152
|
+
console.log(` ${CHECK} Created ${path10.relative(cwd, steraUiPath)}`);
|
|
1153
|
+
const existingCssPath = path10.resolve(cwd, project.existingCssFile);
|
|
1050
1154
|
insertImportLine(existingCssPath, "@import './stera-ui.css';");
|
|
1051
1155
|
console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
|
|
1052
1156
|
} catch {
|
|
@@ -1054,13 +1158,14 @@ async function init(options) {
|
|
|
1054
1158
|
}
|
|
1055
1159
|
} else {
|
|
1056
1160
|
const resolved = await resolveDependencies(["globals"]);
|
|
1057
|
-
const
|
|
1161
|
+
const filtered = resolved.filter((item) => item.name !== "fonts");
|
|
1162
|
+
const { written } = await writeComponentFiles(filtered, config, cwd);
|
|
1058
1163
|
for (const file of written) {
|
|
1059
1164
|
console.log(` ${CHECK} Created ${file}`);
|
|
1060
1165
|
}
|
|
1061
1166
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1167
|
+
await executeFontStrategy(
|
|
1168
|
+
strategy,
|
|
1064
1169
|
project,
|
|
1065
1170
|
detectedFonts,
|
|
1066
1171
|
config,
|
|
@@ -1078,15 +1183,7 @@ async function init(options) {
|
|
|
1078
1183
|
console.log("");
|
|
1079
1184
|
console.log(" Next steps:");
|
|
1080
1185
|
console.log(" stera-ui add <component>");
|
|
1081
|
-
if (
|
|
1082
|
-
console.log("");
|
|
1083
|
-
console.log(` ${dim("For optimal font loading, add these to your HTML <head>:")}`);
|
|
1084
|
-
console.log("");
|
|
1085
|
-
for (const line of fontResult.preloadTags.split("\n")) {
|
|
1086
|
-
console.log(` ${dim(line)}`);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
if (fontChoice.strategy === "skip") {
|
|
1186
|
+
if (strategy === "skip") {
|
|
1090
1187
|
console.log("");
|
|
1091
1188
|
console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
|
|
1092
1189
|
}
|