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.
@@ -334,7 +334,7 @@ async function writeComponentFiles(items, config, projectRoot, options = {}) {
334
334
  const written = [];
335
335
  const skipped = [];
336
336
  for (const item of items) {
337
- for (const file of item.files) {
337
+ for (const file of item.files ?? []) {
338
338
  const outputPath = resolveOutputPath(config, projectRoot, file);
339
339
  const relativePath = path6.relative(projectRoot, outputPath);
340
340
  if (fs5.existsSync(outputPath)) {
@@ -360,6 +360,317 @@ async function writeComponentFiles(items, config, projectRoot, options = {}) {
360
360
  return { written, skipped };
361
361
  }
362
362
 
363
+ // src/utils/update-fonts.ts
364
+ import { existsSync, promises as fs6 } from "fs";
365
+ import path7 from "path";
366
+ import {
367
+ Project,
368
+ ScriptKind,
369
+ SyntaxKind,
370
+ VariableDeclarationKind
371
+ } from "ts-morph";
372
+ var ROOT_FONT_VARIABLES = /* @__PURE__ */ new Set([
373
+ "--font-sans",
374
+ "--font-serif",
375
+ "--font-mono"
376
+ ]);
377
+ function massageTreeForFonts(fonts, project) {
378
+ const result = {
379
+ cssVars: {},
380
+ dependencies: [],
381
+ cssImports: []
382
+ };
383
+ if (fonts.length === 0) return result;
384
+ const isNext = project.framework === "next";
385
+ for (const font of fonts) {
386
+ if (isNext) {
387
+ result.cssVars[font.font.variable] = `var(${font.font.variable})`;
388
+ } else {
389
+ const fontName = font.name.replace(/^font-/, "");
390
+ const fontSourceDep = font.font.dependency ?? `@fontsource-variable/${fontName}`;
391
+ result.dependencies.push(fontSourceDep);
392
+ result.cssImports.push(`@import "${fontSourceDep}";`);
393
+ result.cssVars[font.font.variable] = font.font.family;
394
+ }
395
+ }
396
+ return result;
397
+ }
398
+ async function updateFonts(fonts, project, cwd, options = {}) {
399
+ if (fonts.length === 0) return;
400
+ if (project.framework !== "next") return;
401
+ const layoutPath = findLayoutFile(project, cwd);
402
+ if (!layoutPath) return;
403
+ if (options.silent) {
404
+ try {
405
+ const layoutContent = await fs6.readFile(layoutPath, "utf-8");
406
+ const updatedContent = transformLayoutFonts(layoutContent, fonts);
407
+ if (updatedContent !== layoutContent) {
408
+ await fs6.writeFile(layoutPath, updatedContent, "utf-8");
409
+ }
410
+ } catch {
411
+ }
412
+ return;
413
+ }
414
+ const spinner = createSpinner("Updating fonts");
415
+ try {
416
+ const layoutContent = await fs6.readFile(layoutPath, "utf-8");
417
+ const updatedContent = transformLayoutFonts(layoutContent, fonts);
418
+ if (updatedContent !== layoutContent) {
419
+ await fs6.writeFile(layoutPath, updatedContent, "utf-8");
420
+ }
421
+ spinner.succeed("Updated layout with font imports");
422
+ } catch {
423
+ spinner.fail("Failed to update fonts");
424
+ }
425
+ }
426
+ function findLayoutFile(project, cwd) {
427
+ if (!project.layoutFile) return null;
428
+ const fullPath = path7.join(cwd, project.layoutFile);
429
+ return existsSync(fullPath) ? fullPath : null;
430
+ }
431
+ function transformLayoutFonts(input, fonts) {
432
+ const project = new Project({ compilerOptions: {} });
433
+ const sourceFile = project.createSourceFile("layout.tsx", input, {
434
+ scriptKind: ScriptKind.TSX
435
+ });
436
+ const googleFonts = fonts.filter((f) => f.font.provider === "google");
437
+ const fontVariableNames = [];
438
+ const fontUtilityClasses = [];
439
+ for (const font of googleFonts) {
440
+ const importName = font.font.import;
441
+ if (!importName) continue;
442
+ const existingImport = sourceFile.getImportDeclaration(
443
+ (decl) => decl.getModuleSpecifierValue() === "next/font/google"
444
+ );
445
+ let hasExistingImport = false;
446
+ if (existingImport) {
447
+ const namedImports = existingImport.getNamedImports();
448
+ hasExistingImport = namedImports.some(
449
+ (imp) => imp.getName() === importName
450
+ );
451
+ if (!hasExistingImport) {
452
+ existingImport.addNamedImport(importName);
453
+ }
454
+ } else {
455
+ sourceFile.addImportDeclaration({
456
+ moduleSpecifier: "next/font/google",
457
+ namedImports: [importName]
458
+ });
459
+ }
460
+ const varName = getFontVariableName(importName, font.font.variable);
461
+ const fontOptions = buildFontOptions(font);
462
+ const existingVarDecl = findFontVariableDeclaration(
463
+ sourceFile,
464
+ font.font.variable
465
+ );
466
+ if (hasExistingImport && !existingVarDecl && isRootFontVariable(font.font.variable)) {
467
+ continue;
468
+ }
469
+ if (existingVarDecl) {
470
+ existingVarDecl.setInitializer(`${importName}(${fontOptions})`);
471
+ if (existingVarDecl.getName() !== varName) {
472
+ existingVarDecl.rename(varName);
473
+ }
474
+ } else {
475
+ const insertPosition = findInsertPosition(sourceFile);
476
+ const statement = sourceFile.insertVariableStatement(insertPosition, {
477
+ declarationKind: VariableDeclarationKind.Const,
478
+ declarations: [
479
+ {
480
+ name: varName,
481
+ initializer: `${importName}(${fontOptions})`
482
+ }
483
+ ]
484
+ });
485
+ statement.appendWhitespace("\n");
486
+ }
487
+ fontVariableNames.push(varName);
488
+ if (shouldApplyFontUtilityToHtml(font)) {
489
+ fontUtilityClasses.push(font.font.variable.replace("--", ""));
490
+ }
491
+ }
492
+ const fontFamilyClasses = /* @__PURE__ */ new Set(["font-sans", "font-serif", "font-mono"]);
493
+ const lastFontFamilyClass = [...fontUtilityClasses].reverse().find((cls) => fontFamilyClasses.has(cls));
494
+ const filteredUtilityClasses = fontUtilityClasses.filter(
495
+ (cls) => !fontFamilyClasses.has(cls)
496
+ );
497
+ if (lastFontFamilyClass) {
498
+ filteredUtilityClasses.unshift(lastFontFamilyClass);
499
+ }
500
+ if (fontVariableNames.length > 0) {
501
+ updateHtmlClassName(sourceFile, fontVariableNames, filteredUtilityClasses);
502
+ }
503
+ return sourceFile.getFullText();
504
+ }
505
+ function buildFontOptions(font) {
506
+ const options = {};
507
+ if (font.font.subsets?.length) options.subsets = font.font.subsets;
508
+ if (font.font.weight?.length) options.weight = font.font.weight;
509
+ options.variable = font.font.variable;
510
+ return JSON.stringify(options).replace(/"([^"]+)":/g, "$1:").replace(/"/g, "'");
511
+ }
512
+ function isRootFontVariable(variable) {
513
+ return ROOT_FONT_VARIABLES.has(variable);
514
+ }
515
+ function shouldApplyFontUtilityToHtml(font) {
516
+ return !font.font.selector && isRootFontVariable(font.font.variable);
517
+ }
518
+ function getFontVariableName(importName, variable) {
519
+ const baseName = toCamelCase(importName);
520
+ if (isRootFontVariable(variable)) return baseName;
521
+ return `${baseName}${toPascalCase(variable.replace(/^--font-/, ""))}`;
522
+ }
523
+ function toCamelCase(str) {
524
+ return str.split("_").map(
525
+ (part, index) => index === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
526
+ ).join("");
527
+ }
528
+ function toPascalCase(str) {
529
+ return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
530
+ }
531
+ function findFontVariableDeclaration(sourceFile, variable) {
532
+ for (const statement of sourceFile.getVariableStatements()) {
533
+ for (const declaration of statement.getDeclarations()) {
534
+ const initializer = declaration.getInitializer();
535
+ if (!initializer || initializer.getKind() !== SyntaxKind.CallExpression)
536
+ continue;
537
+ const args = initializer.getArguments();
538
+ if (args.length === 0) continue;
539
+ const argText = args[0].getText();
540
+ if (argText.includes("variable:") && argText.includes(variable)) {
541
+ return declaration;
542
+ }
543
+ }
544
+ }
545
+ return null;
546
+ }
547
+ function findInsertPosition(sourceFile) {
548
+ const imports = sourceFile.getImportDeclarations();
549
+ if (imports.length > 0) {
550
+ return imports[imports.length - 1].getChildIndex() + 1;
551
+ }
552
+ return 0;
553
+ }
554
+ function updateHtmlClassName(sourceFile, fontVariableNames, fontUtilityClasses) {
555
+ const jsxElements = sourceFile.getDescendantsOfKind(
556
+ SyntaxKind.JsxOpeningElement
557
+ );
558
+ for (const element of jsxElements) {
559
+ if (element.getTagNameNode().getText() !== "html") continue;
560
+ const newUtilityClasses = fontUtilityClasses.map((cls) => `"${cls}"`);
561
+ const newVarExpressions = fontVariableNames.map(
562
+ (name) => `${name}.variable`
563
+ );
564
+ const allNewArgs = [...newUtilityClasses, ...newVarExpressions];
565
+ const classNameAttr = element.getAttribute("className");
566
+ if (!classNameAttr) {
567
+ element.addAttribute({
568
+ name: "className",
569
+ initializer: `{cn(${allNewArgs.join(", ")})}`
570
+ });
571
+ ensureCnImport(sourceFile);
572
+ return;
573
+ }
574
+ if (classNameAttr.getKind() !== SyntaxKind.JsxAttribute) return;
575
+ const jsxAttr = classNameAttr.asKindOrThrow(SyntaxKind.JsxAttribute);
576
+ const initializer = jsxAttr.getInitializer();
577
+ if (!initializer) return;
578
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
579
+ const currentValue = initializer.getText().slice(1, -1);
580
+ ensureCnImport(sourceFile);
581
+ jsxAttr.setInitializer(
582
+ `{cn("${currentValue}", ${allNewArgs.join(", ")})}`
583
+ );
584
+ } else if (initializer.getKind() === SyntaxKind.JsxExpression) {
585
+ const jsxExpr = initializer.asKindOrThrow(SyntaxKind.JsxExpression);
586
+ const expr = jsxExpr.getExpression();
587
+ if (!expr) return;
588
+ const exprText = expr.getText();
589
+ if (exprText.startsWith("cn(")) {
590
+ const hasAllFontVars = newVarExpressions.every(
591
+ (v) => exprText.includes(v)
592
+ );
593
+ const hasAllUtilityClasses = fontUtilityClasses.every(
594
+ (cls) => exprText.includes(`"${cls}"`)
595
+ );
596
+ if (hasAllFontVars && hasAllUtilityClasses) continue;
597
+ let cleanedExpr = exprText;
598
+ for (const varExpr of newVarExpressions) {
599
+ cleanedExpr = cleanedExpr.replace(
600
+ new RegExp(`,?\\s*${varExpr.replace(".", "\\.")}`, "g"),
601
+ ""
602
+ );
603
+ }
604
+ cleanedExpr = cleanedExpr.replace(/cn\(\s*,/, "cn(");
605
+ const newExpr = cleanedExpr.replace(
606
+ /\)$/,
607
+ `, ${allNewArgs.join(", ")})`
608
+ );
609
+ jsxExpr.replaceWithText(`{${newExpr}}`);
610
+ } else if (/^\w+\.variable$/.test(exprText)) {
611
+ ensureCnImport(sourceFile);
612
+ jsxExpr.replaceWithText(`{cn(${allNewArgs.join(", ")})}`);
613
+ } else if (exprText.startsWith("`") && exprText.endsWith("`")) {
614
+ const cnArgs = parseTemplateLiteralToCnArgs(exprText);
615
+ ensureCnImport(sourceFile);
616
+ const allNewArgsSet = new Set(allNewArgs);
617
+ const fontFamilyLiterals = new Set(
618
+ ["font-sans", "font-serif", "font-mono"].map((c) => `"${c}"`)
619
+ );
620
+ const cleanedCnArgs = cnArgs.filter(
621
+ (arg) => !allNewArgsSet.has(arg) && !fontFamilyLiterals.has(arg)
622
+ );
623
+ jsxExpr.replaceWithText(
624
+ `{cn(${[...cleanedCnArgs, ...allNewArgs].join(", ")})}`
625
+ );
626
+ } else {
627
+ ensureCnImport(sourceFile);
628
+ jsxExpr.replaceWithText(`{cn(${exprText}, ${allNewArgs.join(", ")})}`);
629
+ }
630
+ }
631
+ }
632
+ }
633
+ function ensureCnImport(sourceFile) {
634
+ const existingImport = sourceFile.getImportDeclaration(
635
+ (decl) => decl.getNamedImports().some((imp) => imp.getName() === "cn")
636
+ );
637
+ if (!existingImport) {
638
+ const utilsImport = sourceFile.getImportDeclaration(
639
+ (decl) => decl.getModuleSpecifierValue().includes("/lib/utils")
640
+ );
641
+ if (utilsImport) {
642
+ const namedImports = utilsImport.getNamedImports();
643
+ if (!namedImports.some((imp) => imp.getName() === "cn")) {
644
+ utilsImport.addNamedImport("cn");
645
+ }
646
+ } else {
647
+ sourceFile.addImportDeclaration({
648
+ moduleSpecifier: "@/lib/utils",
649
+ namedImports: ["cn"]
650
+ });
651
+ }
652
+ }
653
+ }
654
+ function parseTemplateLiteralToCnArgs(templateLiteral) {
655
+ const staticArgs = [];
656
+ const variableArgs = [];
657
+ const content = templateLiteral.slice(1, -1);
658
+ const parts = content.split(/(\$\{[^}]+\})/);
659
+ for (const part of parts) {
660
+ if (!part) continue;
661
+ if (part.startsWith("${") && part.endsWith("}")) {
662
+ const expr = part.slice(2, -1).trim();
663
+ if (expr) variableArgs.push(expr);
664
+ } else {
665
+ const staticParts = part.trim().split(/\s+/).filter(Boolean);
666
+ for (const staticPart of staticParts) {
667
+ staticArgs.push(`"${staticPart}"`);
668
+ }
669
+ }
670
+ }
671
+ return [...staticArgs, ...variableArgs];
672
+ }
673
+
363
674
  export {
364
675
  applyTransforms,
365
676
  transformImports,
@@ -378,5 +689,7 @@ export {
378
689
  createSpinner,
379
690
  detectPackageManager,
380
691
  installDependencies,
381
- writeComponentFiles
692
+ writeComponentFiles,
693
+ massageTreeForFonts,
694
+ updateFonts
382
695
  };
@@ -5,7 +5,8 @@ var registryItemTypeSchema = z.enum([
5
5
  "registry:component",
6
6
  "registry:hook",
7
7
  "registry:lib",
8
- "registry:style"
8
+ "registry:style",
9
+ "registry:font"
9
10
  ]);
10
11
  var registryItemFileSchema = z.object({
11
12
  path: z.string(),
@@ -13,13 +14,33 @@ var registryItemFileSchema = z.object({
13
14
  content: z.string().optional(),
14
15
  target: z.string().optional()
15
16
  });
16
- var registryItemSchema = z.object({
17
+ var registryItemFontSchema = z.object({
18
+ family: z.string(),
19
+ provider: z.literal("google"),
20
+ import: z.string(),
21
+ variable: z.string(),
22
+ weight: z.array(z.string()).optional(),
23
+ subsets: z.array(z.string()).optional(),
24
+ selector: z.string().optional(),
25
+ dependency: z.string().optional()
26
+ });
27
+ var registryItemCommonSchema = z.object({
17
28
  name: z.string(),
18
- type: registryItemTypeSchema,
19
29
  dependencies: z.array(z.string()).optional(),
20
30
  registryDependencies: z.array(z.string()).optional(),
21
- files: z.array(registryItemFileSchema)
31
+ files: z.array(registryItemFileSchema).optional()
22
32
  });
33
+ var registryItemSchema = z.discriminatedUnion("type", [
34
+ registryItemCommonSchema.extend({
35
+ type: z.literal("registry:font"),
36
+ font: registryItemFontSchema,
37
+ files: z.array(registryItemFileSchema).optional()
38
+ }),
39
+ registryItemCommonSchema.extend({
40
+ type: registryItemTypeSchema.exclude(["registry:font"]),
41
+ files: z.array(registryItemFileSchema)
42
+ })
43
+ ]);
23
44
  var registryIndexSchema = z.object({
24
45
  name: z.string(),
25
46
  items: z.array(registryItemSchema)
@@ -44,6 +65,7 @@ var configSchema = z.object({
44
65
  export {
45
66
  registryItemTypeSchema,
46
67
  registryItemFileSchema,
68
+ registryItemFontSchema,
47
69
  registryItemSchema,
48
70
  registryIndexSchema,
49
71
  configSchema