stera-ui 0.2.1 → 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-KIEC5SPW.js → chunk-26HP67ZM.js} +315 -2
- package/dist/{chunk-AXIACJ2C.js → chunk-3KADOWKC.js} +26 -4
- package/dist/cli.js +371 -281
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/schema/index.d.ts +287 -27
- 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/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)) {
|
|
@@ -401,7 +401,7 @@ async function add(components, options) {
|
|
|
401
401
|
}
|
|
402
402
|
const resolved = await resolveDependencies(components);
|
|
403
403
|
for (const item of resolved) {
|
|
404
|
-
for (const file of item.files) {
|
|
404
|
+
for (const file of item.files ?? []) {
|
|
405
405
|
if (file.content) {
|
|
406
406
|
file.content = applyTransforms(
|
|
407
407
|
file.content,
|
|
@@ -451,9 +451,9 @@ async function add(components, options) {
|
|
|
451
451
|
}
|
|
452
452
|
|
|
453
453
|
// src/commands/init.ts
|
|
454
|
-
import
|
|
455
|
-
import
|
|
456
|
-
import { select
|
|
454
|
+
import fs9 from "fs";
|
|
455
|
+
import path10 from "path";
|
|
456
|
+
import { select } from "@inquirer/prompts";
|
|
457
457
|
|
|
458
458
|
// src/utils/detect-project.ts
|
|
459
459
|
import fs5 from "fs";
|
|
@@ -626,243 +626,324 @@ function patchCssVariables(filePath, patches) {
|
|
|
626
626
|
}
|
|
627
627
|
return patched;
|
|
628
628
|
}
|
|
629
|
-
function removeCssImport(filePath, importPath) {
|
|
630
|
-
const content = fs7.readFileSync(filePath, "utf-8");
|
|
631
|
-
const pattern = new RegExp(
|
|
632
|
-
`@import\\s+['"]${escapeRegex(importPath)}['"]\\s*;?\\n?`
|
|
633
|
-
);
|
|
634
|
-
if (!pattern.test(content)) return false;
|
|
635
|
-
fs7.writeFileSync(filePath, content.replace(pattern, ""), "utf-8");
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
629
|
function escapeRegex(str) {
|
|
639
630
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
640
631
|
}
|
|
641
632
|
|
|
642
|
-
// src/utils/
|
|
643
|
-
import
|
|
644
|
-
import path10 from "path";
|
|
645
|
-
|
|
646
|
-
// src/utils/copy-fonts.ts
|
|
647
|
-
import fs8 from "fs";
|
|
633
|
+
// src/utils/update-fonts.ts
|
|
634
|
+
import { existsSync, promises as fs8 } from "fs";
|
|
648
635
|
import path9 from "path";
|
|
649
|
-
import {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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;
|
|
673
667
|
}
|
|
674
|
-
function
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
|
687
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("--", ""));
|
|
688
760
|
}
|
|
689
|
-
fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
|
|
690
|
-
fs8.copyFileSync(srcPath, destPath);
|
|
691
|
-
written.push(path9.relative(cwd, destPath));
|
|
692
761
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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);
|
|
697
772
|
}
|
|
698
|
-
return
|
|
773
|
+
return sourceFile.getFullText();
|
|
699
774
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
2. Add an @font-face block below
|
|
707
|
-
3. Add a --font-* variable in globals.css
|
|
708
|
-
\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 */
|
|
709
|
-
|
|
710
|
-
/* \u2500\u2500\u2500 Geist Sans \u2500\u2500\u2500 */
|
|
711
|
-
@font-face {
|
|
712
|
-
font-family: 'Geist';
|
|
713
|
-
src: url('/fonts/geist/Geist-Variable.woff2') format('woff2');
|
|
714
|
-
font-weight: 100 900;
|
|
715
|
-
font-style: normal;
|
|
716
|
-
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, "'");
|
|
717
781
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
font-family: 'Geist';
|
|
721
|
-
src: url('/fonts/geist/Geist-Italic.woff2') format('woff2');
|
|
722
|
-
font-weight: 100 900;
|
|
723
|
-
font-style: italic;
|
|
724
|
-
font-display: swap;
|
|
782
|
+
function isRootFontVariable(variable) {
|
|
783
|
+
return ROOT_FONT_VARIABLES.has(variable);
|
|
725
784
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
@font-face {
|
|
729
|
-
font-family: 'Geist Mono';
|
|
730
|
-
src: url('/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2');
|
|
731
|
-
font-weight: 100 900;
|
|
732
|
-
font-style: normal;
|
|
733
|
-
font-display: swap;
|
|
785
|
+
function shouldApplyFontUtilityToHtml(font) {
|
|
786
|
+
return !font.font.selector && isRootFontVariable(font.font.variable);
|
|
734
787
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
font-weight: 100 900;
|
|
740
|
-
font-style: italic;
|
|
741
|
-
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-/, ""))}`;
|
|
742
792
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
copyFonts(cwd);
|
|
748
|
-
const fontsPath = path10.join(cssDir, "fonts.css");
|
|
749
|
-
if (!fs9.existsSync(fontsPath)) {
|
|
750
|
-
fs9.mkdirSync(path10.dirname(fontsPath), { recursive: true });
|
|
751
|
-
fs9.writeFileSync(fontsPath, FONTS_CSS_CONTENT, "utf-8");
|
|
752
|
-
console.log(` ${CHECK} Created ${path10.relative(cwd, fontsPath)}`);
|
|
753
|
-
} else {
|
|
754
|
-
console.log(` ${dim(path10.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
|
|
755
|
-
}
|
|
756
|
-
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("");
|
|
757
797
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
import fs10 from "fs";
|
|
761
|
-
import path11 from "path";
|
|
762
|
-
var GEIST_IMPORTS = `import { GeistSans } from "geist/font/sans"
|
|
763
|
-
import { GeistMono } from "geist/font/mono"`;
|
|
764
|
-
function canPatchLayout(content) {
|
|
765
|
-
if (!/<html[\s>]/.test(content)) return false;
|
|
766
|
-
if ((content.match(/<html[\s>]/g) || []).length > 1) return false;
|
|
767
|
-
return true;
|
|
798
|
+
function toPascalCase(str) {
|
|
799
|
+
return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
768
800
|
}
|
|
769
|
-
function
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
+
}
|
|
776
813
|
}
|
|
777
814
|
}
|
|
778
|
-
|
|
779
|
-
lines.splice(lastImportIndex + 1, 0, imports);
|
|
780
|
-
} else {
|
|
781
|
-
lines.unshift(imports);
|
|
782
|
-
}
|
|
783
|
-
return lines.join("\n");
|
|
815
|
+
return null;
|
|
784
816
|
}
|
|
785
|
-
function
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
);
|
|
790
|
-
if (templateMatch) {
|
|
791
|
-
const existing = templateMatch[2].trim();
|
|
792
|
-
const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
|
|
793
|
-
return content.replace(
|
|
794
|
-
templateMatch[0],
|
|
795
|
-
`${templateMatch[1]}${newClassName}${templateMatch[3]}`
|
|
796
|
-
);
|
|
817
|
+
function findInsertPosition(sourceFile) {
|
|
818
|
+
const imports = sourceFile.getImportDeclarations();
|
|
819
|
+
if (imports.length > 0) {
|
|
820
|
+
return imports[imports.length - 1].getChildIndex() + 1;
|
|
797
821
|
}
|
|
798
|
-
|
|
799
|
-
|
|
822
|
+
return 0;
|
|
823
|
+
}
|
|
824
|
+
function updateHtmlClassName(sourceFile, fontVariableNames, fontUtilityClasses) {
|
|
825
|
+
const jsxElements = sourceFile.getDescendantsOfKind(
|
|
826
|
+
SyntaxKind.JsxOpeningElement
|
|
800
827
|
);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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`
|
|
805
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
|
+
}
|
|
806
901
|
}
|
|
807
|
-
|
|
808
|
-
|
|
902
|
+
}
|
|
903
|
+
function ensureCnImport(sourceFile) {
|
|
904
|
+
const existingImport = sourceFile.getImportDeclaration(
|
|
905
|
+
(decl) => decl.getNamedImports().some((imp) => imp.getName() === "cn")
|
|
809
906
|
);
|
|
810
|
-
if (
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
return content.replace(
|
|
814
|
-
stringMatch[0],
|
|
815
|
-
`${stringMatch[1]}{\`${newClassName}\`}`
|
|
907
|
+
if (!existingImport) {
|
|
908
|
+
const utilsImport = sourceFile.getImportDeclaration(
|
|
909
|
+
(decl) => decl.getModuleSpecifierValue().includes("/lib/utils")
|
|
816
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
|
+
}
|
|
817
922
|
}
|
|
818
|
-
return content.replace(
|
|
819
|
-
/<html(\s)/,
|
|
820
|
-
`<html className={\`${fontClasses}\`}$1`
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
function generateSnippet() {
|
|
824
|
-
return `
|
|
825
|
-
Add these imports to your root layout file:
|
|
826
|
-
|
|
827
|
-
${dim(GEIST_IMPORTS)}
|
|
828
|
-
|
|
829
|
-
Then add the font variables to your <html> tag:
|
|
830
|
-
|
|
831
|
-
${dim("<html className={`${GeistSans.variable} ${GeistMono.variable}`}>")}
|
|
832
|
-
`;
|
|
833
923
|
}
|
|
834
|
-
function
|
|
835
|
-
const
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
fs10.writeFileSync(layoutPath, layoutContent, "utf-8");
|
|
850
|
-
console.log(` ${CHECK} Updated ${layoutFile} with geist/font imports`);
|
|
851
|
-
if (fs10.existsSync(cssPath)) {
|
|
852
|
-
const patched = patchCssVariables(cssPath, {
|
|
853
|
-
"--font-display": "var(--font-geist-sans)",
|
|
854
|
-
"--font-body": "var(--font-geist-sans)"
|
|
855
|
-
});
|
|
856
|
-
if (patched.length > 0) {
|
|
857
|
-
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
|
+
}
|
|
858
939
|
}
|
|
859
|
-
removeCssImport(cssPath, "./fonts.css");
|
|
860
940
|
}
|
|
861
|
-
return
|
|
941
|
+
return [...staticArgs, ...variableArgs];
|
|
862
942
|
}
|
|
863
943
|
|
|
864
944
|
// src/commands/init.ts
|
|
865
|
-
|
|
945
|
+
var DEFAULT_FONT_NAMES = ["font-geist", "font-geist-mono"];
|
|
946
|
+
function buildConfig(project, strategy) {
|
|
866
947
|
let cssPath;
|
|
867
948
|
if (project.existingCssFile) {
|
|
868
949
|
cssPath = project.existingCssFile;
|
|
@@ -883,8 +964,7 @@ function buildConfig(project, fontChoice) {
|
|
|
883
964
|
hooks: "@/hooks"
|
|
884
965
|
},
|
|
885
966
|
fonts: {
|
|
886
|
-
strategy
|
|
887
|
-
...fontChoice.useNextFont ? { nextFont: true } : {}
|
|
967
|
+
strategy
|
|
888
968
|
}
|
|
889
969
|
};
|
|
890
970
|
}
|
|
@@ -892,7 +972,7 @@ function stripTailwindCoreImport(content) {
|
|
|
892
972
|
return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
|
|
893
973
|
}
|
|
894
974
|
function insertImportLine(filePath, importStatement) {
|
|
895
|
-
const content =
|
|
975
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
896
976
|
const lines = content.split("\n");
|
|
897
977
|
let lastImportIndex = -1;
|
|
898
978
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -905,17 +985,12 @@ function insertImportLine(filePath, importStatement) {
|
|
|
905
985
|
} else {
|
|
906
986
|
lines.unshift(importStatement);
|
|
907
987
|
}
|
|
908
|
-
|
|
988
|
+
fs9.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
909
989
|
}
|
|
910
|
-
async function
|
|
990
|
+
async function promptFontStrategy(detected, skipPrompts) {
|
|
911
991
|
const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
|
|
912
992
|
const defaultStrategy = hasExistingFonts ? "keep-existing" : "stera-default";
|
|
913
|
-
if (skipPrompts)
|
|
914
|
-
return {
|
|
915
|
-
strategy: defaultStrategy,
|
|
916
|
-
useNextFont: project.framework === "next" && defaultStrategy === "stera-default"
|
|
917
|
-
};
|
|
918
|
-
}
|
|
993
|
+
if (skipPrompts) return defaultStrategy;
|
|
919
994
|
if (hasExistingFonts) {
|
|
920
995
|
const sources = [];
|
|
921
996
|
if (detected.fontFamilies.length > 0) {
|
|
@@ -937,7 +1012,7 @@ async function promptFontChoice(project, detected, skipPrompts) {
|
|
|
937
1012
|
choices.push({
|
|
938
1013
|
value: "stera-default",
|
|
939
1014
|
name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
|
|
940
|
-
description: "
|
|
1015
|
+
description: "Optimized variable fonts via next/font or fontsource"
|
|
941
1016
|
});
|
|
942
1017
|
choices.push({
|
|
943
1018
|
value: "skip",
|
|
@@ -949,56 +1024,79 @@ async function promptFontChoice(project, detected, skipPrompts) {
|
|
|
949
1024
|
choices,
|
|
950
1025
|
default: defaultStrategy
|
|
951
1026
|
});
|
|
952
|
-
let useNextFont = false;
|
|
953
|
-
if (project.framework === "next" && strategy === "stera-default") {
|
|
954
|
-
useNextFont = await confirm({
|
|
955
|
-
message: "Use next/font for automatic optimization? (recommended)",
|
|
956
|
-
default: true
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
1027
|
console.log("");
|
|
960
|
-
return
|
|
1028
|
+
return strategy;
|
|
961
1029
|
}
|
|
962
|
-
async function
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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)) {
|
|
970
1049
|
const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
|
|
971
1050
|
const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
|
|
972
1051
|
const patched = patchCssVariables(cssPath, {
|
|
973
|
-
"--font-
|
|
974
|
-
"--font-
|
|
1052
|
+
"--font-heading": fontValue,
|
|
1053
|
+
"--font-sans": fontValue
|
|
975
1054
|
});
|
|
976
|
-
const steraUiPath =
|
|
977
|
-
if (
|
|
1055
|
+
const steraUiPath = path10.join(cssDir, "stera-ui.css");
|
|
1056
|
+
if (fs9.existsSync(steraUiPath)) {
|
|
978
1057
|
patchCssVariables(steraUiPath, {
|
|
979
|
-
"--font-
|
|
980
|
-
"--font-
|
|
1058
|
+
"--font-heading": fontValue,
|
|
1059
|
+
"--font-sans": fontValue
|
|
981
1060
|
});
|
|
982
1061
|
}
|
|
983
1062
|
if (patched.length > 0) {
|
|
984
1063
|
console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
|
|
985
1064
|
}
|
|
986
1065
|
}
|
|
987
|
-
return
|
|
1066
|
+
return;
|
|
988
1067
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
|
|
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
|
+
}
|
|
998
1095
|
}
|
|
1096
|
+
await updateFonts(fonts, project, cwd);
|
|
999
1097
|
}
|
|
1000
1098
|
async function init(options) {
|
|
1001
|
-
const cwd = options.cwd ?
|
|
1099
|
+
const cwd = options.cwd ? path10.resolve(options.cwd) : process.cwd();
|
|
1002
1100
|
console.log(`
|
|
1003
1101
|
${LOGO} Stera UI
|
|
1004
1102
|
`);
|
|
@@ -1008,8 +1106,8 @@ async function init(options) {
|
|
|
1008
1106
|
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
1009
1107
|
return;
|
|
1010
1108
|
}
|
|
1011
|
-
const pkgPath =
|
|
1012
|
-
if (!
|
|
1109
|
+
const pkgPath = path10.join(cwd, "package.json");
|
|
1110
|
+
if (!fs9.existsSync(pkgPath)) {
|
|
1013
1111
|
console.error(
|
|
1014
1112
|
" Error: No package.json found. Please run this in a project directory."
|
|
1015
1113
|
);
|
|
@@ -1025,35 +1123,34 @@ async function init(options) {
|
|
|
1025
1123
|
`);
|
|
1026
1124
|
}
|
|
1027
1125
|
const detectedFonts = detectFonts(cwd, project);
|
|
1028
|
-
const
|
|
1029
|
-
project,
|
|
1126
|
+
const strategy = await promptFontStrategy(
|
|
1030
1127
|
detectedFonts,
|
|
1031
1128
|
options.yes ?? false
|
|
1032
1129
|
);
|
|
1033
|
-
const config = buildConfig(project,
|
|
1034
|
-
const configPath =
|
|
1035
|
-
|
|
1130
|
+
const config = buildConfig(project, strategy);
|
|
1131
|
+
const configPath = path10.join(cwd, CONFIG_FILE);
|
|
1132
|
+
fs9.writeFileSync(
|
|
1036
1133
|
configPath,
|
|
1037
1134
|
JSON.stringify(config, null, 2) + "\n",
|
|
1038
1135
|
"utf-8"
|
|
1039
1136
|
);
|
|
1040
1137
|
console.log(` ${CHECK} Created components.json`);
|
|
1041
|
-
const cssDir =
|
|
1138
|
+
const cssDir = path10.dirname(path10.resolve(cwd, config.css));
|
|
1042
1139
|
if (project.existingCssFile) {
|
|
1043
1140
|
if (hasGlobalsCss(config, cwd)) {
|
|
1044
1141
|
console.log(
|
|
1045
|
-
` ${CHECK} Stera already initialised ${dim(`(${
|
|
1142
|
+
` ${CHECK} Stera already initialised ${dim(`(${path10.join(path10.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
|
|
1046
1143
|
);
|
|
1047
1144
|
} else {
|
|
1048
1145
|
try {
|
|
1049
1146
|
const globalsItem = await fetchRegistryItem("globals");
|
|
1050
|
-
const globalsContent = globalsItem.files[0]
|
|
1147
|
+
const globalsContent = globalsItem.files?.[0]?.content ?? "";
|
|
1051
1148
|
const steraUiContent = stripTailwindCoreImport(globalsContent);
|
|
1052
|
-
const steraUiPath =
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
console.log(` ${CHECK} Created ${
|
|
1056
|
-
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);
|
|
1057
1154
|
insertImportLine(existingCssPath, "@import './stera-ui.css';");
|
|
1058
1155
|
console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
|
|
1059
1156
|
} catch {
|
|
@@ -1061,13 +1158,14 @@ async function init(options) {
|
|
|
1061
1158
|
}
|
|
1062
1159
|
} else {
|
|
1063
1160
|
const resolved = await resolveDependencies(["globals"]);
|
|
1064
|
-
const
|
|
1161
|
+
const filtered = resolved.filter((item) => item.name !== "fonts");
|
|
1162
|
+
const { written } = await writeComponentFiles(filtered, config, cwd);
|
|
1065
1163
|
for (const file of written) {
|
|
1066
1164
|
console.log(` ${CHECK} Created ${file}`);
|
|
1067
1165
|
}
|
|
1068
1166
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1167
|
+
await executeFontStrategy(
|
|
1168
|
+
strategy,
|
|
1071
1169
|
project,
|
|
1072
1170
|
detectedFonts,
|
|
1073
1171
|
config,
|
|
@@ -1085,15 +1183,7 @@ async function init(options) {
|
|
|
1085
1183
|
console.log("");
|
|
1086
1184
|
console.log(" Next steps:");
|
|
1087
1185
|
console.log(" stera-ui add <component>");
|
|
1088
|
-
if (
|
|
1089
|
-
console.log("");
|
|
1090
|
-
console.log(` ${dim("For optimal font loading, add these to your HTML <head>:")}`);
|
|
1091
|
-
console.log("");
|
|
1092
|
-
for (const line of fontResult.preloadTags.split("\n")) {
|
|
1093
|
-
console.log(` ${dim(line)}`);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
if (fontChoice.strategy === "skip") {
|
|
1186
|
+
if (strategy === "skip") {
|
|
1097
1187
|
console.log("");
|
|
1098
1188
|
console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
|
|
1099
1189
|
}
|