banhaten 0.1.1 → 0.1.2

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.
Files changed (37) hide show
  1. package/README.md +20 -8
  2. package/package.json +8 -2
  3. package/registry/components/autocomplete.tsx +637 -0
  4. package/registry/components/avatar.tsx +258 -22
  5. package/registry/components/badge.tsx +97 -35
  6. package/registry/components/date-picker-state.ts +253 -0
  7. package/registry/components/date-picker.tsx +115 -158
  8. package/registry/components/expanded/EmptyState.tsx +155 -0
  9. package/registry/components/expanded/emptyState.css +111 -0
  10. package/registry/components/expanded/slideout.css +1 -0
  11. package/registry/components/expanded/table.css +1 -0
  12. package/registry/components/input-otp.tsx +574 -0
  13. package/registry/components/input.tsx +21 -11
  14. package/registry/components/menu.tsx +371 -8
  15. package/registry/components/popover.tsx +840 -0
  16. package/registry/components/select.tsx +4 -0
  17. package/registry/components/skeleton.css +57 -0
  18. package/registry/components/skeleton.tsx +482 -0
  19. package/registry/components/spinner.tsx +79 -11
  20. package/registry/components/textarea.tsx +1 -1
  21. package/registry/components/tooltip.tsx +4 -0
  22. package/registry/examples/autocomplete-demo.tsx +109 -0
  23. package/registry/examples/avatar-demo.tsx +102 -47
  24. package/registry/examples/badge-demo.tsx +16 -0
  25. package/registry/examples/expanded/command-bar-demo.tsx +236 -0
  26. package/registry/examples/expanded/empty-state-demo.tsx +39 -0
  27. package/registry/examples/input-demo.tsx +1 -1
  28. package/registry/examples/input-otp-demo.tsx +72 -0
  29. package/registry/examples/menu-demo.tsx +101 -88
  30. package/registry/examples/popover-demo.tsx +546 -0
  31. package/registry/examples/select-demo.tsx +1 -1
  32. package/registry/examples/skeleton-demo.tsx +56 -0
  33. package/registry/examples/spinner-demo.tsx +23 -1
  34. package/registry/examples/textarea-demo.tsx +1 -1
  35. package/registry/index.json +240 -8
  36. package/registry/styles/globals.css +88 -0
  37. package/src/cli/index.js +997 -62
