stera-ui 0.2.1 → 0.3.1
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 +394 -284
- 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);
|
|
697
769
|
}
|
|
698
|
-
|
|
770
|
+
if (fontVariableNames.length > 0) {
|
|
771
|
+
updateHtmlClassName(sourceFile, fontVariableNames, filteredUtilityClasses);
|
|
772
|
+
}
|
|
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
923
|
}
|
|
823
|
-
function
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (layoutContent.includes("geist/font/sans") || layoutContent.includes("GeistSans")) {
|
|
839
|
-
console.log(` ${dim("geist/font already configured in " + layoutFile)}`);
|
|
840
|
-
return true;
|
|
841
|
-
}
|
|
842
|
-
if (!canPatchLayout(layoutContent)) {
|
|
843
|
-
console.log(` ${WARN} Could not auto-patch ${layoutFile}`);
|
|
844
|
-
console.log(generateSnippet());
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
layoutContent = insertImports(layoutContent, GEIST_IMPORTS);
|
|
848
|
-
layoutContent = addClassNameToHtml(layoutContent);
|
|
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,16 +964,32 @@ 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
|
}
|
|
891
|
-
function
|
|
892
|
-
|
|
971
|
+
function stripImportLines(content) {
|
|
972
|
+
const lines = content.split("\n");
|
|
973
|
+
const imports = [];
|
|
974
|
+
const kept = [];
|
|
975
|
+
for (const line of lines) {
|
|
976
|
+
if (line.trim().match(/^@import\s+["'].+["']/)) {
|
|
977
|
+
if (!line.trim().match(/^@import\s+["']tailwindcss["']/)) {
|
|
978
|
+
imports.push(line.trim());
|
|
979
|
+
}
|
|
980
|
+
} else {
|
|
981
|
+
kept.push(line);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return { content: kept.join("\n"), imports };
|
|
893
985
|
}
|
|
894
986
|
function insertImportLine(filePath, importStatement) {
|
|
895
|
-
const content =
|
|
987
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
988
|
+
const importPath = importStatement.match(/["']([^"']+)["']/)?.[1];
|
|
989
|
+
if (importPath) {
|
|
990
|
+
const escaped = importPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
991
|
+
if (new RegExp(`@import\\s+["']${escaped}["']`).test(content)) return;
|
|
992
|
+
}
|
|
896
993
|
const lines = content.split("\n");
|
|
897
994
|
let lastImportIndex = -1;
|
|
898
995
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -905,17 +1002,12 @@ function insertImportLine(filePath, importStatement) {
|
|
|
905
1002
|
} else {
|
|
906
1003
|
lines.unshift(importStatement);
|
|
907
1004
|
}
|
|
908
|
-
|
|
1005
|
+
fs9.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
909
1006
|
}
|
|
910
|
-
async function
|
|
1007
|
+
async function promptFontStrategy(detected, skipPrompts) {
|
|
911
1008
|
const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
|
|
912
1009
|
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
|
-
}
|
|
1010
|
+
if (skipPrompts) return defaultStrategy;
|
|
919
1011
|
if (hasExistingFonts) {
|
|
920
1012
|
const sources = [];
|
|
921
1013
|
if (detected.fontFamilies.length > 0) {
|
|
@@ -937,7 +1029,7 @@ async function promptFontChoice(project, detected, skipPrompts) {
|
|
|
937
1029
|
choices.push({
|
|
938
1030
|
value: "stera-default",
|
|
939
1031
|
name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
|
|
940
|
-
description: "
|
|
1032
|
+
description: "Optimized variable fonts via next/font or fontsource"
|
|
941
1033
|
});
|
|
942
1034
|
choices.push({
|
|
943
1035
|
value: "skip",
|
|
@@ -949,56 +1041,79 @@ async function promptFontChoice(project, detected, skipPrompts) {
|
|
|
949
1041
|
choices,
|
|
950
1042
|
default: defaultStrategy
|
|
951
1043
|
});
|
|
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
1044
|
console.log("");
|
|
960
|
-
return
|
|
1045
|
+
return strategy;
|
|
1046
|
+
}
|
|
1047
|
+
async function fetchDefaultFonts() {
|
|
1048
|
+
const fonts = [];
|
|
1049
|
+
for (const name of DEFAULT_FONT_NAMES) {
|
|
1050
|
+
try {
|
|
1051
|
+
const item = await fetchRegistryItem(name);
|
|
1052
|
+
if (item.type === "registry:font") {
|
|
1053
|
+
fonts.push(item);
|
|
1054
|
+
}
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return fonts;
|
|
961
1059
|
}
|
|
962
|
-
async function executeFontStrategy(
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (fontChoice.strategy === "keep-existing") {
|
|
969
|
-
if (fs11.existsSync(cssPath)) {
|
|
1060
|
+
async function executeFontStrategy(strategy, project, detected, config, cwd) {
|
|
1061
|
+
if (strategy === "skip") return;
|
|
1062
|
+
if (strategy === "keep-existing") {
|
|
1063
|
+
const cssPath = path10.resolve(cwd, config.css);
|
|
1064
|
+
const cssDir = path10.dirname(cssPath);
|
|
1065
|
+
if (fs9.existsSync(cssPath)) {
|
|
970
1066
|
const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
|
|
971
1067
|
const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
|
|
972
1068
|
const patched = patchCssVariables(cssPath, {
|
|
973
|
-
"--font-
|
|
974
|
-
"--font-
|
|
1069
|
+
"--font-heading": fontValue,
|
|
1070
|
+
"--font-sans": fontValue
|
|
975
1071
|
});
|
|
976
|
-
const steraUiPath =
|
|
977
|
-
if (
|
|
1072
|
+
const steraUiPath = path10.join(cssDir, "stera-ui.css");
|
|
1073
|
+
if (fs9.existsSync(steraUiPath)) {
|
|
978
1074
|
patchCssVariables(steraUiPath, {
|
|
979
|
-
"--font-
|
|
980
|
-
"--font-
|
|
1075
|
+
"--font-heading": fontValue,
|
|
1076
|
+
"--font-sans": fontValue
|
|
981
1077
|
});
|
|
982
1078
|
}
|
|
983
1079
|
if (patched.length > 0) {
|
|
984
1080
|
console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
|
|
985
1081
|
}
|
|
986
1082
|
}
|
|
987
|
-
return
|
|
1083
|
+
return;
|
|
988
1084
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
|
|
1085
|
+
const fonts = await fetchDefaultFonts();
|
|
1086
|
+
if (fonts.length === 0) return;
|
|
1087
|
+
const fontTree = massageTreeForFonts(fonts, project);
|
|
1088
|
+
if (fontTree.dependencies.length > 0) {
|
|
1089
|
+
await installDependencies(fontTree.dependencies, cwd);
|
|
1090
|
+
}
|
|
1091
|
+
if (fontTree.cssImports.length > 0) {
|
|
1092
|
+
const cssPath = path10.resolve(cwd, config.css);
|
|
1093
|
+
const cssDir = path10.dirname(cssPath);
|
|
1094
|
+
const targetCss = fs9.existsSync(path10.join(cssDir, "stera-ui.css")) ? path10.join(cssDir, "stera-ui.css") : cssPath;
|
|
1095
|
+
if (fs9.existsSync(targetCss)) {
|
|
1096
|
+
for (const importLine of fontTree.cssImports) {
|
|
1097
|
+
insertImportLine(targetCss, importLine);
|
|
1098
|
+
}
|
|
1099
|
+
console.log(` ${CHECK} Added font imports to ${path10.relative(cwd, targetCss)}`);
|
|
1100
|
+
}
|
|
998
1101
|
}
|
|
1102
|
+
if (Object.keys(fontTree.cssVars).length > 0) {
|
|
1103
|
+
const cssPath = path10.resolve(cwd, config.css);
|
|
1104
|
+
const cssDir = path10.dirname(cssPath);
|
|
1105
|
+
const targetCss = fs9.existsSync(path10.join(cssDir, "stera-ui.css")) ? path10.join(cssDir, "stera-ui.css") : cssPath;
|
|
1106
|
+
if (fs9.existsSync(targetCss)) {
|
|
1107
|
+
const patched = patchCssVariables(targetCss, fontTree.cssVars);
|
|
1108
|
+
if (patched.length > 0) {
|
|
1109
|
+
console.log(` ${CHECK} Updated font variables in ${path10.relative(cwd, targetCss)}`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
await updateFonts(fonts, project, cwd);
|
|
999
1114
|
}
|
|
1000
1115
|
async function init(options) {
|
|
1001
|
-
const cwd = options.cwd ?
|
|
1116
|
+
const cwd = options.cwd ? path10.resolve(options.cwd) : process.cwd();
|
|
1002
1117
|
console.log(`
|
|
1003
1118
|
${LOGO} Stera UI
|
|
1004
1119
|
`);
|
|
@@ -1008,8 +1123,8 @@ async function init(options) {
|
|
|
1008
1123
|
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
1009
1124
|
return;
|
|
1010
1125
|
}
|
|
1011
|
-
const pkgPath =
|
|
1012
|
-
if (!
|
|
1126
|
+
const pkgPath = path10.join(cwd, "package.json");
|
|
1127
|
+
if (!fs9.existsSync(pkgPath)) {
|
|
1013
1128
|
console.error(
|
|
1014
1129
|
" Error: No package.json found. Please run this in a project directory."
|
|
1015
1130
|
);
|
|
@@ -1025,35 +1140,37 @@ async function init(options) {
|
|
|
1025
1140
|
`);
|
|
1026
1141
|
}
|
|
1027
1142
|
const detectedFonts = detectFonts(cwd, project);
|
|
1028
|
-
const
|
|
1029
|
-
project,
|
|
1143
|
+
const strategy = await promptFontStrategy(
|
|
1030
1144
|
detectedFonts,
|
|
1031
1145
|
options.yes ?? false
|
|
1032
1146
|
);
|
|
1033
|
-
const config = buildConfig(project,
|
|
1034
|
-
const configPath =
|
|
1035
|
-
|
|
1147
|
+
const config = buildConfig(project, strategy);
|
|
1148
|
+
const configPath = path10.join(cwd, CONFIG_FILE);
|
|
1149
|
+
fs9.writeFileSync(
|
|
1036
1150
|
configPath,
|
|
1037
1151
|
JSON.stringify(config, null, 2) + "\n",
|
|
1038
1152
|
"utf-8"
|
|
1039
1153
|
);
|
|
1040
1154
|
console.log(` ${CHECK} Created components.json`);
|
|
1041
|
-
const cssDir =
|
|
1155
|
+
const cssDir = path10.dirname(path10.resolve(cwd, config.css));
|
|
1042
1156
|
if (project.existingCssFile) {
|
|
1043
1157
|
if (hasGlobalsCss(config, cwd)) {
|
|
1044
1158
|
console.log(
|
|
1045
|
-
` ${CHECK} Stera already initialised ${dim(`(${
|
|
1159
|
+
` ${CHECK} Stera already initialised ${dim(`(${path10.join(path10.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
|
|
1046
1160
|
);
|
|
1047
1161
|
} else {
|
|
1048
1162
|
try {
|
|
1049
1163
|
const globalsItem = await fetchRegistryItem("globals");
|
|
1050
|
-
const globalsContent = globalsItem.files[0]
|
|
1051
|
-
const steraUiContent =
|
|
1052
|
-
const steraUiPath =
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
console.log(` ${CHECK} Created ${
|
|
1056
|
-
const existingCssPath =
|
|
1164
|
+
const globalsContent = globalsItem.files?.[0]?.content ?? "";
|
|
1165
|
+
const { content: steraUiContent, imports } = stripImportLines(globalsContent);
|
|
1166
|
+
const steraUiPath = path10.join(cssDir, "stera-ui.css");
|
|
1167
|
+
fs9.mkdirSync(cssDir, { recursive: true });
|
|
1168
|
+
fs9.writeFileSync(steraUiPath, steraUiContent, "utf-8");
|
|
1169
|
+
console.log(` ${CHECK} Created ${path10.relative(cwd, steraUiPath)}`);
|
|
1170
|
+
const existingCssPath = path10.resolve(cwd, project.existingCssFile);
|
|
1171
|
+
for (const imp of imports) {
|
|
1172
|
+
insertImportLine(existingCssPath, imp);
|
|
1173
|
+
}
|
|
1057
1174
|
insertImportLine(existingCssPath, "@import './stera-ui.css';");
|
|
1058
1175
|
console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
|
|
1059
1176
|
} catch {
|
|
@@ -1061,13 +1178,14 @@ async function init(options) {
|
|
|
1061
1178
|
}
|
|
1062
1179
|
} else {
|
|
1063
1180
|
const resolved = await resolveDependencies(["globals"]);
|
|
1064
|
-
const
|
|
1181
|
+
const filtered = resolved.filter((item) => item.name !== "fonts");
|
|
1182
|
+
const { written } = await writeComponentFiles(filtered, config, cwd);
|
|
1065
1183
|
for (const file of written) {
|
|
1066
1184
|
console.log(` ${CHECK} Created ${file}`);
|
|
1067
1185
|
}
|
|
1068
1186
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1187
|
+
await executeFontStrategy(
|
|
1188
|
+
strategy,
|
|
1071
1189
|
project,
|
|
1072
1190
|
detectedFonts,
|
|
1073
1191
|
config,
|
|
@@ -1085,15 +1203,7 @@ async function init(options) {
|
|
|
1085
1203
|
console.log("");
|
|
1086
1204
|
console.log(" Next steps:");
|
|
1087
1205
|
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") {
|
|
1206
|
+
if (strategy === "skip") {
|
|
1097
1207
|
console.log("");
|
|
1098
1208
|
console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
|
|
1099
1209
|
}
|