package/src/cli/index.js CHANGED
@@ -409,13 +409,16 @@ async function writeGlobalCss(cwd, config, options) {
409
409
  const sourcePath = path.join(registryRoot, "styles", "globals.css")
410
410
  const tokenCss = await fs.readFile(sourcePath, "utf8")
411
411
  const current = await readTextIfExists(cssPath)
412
+ const tokenBlock = current
413
+ ? removeExistingTokenBlock(current, relative(cwd, cssPath))
414
+ : { content: "", found: false }
412
415
 
413
- if (current?.includes(CSS_START) && !options.force) {
416
+ if (tokenBlock.found && !options.force) {
414
417
  return [`kept ${relative(cwd, cssPath)}`]
415
418
  }
416
419
 
417
420
  const nextCss = current
418
- ? `${removeExistingTokenBlock(current).trimEnd()}\n\n${tokenCss}\n`
421
+ ? `${tokenBlock.content.trimEnd()}\n\n${tokenCss}\n`
419
422
  : `${tokenCss}\n`
420
423
  const next = ensureGlobalCssImports(nextCss)
421
424
 
@@ -489,20 +492,17 @@ async function writeViteAliasConfig(cwd, aliasRoot, options) {
489
492
 
490
493
  const current = await fs.readFile(viteConfigPath, "utf8")
491
494
  const sourceRoot = shouldUseSrcDirectory(cwd) ? "./src" : "."
492
- const aliasLine = `"${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url))`
495
+ let next
493
496
 
494
- if (
495
- current.includes(aliasLine) &&
496
- current.includes("@tailwindcss/vite") &&
497
- current.includes("tailwindcss()")
498
- ) {
499
- return [`kept ${relative(cwd, viteConfigPath)}`]
497
+ try {
498
+ next = updateViteConfigSource(current, { aliasRoot, sourceRoot })
499
+ } catch (error) {
500
+ throw new Error(`${relative(cwd, viteConfigPath)}: ${error.message}`)
500
501
  }
501
502
 
502
- let next = ensureNodeUrlImport(current)
503
- next = ensureTailwindViteImport(next)
504
- next = addViteResolveAlias(next, aliasRoot, sourceRoot)
505
- next = addVitePluginCall(next, "tailwindcss()")
503
+ if (next === current) {
504
+ return [`kept ${relative(cwd, viteConfigPath)}`]
505
+ }
506
506
 
507
507
  const status = await writeFile(viteConfigPath, next, {
508
508
  dryRun: options.dryRun,
@@ -528,50 +528,809 @@ async function findViteConfigPath(cwd) {
528
528
  }
529
529
 
530
530
  function ensureNodeUrlImport(source) {
531
- if (source.includes("fileURLToPath") && source.includes("node:url")) return source
531
+ const imports = parseImportDeclarations(source)
532
+ const nodeUrlImports = imports.filter((entry) => entry.moduleSpecifier === "node:url")
533
+ const importedLocals = new Set(nodeUrlImports.flatMap((entry) => entry.namedLocals))
534
+ const missing = ["fileURLToPath", "URL"].filter((name) => !importedLocals.has(name))
535
+
536
+ if (missing.length === 0) return source
537
+
538
+ return prependImportLines(
539
+ source,
540
+ [`import { ${missing.join(", ")} } from "node:url"`]
541
+ )
542
+ }
543
+
544
+ function ensureTailwindViteImport(source, localName) {
545
+ const imports = parseImportDeclarations(source)
546
+ const tailwindImport = imports.find(
547
+ (entry) => entry.moduleSpecifier === "@tailwindcss/vite"
548
+ )
549
+
550
+ if (!tailwindImport) {
551
+ return prependImportLines(
552
+ source,
553
+ [`import ${localName} from "@tailwindcss/vite"`]
554
+ )
555
+ }
532
556
 
533
- return `import { fileURLToPath, URL } from "node:url"\n${source}`
557
+ if (!tailwindImport.defaultLocal) {
558
+ throw new Error(
559
+ "unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
560
+ )
561
+ }
562
+
563
+ return source
534
564
  }
535
565
 
536
- function ensureTailwindViteImport(source) {
537
- if (source.includes("@tailwindcss/vite")) return source
566
+ function updateViteConfigSource(source, { aliasRoot, sourceRoot }) {
567
+ const tailwindLocalName = getTailwindViteLocalName(source)
568
+ let next = addViteResolveAlias(source, aliasRoot, sourceRoot)
569
+ next = addVitePluginCall(next, `${tailwindLocalName}()`, tailwindLocalName)
570
+ next = ensureTailwindViteImport(next, tailwindLocalName)
571
+ next = ensureNodeUrlImport(next)
572
+ return next
573
+ }
538
574
 
539
- return `import tailwindcss from "@tailwindcss/vite"\n${source}`
575
+ function getTailwindViteLocalName(source) {
576
+ const tailwindImport = parseImportDeclarations(source).find(
577
+ (entry) => entry.moduleSpecifier === "@tailwindcss/vite"
578
+ )
579
+
580
+ if (!tailwindImport) return "tailwindcss"
581
+ if (tailwindImport.defaultLocal) return tailwindImport.defaultLocal
582
+
583
+ throw new Error(
584
+ "unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
585
+ )
540
586
  }
541
587
 
542
588
  function addViteResolveAlias(source, aliasRoot, sourceRoot) {
543
- const aliasLine = `"${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url))`
544
- if (source.includes(aliasLine)) return source
545
- if (!source.includes("defineConfig({")) return source
546
-
547
- const resolveBlock = [
548
- " resolve: {",
549
- " alias: {",
550
- ` "${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url)),`,
551
- " },",
552
- " },",
553
- ].join("\n")
554
-
555
- return source.replace("defineConfig({", `defineConfig({\n${resolveBlock}`)
556
- }
557
-
558
- function addVitePluginCall(source, pluginCall) {
559
- if (source.includes(pluginCall)) return source
560
-
561
- const pluginsPattern = /plugins:\s*\[([\s\S]*?)\]/
562
- if (pluginsPattern.test(source)) {
563
- return source.replace(pluginsPattern, (_match, plugins) => {
564
- const currentPlugins = plugins.trim().replace(/,\s*$/, "")
565
- const nextPlugins = currentPlugins
566
- ? `${currentPlugins}, ${pluginCall}`
567
- : pluginCall
568
- return `plugins: [${nextPlugins}]`
569
- })
589
+ const configObject = findViteConfigObject(source)
590
+ const resolveProperty = findObjectProperty(source, configObject, "resolve")
591
+ const aliasEntry = `${JSON.stringify(aliasRoot)}: fileURLToPath(new URL(${JSON.stringify(
592
+ sourceRoot
593
+ )}, import.meta.url))`
594
+
595
+ if (!resolveProperty) {
596
+ return insertObjectProperty(
597
+ source,
598
+ configObject,
599
+ [
600
+ "resolve: {",
601
+ " alias: {",
602
+ ` ${aliasEntry},`,
603
+ " },",
604
+ "},",
605
+ ].join("\n")
606
+ )
607
+ }
608
+
609
+ const resolveObject = expectObjectLiteralValue(
610
+ source,
611
+ resolveProperty,
612
+ "unsupported Vite config: resolve must be an object literal."
613
+ )
614
+ const aliasProperty = findObjectProperty(source, resolveObject, "alias")
615
+
616
+ if (!aliasProperty) {
617
+ return insertObjectProperty(
618
+ source,
619
+ resolveObject,
620
+ [
621
+ "alias: {",
622
+ ` ${aliasEntry},`,
623
+ "},",
624
+ ].join("\n")
625
+ )
626
+ }
627
+
628
+ const aliasObject = expectObjectLiteralValue(
629
+ source,
630
+ aliasProperty,
631
+ "unsupported Vite config: resolve.alias must be an object literal."
632
+ )
633
+
634
+ if (findObjectProperty(source, aliasObject, aliasRoot)) {
635
+ return source
636
+ }
637
+
638
+ return insertObjectProperty(source, aliasObject, `${aliasEntry},`)
639
+ }
640
+
641
+ function addVitePluginCall(source, pluginCall, pluginLocalName) {
642
+ const configObject = findViteConfigObject(source)
643
+ const pluginsProperty = findObjectProperty(source, configObject, "plugins")
644
+
645
+ if (!pluginsProperty) {
646
+ return insertObjectProperty(source, configObject, `plugins: [${pluginCall}],`)
647
+ }
648
+
649
+ const pluginsArray = expectArrayLiteralValue(
650
+ source,
651
+ pluginsProperty,
652
+ "unsupported Vite config: plugins must be an array literal."
653
+ )
654
+
655
+ if (arrayHasCallExpression(source, pluginsArray, pluginLocalName)) {
656
+ return source
657
+ }
658
+
659
+ return appendArrayElement(source, pluginsArray, pluginCall)
660
+ }
661
+
662
+ function findViteConfigObject(source) {
663
+ const expressionStart = findExportDefaultExpressionStart(source)
664
+
665
+ if (expressionStart === -1) {
666
+ throw new Error(
667
+ "unsupported Vite config: expected `export default defineConfig({ ... })` or `export default { ... }`."
668
+ )
669
+ }
670
+
671
+ if (source[expressionStart] === "{") {
672
+ return createBracketRange(source, expressionStart, "{")
673
+ }
674
+
675
+ const callee = readIdentifierAt(source, expressionStart)
676
+ if (callee?.name !== "defineConfig") {
677
+ throw new Error(
678
+ "unsupported Vite config: default export must be `defineConfig({ ... })` or an object literal."
679
+ )
680
+ }
681
+
682
+ let index = skipWhitespaceAndComments(source, callee.end)
683
+ if (source[index] !== "(") {
684
+ throw new Error("unsupported Vite config: malformed defineConfig call.")
570
685
  }
571
686
 
572
- if (!source.includes("defineConfig({")) return source
687
+ index = skipWhitespaceAndComments(source, index + 1)
688
+ if (source[index] !== "{") {
689
+ throw new Error(
690
+ "unsupported Vite config: defineConfig must receive a config object literal."
691
+ )
692
+ }
693
+
694
+ return createBracketRange(source, index, "{")
695
+ }
696
+
697
+ function findExportDefaultExpressionStart(source) {
698
+ let index = 0
573
699
 
574
- return source.replace("defineConfig({", `defineConfig({\n plugins: [${pluginCall}],`)
700
+ while (index < source.length) {
701
+ const next = skipIgnoredSyntax(source, index)
702
+ if (next !== index) {
703
+ index = next
704
+ continue
705
+ }
706
+
707
+ if (!matchIdentifier(source, index, "export")) {
708
+ index += 1
709
+ continue
710
+ }
711
+
712
+ const defaultStart = skipWhitespaceAndComments(source, index + "export".length)
713
+ if (!matchIdentifier(source, defaultStart, "default")) {
714
+ index += "export".length
715
+ continue
716
+ }
717
+
718
+ return skipWhitespaceAndComments(source, defaultStart + "default".length)
719
+ }
720
+
721
+ return -1
722
+ }
723
+
724
+ function createBracketRange(source, start, open) {
725
+ const close = open === "{" ? "}" : open === "[" ? "]" : ")"
726
+ const end = findMatchingBracket(source, start, open, close)
727
+ return { start, end: end + 1 }
728
+ }
729
+
730
+ function expectObjectLiteralValue(source, property, message) {
731
+ const valueStart = skipWhitespaceAndComments(source, property.valueStart)
732
+ if (source[valueStart] !== "{") throw new Error(message)
733
+ return createBracketRange(source, valueStart, "{")
734
+ }
735
+
736
+ function expectArrayLiteralValue(source, property, message) {
737
+ const valueStart = skipWhitespaceAndComments(source, property.valueStart)
738
+ if (source[valueStart] !== "[") throw new Error(message)
739
+ return createBracketRange(source, valueStart, "[")
740
+ }
741
+
742
+ function findObjectProperty(source, objectRange, propertyName) {
743
+ const bodyEnd = objectRange.end - 1
744
+ let index = objectRange.start + 1
745
+
746
+ while (index < bodyEnd) {
747
+ index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
748
+ if (index >= bodyEnd) return null
749
+
750
+ const propertyStart = index
751
+ const name = readPropertyName(source, index)
752
+ const propertyEnd = findTopLevelCommaOrEnd(source, propertyStart, bodyEnd)
753
+
754
+ if (!name) {
755
+ index = propertyEnd + 1
756
+ continue
757
+ }
758
+
759
+ index = skipWhitespaceAndComments(source, name.end)
760
+ if (source[index] !== ":") {
761
+ if (name.name === propertyName) {
762
+ throw new Error(
763
+ `unsupported Vite config: ${propertyName} must be a normal object property.`
764
+ )
765
+ }
766
+ index = propertyEnd + 1
767
+ continue
768
+ }
769
+
770
+ if (name.name === propertyName) {
771
+ return {
772
+ name: name.name,
773
+ start: propertyStart,
774
+ end: propertyEnd,
775
+ valueStart: skipWhitespaceAndComments(source, index + 1),
776
+ valueEnd: propertyEnd,
777
+ }
778
+ }
779
+
780
+ index = propertyEnd + 1
781
+ }
782
+
783
+ return null
784
+ }
785
+
786
+ function readPropertyName(source, index) {
787
+ if (source[index] === "\"" || source[index] === "'") {
788
+ const literal = readStringLiteral(source, index)
789
+ return literal ? { name: literal.value, end: literal.end } : null
790
+ }
791
+
792
+ return readIdentifierAt(source, index)
793
+ }
794
+
795
+ function insertObjectProperty(source, objectRange, propertyText) {
796
+ const insertAt = objectRange.start + 1
797
+ const bodyEnd = objectRange.end - 1
798
+ const body = source.slice(insertAt, bodyEnd)
799
+ const hasContent = body.trim().length > 0
800
+ const propertyIndent = detectObjectChildIndent(source, objectRange)
801
+ const closeIndent = getLineIndent(source, bodyEnd)
802
+ const indentedProperty = indentMultiline(propertyText, propertyIndent)
803
+
804
+ if (!hasContent) {
805
+ return `${source.slice(0, insertAt)}\n${indentedProperty}\n${closeIndent}${source.slice(insertAt)}`
806
+ }
807
+
808
+ const needsTrailingNewline = source[insertAt] !== "\n" && source[insertAt] !== "\r"
809
+ const insertion = `\n${indentedProperty}${needsTrailingNewline ? "\n" : ""}`
810
+ return `${source.slice(0, insertAt)}${insertion}${source.slice(insertAt)}`
811
+ }
812
+
813
+ function detectObjectChildIndent(source, objectRange) {
814
+ const bodyEnd = objectRange.end - 1
815
+ let index = objectRange.start + 1
816
+
817
+ while (index < bodyEnd) {
818
+ if (source[index] === "\n") {
819
+ const next = skipHorizontalWhitespace(source, index + 1)
820
+ if (next < bodyEnd && source[next] !== "\n" && source[next] !== "\r") {
821
+ return source.slice(index + 1, next)
822
+ }
823
+ }
824
+ index += 1
825
+ }
826
+
827
+ return `${getLineIndent(source, objectRange.start)} `
828
+ }
829
+
830
+ function appendArrayElement(source, arrayRange, elementText) {
831
+ const bodyStart = arrayRange.start + 1
832
+ const bodyEnd = arrayRange.end - 1
833
+ const body = source.slice(bodyStart, bodyEnd)
834
+
835
+ if (body.trim().length === 0) {
836
+ return `${source.slice(0, bodyStart)}${elementText}${source.slice(bodyStart)}`
837
+ }
838
+
839
+ const contentEnd = trimEndIndex(source, bodyStart, bodyEnd)
840
+ const hasNewline = bodyIncludesNewline(source, bodyStart, bodyEnd)
841
+ const hasTrailingComma = previousCodeChar(source, contentEnd) === ","
842
+
843
+ if (!hasNewline) {
844
+ const separator = hasTrailingComma ? " " : ", "
845
+ return `${source.slice(0, contentEnd)}${separator}${elementText}${source.slice(contentEnd)}`
846
+ }
847
+
848
+ const closeIndent = getLineIndent(source, bodyEnd)
849
+ const elementIndent = `${closeIndent} `
850
+ const separator = hasTrailingComma ? "\n" : ",\n"
851
+ return `${source.slice(0, contentEnd)}${separator}${elementIndent}${elementText},${source.slice(contentEnd)}`
852
+ }
853
+
854
+ function arrayHasCallExpression(source, arrayRange, localName) {
855
+ const bodyEnd = arrayRange.end - 1
856
+ let index = arrayRange.start + 1
857
+
858
+ while (index < bodyEnd) {
859
+ index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
860
+ if (index >= bodyEnd) return false
861
+
862
+ const elementEnd = findTopLevelCommaOrEnd(source, index, bodyEnd)
863
+ if (startsWithCallExpression(source, index, elementEnd, localName)) {
864
+ return true
865
+ }
866
+ index = elementEnd + 1
867
+ }
868
+
869
+ return false
870
+ }
871
+
872
+ function startsWithCallExpression(source, start, end, localName) {
873
+ const index = skipWhitespaceAndComments(source, start)
874
+ if (!matchIdentifier(source, index, localName)) return false
875
+
876
+ const callStart = skipWhitespaceAndComments(source, index + localName.length)
877
+ if (source[callStart] !== "(") return false
878
+
879
+ const callEnd = createBracketRange(source, callStart, "(").end
880
+ const next = skipWhitespaceAndComments(source, callEnd)
881
+ return next <= end
882
+ }
883
+
884
+ function parseImportDeclarations(source) {
885
+ const declarations = []
886
+ let index = 0
887
+
888
+ while (index < source.length) {
889
+ const next = skipIgnoredSyntax(source, index)
890
+ if (next !== index) {
891
+ index = next
892
+ continue
893
+ }
894
+
895
+ if (!matchIdentifier(source, index, "import")) {
896
+ index += 1
897
+ continue
898
+ }
899
+
900
+ const clauseStart = skipWhitespaceAndComments(source, index + "import".length)
901
+ if (source[clauseStart] === ".") {
902
+ index = clauseStart + 1
903
+ continue
904
+ }
905
+ if (source[clauseStart] === "(") {
906
+ index = clauseStart + 1
907
+ continue
908
+ }
909
+
910
+ const end = findImportDeclarationEnd(source, index)
911
+ const declaration = source.slice(index, end)
912
+ const moduleSpecifier = getImportModuleSpecifier(declaration)
913
+
914
+ if (moduleSpecifier) {
915
+ declarations.push({
916
+ start: index,
917
+ end,
918
+ moduleSpecifier,
919
+ defaultLocal: getDefaultImportLocal(declaration),
920
+ namedLocals: getNamedImportLocals(declaration),
921
+ })
922
+ }
923
+
924
+ index = end
925
+ }
926
+
927
+ return declarations
928
+ }
929
+
930
+ function findImportDeclarationEnd(source, start) {
931
+ const stack = []
932
+ let sawModuleSpecifier = false
933
+ let index = start
934
+
935
+ while (index < source.length) {
936
+ const skipped = skipComment(source, index)
937
+ if (skipped !== index) {
938
+ index = skipped
939
+ continue
940
+ }
941
+
942
+ if (source[index] === "\"" || source[index] === "'") {
943
+ const literal = readStringLiteral(source, index)
944
+ if (!literal) return source.length
945
+ sawModuleSpecifier = true
946
+ index = literal.end
947
+ continue
948
+ }
949
+
950
+ if (isOpeningBracket(source[index])) {
951
+ stack.push(expectedClosingBracket(source[index]))
952
+ index += 1
953
+ continue
954
+ }
955
+
956
+ if (isClosingBracket(source[index])) {
957
+ if (stack.at(-1) === source[index]) stack.pop()
958
+ index += 1
959
+ continue
960
+ }
961
+
962
+ if (stack.length === 0 && source[index] === ";") return index + 1
963
+ if (
964
+ stack.length === 0 &&
965
+ sawModuleSpecifier &&
966
+ (source[index] === "\n" || source[index] === "\r")
967
+ ) {
968
+ return index + 1
969
+ }
970
+
971
+ index += 1
972
+ }
973
+
974
+ return source.length
975
+ }
976
+
977
+ function getImportModuleSpecifier(declaration) {
978
+ let moduleSpecifier = null
979
+ let index = 0
980
+
981
+ while (index < declaration.length) {
982
+ const skipped = skipComment(declaration, index)
983
+ if (skipped !== index) {
984
+ index = skipped
985
+ continue
986
+ }
987
+
988
+ if (declaration[index] === "\"" || declaration[index] === "'") {
989
+ const literal = readStringLiteral(declaration, index)
990
+ if (!literal) break
991
+ moduleSpecifier = literal.value
992
+ index = literal.end
993
+ continue
994
+ }
995
+
996
+ index += 1
997
+ }
998
+
999
+ return moduleSpecifier
1000
+ }
1001
+
1002
+ function getDefaultImportLocal(declaration) {
1003
+ let index = skipWhitespaceAndComments(declaration, "import".length)
1004
+
1005
+ if (matchIdentifier(declaration, index, "type")) {
1006
+ index = skipWhitespaceAndComments(declaration, index + "type".length)
1007
+ }
1008
+
1009
+ if (declaration[index] === "\"" || declaration[index] === "{" || declaration[index] === "*") {
1010
+ return null
1011
+ }
1012
+
1013
+ const local = readIdentifierAt(declaration, index)
1014
+ return local?.name ?? null
1015
+ }
1016
+
1017
+ function getNamedImportLocals(declaration) {
1018
+ const open = declaration.indexOf("{")
1019
+ if (open === -1) return []
1020
+
1021
+ const close = findMatchingBracket(declaration, open, "{", "}")
1022
+ const namedImportBody = declaration.slice(open + 1, close)
1023
+ const locals = []
1024
+
1025
+ for (const specifier of splitTopLevelList(namedImportBody)) {
1026
+ const trimmed = specifier.trim()
1027
+ if (!trimmed) continue
1028
+
1029
+ const withoutType = trimmed.startsWith("type ")
1030
+ ? trimmed.slice("type ".length).trim()
1031
+ : trimmed
1032
+ const asIndex = withoutType.toLowerCase().lastIndexOf(" as ")
1033
+ const localName = asIndex === -1
1034
+ ? withoutType
1035
+ : withoutType.slice(asIndex + " as ".length).trim()
1036
+ const local = readIdentifierAt(localName, 0)
1037
+
1038
+ if (local) locals.push(local.name)
1039
+ }
1040
+
1041
+ return locals
1042
+ }
1043
+
1044
+ function splitTopLevelList(source) {
1045
+ const items = []
1046
+ let start = 0
1047
+ let index = 0
1048
+
1049
+ while (index < source.length) {
1050
+ const skipped = skipIgnoredSyntax(source, index)
1051
+ if (skipped !== index) {
1052
+ index = skipped
1053
+ continue
1054
+ }
1055
+
1056
+ if (source[index] === ",") {
1057
+ items.push(source.slice(start, index))
1058
+ start = index + 1
1059
+ }
1060
+ index += 1
1061
+ }
1062
+
1063
+ items.push(source.slice(start))
1064
+ return items
1065
+ }
1066
+
1067
+ function prependImportLines(source, lines) {
1068
+ if (lines.length === 0) return source
1069
+
1070
+ let insertAt = 0
1071
+ if (source.startsWith("#!")) {
1072
+ const lineEnd = source.indexOf("\n")
1073
+ insertAt = lineEnd === -1 ? source.length : lineEnd + 1
1074
+ }
1075
+
1076
+ const prefix = `${lines.join("\n")}\n`
1077
+ return `${source.slice(0, insertAt)}${prefix}${source.slice(insertAt)}`
1078
+ }
1079
+
1080
+ function findTopLevelCommaOrEnd(source, start, end) {
1081
+ const stack = []
1082
+ let index = start
1083
+
1084
+ while (index < end) {
1085
+ const next = skipIgnoredSyntax(source, index)
1086
+ if (next !== index) {
1087
+ index = next
1088
+ continue
1089
+ }
1090
+
1091
+ if (isOpeningBracket(source[index])) {
1092
+ stack.push(expectedClosingBracket(source[index]))
1093
+ index += 1
1094
+ continue
1095
+ }
1096
+
1097
+ if (isClosingBracket(source[index])) {
1098
+ if (stack.at(-1) === source[index]) stack.pop()
1099
+ index += 1
1100
+ continue
1101
+ }
1102
+
1103
+ if (stack.length === 0 && source[index] === ",") return index
1104
+ index += 1
1105
+ }
1106
+
1107
+ return end
1108
+ }
1109
+
1110
+ function findMatchingBracket(source, start, open, close) {
1111
+ if (source[start] !== open) {
1112
+ throw new Error("unsupported Vite config: malformed JavaScript object.")
1113
+ }
1114
+
1115
+ const stack = []
1116
+ let index = start
1117
+
1118
+ while (index < source.length) {
1119
+ const next = skipIgnoredSyntax(source, index)
1120
+ if (next !== index) {
1121
+ index = next
1122
+ continue
1123
+ }
1124
+
1125
+ if (isOpeningBracket(source[index])) {
1126
+ stack.push(expectedClosingBracket(source[index]))
1127
+ index += 1
1128
+ continue
1129
+ }
1130
+
1131
+ if (isClosingBracket(source[index])) {
1132
+ const expected = stack.pop()
1133
+ if (expected !== source[index]) {
1134
+ throw new Error("unsupported Vite config: malformed JavaScript brackets.")
1135
+ }
1136
+ if (stack.length === 0) return index
1137
+ index += 1
1138
+ continue
1139
+ }
1140
+
1141
+ index += 1
1142
+ }
1143
+
1144
+ throw new Error(`unsupported Vite config: missing closing ${close}.`)
1145
+ }
1146
+
1147
+ function skipIgnoredSyntax(source, index) {
1148
+ const commentEnd = skipComment(source, index)
1149
+ if (commentEnd !== index) return commentEnd
1150
+
1151
+ if (source[index] === "\"" || source[index] === "'" || source[index] === "`") {
1152
+ return skipQuotedText(source, index)
1153
+ }
1154
+
1155
+ return index
1156
+ }
1157
+
1158
+ function skipComment(source, index) {
1159
+ if (source[index] !== "/") return index
1160
+
1161
+ if (source[index + 1] === "/") {
1162
+ const lineEnd = source.indexOf("\n", index + 2)
1163
+ return lineEnd === -1 ? source.length : lineEnd + 1
1164
+ }
1165
+
1166
+ if (source[index + 1] === "*") {
1167
+ const blockEnd = source.indexOf("*/", index + 2)
1168
+ return blockEnd === -1 ? source.length : blockEnd + 2
1169
+ }
1170
+
1171
+ return index
1172
+ }
1173
+
1174
+ function skipQuotedText(source, index) {
1175
+ const quote = source[index]
1176
+ index += 1
1177
+
1178
+ while (index < source.length) {
1179
+ if (source[index] === "\\") {
1180
+ index += 2
1181
+ continue
1182
+ }
1183
+
1184
+ if (source[index] === quote) return index + 1
1185
+ index += 1
1186
+ }
1187
+
1188
+ return source.length
1189
+ }
1190
+
1191
+ function readStringLiteral(source, index) {
1192
+ const quote = source[index]
1193
+ if (quote !== "\"" && quote !== "'") return null
1194
+
1195
+ let value = ""
1196
+ index += 1
1197
+
1198
+ while (index < source.length) {
1199
+ if (source[index] === "\\") {
1200
+ value += source[index + 1] ?? ""
1201
+ index += 2
1202
+ continue
1203
+ }
1204
+
1205
+ if (source[index] === quote) {
1206
+ return { value, end: index + 1 }
1207
+ }
1208
+
1209
+ value += source[index]
1210
+ index += 1
1211
+ }
1212
+
1213
+ return null
1214
+ }
1215
+
1216
+ function readIdentifierAt(source, index) {
1217
+ if (!isIdentifierStart(source[index])) return null
1218
+
1219
+ let end = index + 1
1220
+ while (end < source.length && isIdentifierPart(source[end])) {
1221
+ end += 1
1222
+ }
1223
+
1224
+ return { name: source.slice(index, end), end }
1225
+ }
1226
+
1227
+ function matchIdentifier(source, index, name) {
1228
+ if (source.slice(index, index + name.length) !== name) return false
1229
+
1230
+ const before = source[index - 1]
1231
+ const after = source[index + name.length]
1232
+ return !isIdentifierPart(before) && !isIdentifierPart(after)
1233
+ }
1234
+
1235
+ function isIdentifierStart(char) {
1236
+ return Boolean(char && /[A-Za-z_$]/.test(char))
1237
+ }
1238
+
1239
+ function isIdentifierPart(char) {
1240
+ return Boolean(char && /[A-Za-z0-9_$]/.test(char))
1241
+ }
1242
+
1243
+ function isOpeningBracket(char) {
1244
+ return char === "{" || char === "[" || char === "("
1245
+ }
1246
+
1247
+ function isClosingBracket(char) {
1248
+ return char === "}" || char === "]" || char === ")"
1249
+ }
1250
+
1251
+ function expectedClosingBracket(char) {
1252
+ if (char === "{") return "}"
1253
+ if (char === "[") return "]"
1254
+ return ")"
1255
+ }
1256
+
1257
+ function skipWhitespaceAndComments(source, index) {
1258
+ while (index < source.length) {
1259
+ const next = skipComment(source, index)
1260
+ if (next !== index) {
1261
+ index = next
1262
+ continue
1263
+ }
1264
+
1265
+ if (!isWhitespace(source[index])) return index
1266
+ index += 1
1267
+ }
1268
+
1269
+ return index
1270
+ }
1271
+
1272
+ function skipWhitespaceCommentsAndCommas(source, index, end) {
1273
+ while (index < end) {
1274
+ const next = skipWhitespaceAndComments(source, index)
1275
+ if (next !== index) {
1276
+ index = next
1277
+ continue
1278
+ }
1279
+
1280
+ if (source[index] !== ",") return index
1281
+ index += 1
1282
+ }
1283
+
1284
+ return index
1285
+ }
1286
+
1287
+ function skipHorizontalWhitespace(source, index) {
1288
+ while (source[index] === " " || source[index] === "\t") {
1289
+ index += 1
1290
+ }
1291
+ return index
1292
+ }
1293
+
1294
+ function isWhitespace(char) {
1295
+ return char === " " || char === "\t" || char === "\n" || char === "\r"
1296
+ }
1297
+
1298
+ function indentMultiline(text, indent) {
1299
+ return text
1300
+ .split("\n")
1301
+ .map((line) => `${indent}${line}`)
1302
+ .join("\n")
1303
+ }
1304
+
1305
+ function getLineIndent(source, index) {
1306
+ let lineStart = index
1307
+ while (lineStart > 0 && source[lineStart - 1] !== "\n") {
1308
+ lineStart -= 1
1309
+ }
1310
+
1311
+ return source.slice(lineStart, skipHorizontalWhitespace(source, lineStart))
1312
+ }
1313
+
1314
+ function trimEndIndex(source, start, end) {
1315
+ while (end > start && isWhitespace(source[end - 1])) {
1316
+ end -= 1
1317
+ }
1318
+ return end
1319
+ }
1320
+
1321
+ function previousCodeChar(source, index) {
1322
+ index -= 1
1323
+ while (index >= 0 && isWhitespace(source[index])) {
1324
+ index -= 1
1325
+ }
1326
+ return source[index]
1327
+ }
1328
+
1329
+ function bodyIncludesNewline(source, start, end) {
1330
+ for (let index = start; index < end; index += 1) {
1331
+ if (source[index] === "\n" || source[index] === "\r") return true
1332
+ }
1333
+ return false
575
1334
  }
576
1335
 
577
1336
  async function getPackageChanges(cwd, dependencies) {
@@ -734,9 +1493,76 @@ async function readJsonc(filePath) {
734
1493
  }
735
1494
 
736
1495
  function stripJsonComments(content) {
737
- return content
738
- .replace(/\/\*[\s\S]*?\*\//g, "")
739
- .replace(/(^|[^:])\/\/.*$/gm, "$1")
1496
+ let output = ""
1497
+ let inString = false
1498
+ let escaped = false
1499
+
1500
+ for (let index = 0; index < content.length; index += 1) {
1501
+ const char = content[index]
1502
+ const next = content[index + 1]
1503
+
1504
+ if (inString) {
1505
+ output += char
1506
+
1507
+ if (escaped) {
1508
+ escaped = false
1509
+ } else if (char === "\\") {
1510
+ escaped = true
1511
+ } else if (char === "\"") {
1512
+ inString = false
1513
+ }
1514
+
1515
+ continue
1516
+ }
1517
+
1518
+ if (char === "\"") {
1519
+ inString = true
1520
+ output += char
1521
+ continue
1522
+ }
1523
+
1524
+ if (char === "/" && next === "/") {
1525
+ output += " "
1526
+ index += 2
1527
+
1528
+ while (index < content.length && content[index] !== "\n" && content[index] !== "\r") {
1529
+ output += " "
1530
+ index += 1
1531
+ }
1532
+
1533
+ if (index < content.length) {
1534
+ output += content[index]
1535
+ if (content[index] === "\r" && content[index + 1] === "\n") {
1536
+ index += 1
1537
+ output += content[index]
1538
+ }
1539
+ }
1540
+
1541
+ continue
1542
+ }
1543
+
1544
+ if (char === "/" && next === "*") {
1545
+ output += " "
1546
+ index += 2
1547
+
1548
+ while (index < content.length) {
1549
+ if (content[index] === "*" && content[index + 1] === "/") {
1550
+ output += " "
1551
+ index += 1
1552
+ break
1553
+ }
1554
+
1555
+ output += content[index] === "\n" || content[index] === "\r" ? content[index] : " "
1556
+ index += 1
1557
+ }
1558
+
1559
+ continue
1560
+ }
1561
+
1562
+ output += char
1563
+ }
1564
+
1565
+ return output
740
1566
  }
741
1567
 
742
1568
  async function readJsonIfExists(filePath) {
@@ -766,27 +1592,136 @@ async function exists(filePath) {
766
1592
  }
767
1593
  }
768
1594
 
769
- function removeExistingTokenBlock(content) {
770
- const start = content.indexOf(CSS_START)
771
- if (start === -1) return content
1595
+ function removeExistingTokenBlock(content, fileLabel = "CSS file") {
1596
+ const lines = splitLinesPreservingEndings(content)
1597
+ const kept = []
1598
+ let found = false
1599
+ let inTokenBlock = false
1600
+
1601
+ for (const line of lines) {
1602
+ const trimmed = line.trimStart()
772
1603
 
773
- const end = content.indexOf(CSS_END, start)
774
- if (end === -1) return content
1604
+ if (trimmed.startsWith(CSS_START)) {
1605
+ if (found || inTokenBlock) {
1606
+ throw new Error(`${fileLabel}: multiple Banhaten token blocks found.`)
1607
+ }
1608
+ found = true
1609
+ inTokenBlock = true
1610
+ continue
1611
+ }
775
1612
 
776
- return `${content.slice(0, start)}${content.slice(end + CSS_END.length)}`
1613
+ if (trimmed.startsWith(CSS_END)) {
1614
+ if (!inTokenBlock) {
1615
+ throw new Error(`${fileLabel}: Banhaten token end marker has no start marker.`)
1616
+ }
1617
+ inTokenBlock = false
1618
+ continue
1619
+ }
1620
+
1621
+ if (!inTokenBlock) {
1622
+ kept.push(line)
1623
+ }
1624
+ }
1625
+
1626
+ if (inTokenBlock) {
1627
+ throw new Error(`${fileLabel}: Banhaten token block is missing its end marker.`)
1628
+ }
1629
+
1630
+ return { content: kept.join(""), found }
777
1631
  }
778
1632
 
779
1633
  function ensureGlobalCssImports(content) {
780
- const fontImportPattern =
781
- /^\s*@import\s+["']@fontsource(?:-variable)?\/inter(?:\/(?:400|500|600|700)\.css)?["'];\s*/gm
782
- const tailwindImportPattern = /^\s*@import\s+["']tailwindcss["'];\s*/gm
783
- const cleaned = content
784
- .replace(fontImportPattern, "")
785
- .replace(tailwindImportPattern, "")
1634
+ const cleaned = splitLinesPreservingEndings(content)
1635
+ .filter((line) => {
1636
+ const importSpecifier = readCssImportSpecifier(line)
1637
+ return (
1638
+ importSpecifier !== "tailwindcss" &&
1639
+ !isManagedInterFontImport(importSpecifier)
1640
+ )
1641
+ })
1642
+ .join("")
786
1643
  .trimStart()
787
1644
  return `${TAILWIND_IMPORT}\n${FONT_IMPORTS.join("\n")}\n${cleaned}`
788
1645
  }
789
1646
 
1647
+ function splitLinesPreservingEndings(content) {
1648
+ const lines = []
1649
+ let lineStart = 0
1650
+ let index = 0
1651
+
1652
+ while (index < content.length) {
1653
+ if (content[index] === "\r" && content[index + 1] === "\n") {
1654
+ lines.push(content.slice(lineStart, index + 2))
1655
+ index += 2
1656
+ lineStart = index
1657
+ continue
1658
+ }
1659
+
1660
+ if (content[index] === "\n") {
1661
+ lines.push(content.slice(lineStart, index + 1))
1662
+ index += 1
1663
+ lineStart = index
1664
+ continue
1665
+ }
1666
+
1667
+ index += 1
1668
+ }
1669
+
1670
+ if (lineStart < content.length) {
1671
+ lines.push(content.slice(lineStart))
1672
+ }
1673
+
1674
+ return lines
1675
+ }
1676
+
1677
+ function readCssImportSpecifier(line) {
1678
+ const trimmed = line.trim()
1679
+ const importPrefix = "@import"
1680
+ if (!trimmed.startsWith(importPrefix)) return null
1681
+
1682
+ let index = importPrefix.length
1683
+ while (trimmed[index] === " " || trimmed[index] === "\t") {
1684
+ index += 1
1685
+ }
1686
+
1687
+ const quote = trimmed[index]
1688
+ if (quote !== "\"" && quote !== "'") return null
1689
+
1690
+ const start = index + 1
1691
+ index = start
1692
+ while (index < trimmed.length && trimmed[index] !== quote) {
1693
+ index += 1
1694
+ }
1695
+
1696
+ if (index >= trimmed.length) return null
1697
+ const specifier = trimmed.slice(start, index)
1698
+ index += 1
1699
+
1700
+ while (trimmed[index] === " " || trimmed[index] === "\t") {
1701
+ index += 1
1702
+ }
1703
+
1704
+ if (trimmed[index] === ";") index += 1
1705
+
1706
+ while (trimmed[index] === " " || trimmed[index] === "\t") {
1707
+ index += 1
1708
+ }
1709
+
1710
+ return index === trimmed.length ? specifier : null
1711
+ }
1712
+
1713
+ function isManagedInterFontImport(specifier) {
1714
+ if (!specifier) return false
1715
+ if (specifier === "@fontsource/inter") return true
1716
+ if (specifier === "@fontsource-variable/inter") return true
1717
+
1718
+ for (const weight of ["400", "500", "600", "700"]) {
1719
+ if (specifier === `@fontsource/inter/${weight}.css`) return true
1720
+ }
1721
+
1722
+ return false
1723
+ }
1724
+
790
1725
  function appendAliasSegment(alias, segment) {
791
1726
  if (!alias) return null
792
1727
  return `${alias.replace(/\/$/, "")}/${segment}`