@getvitops/utils 1.0.0 → 2.0.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/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import { faviconLinks, faviconManifest, generateFavicons, writeFaviconManifest } from "./favicon.mjs";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { extname, join } from "node:path";
2
4
  import { parseFragment, serialize as serialize$1 } from "parse5";
3
5
 
4
6
  //#region src/content.ts
@@ -327,8 +329,26 @@ const prefixToMapKey = {
327
329
  "fa7-brands": "fa7-brands",
328
330
  "simple-icons": "simple-icons",
329
331
  "material-symbols": "material-symbols",
330
- lucide: "lucide"
332
+ lucide: "lucide",
333
+ ph: "ph"
331
334
  };
335
+ /**
336
+ * Sets whose weights are a name SUFFIX inside one collection rather than
337
+ * separate prefixed collections. Phosphor is the case that forced this:
338
+ * `ph:lightning`, `ph:lightning-bold`, `ph:lightning-fill` all live in `ph`,
339
+ * whereas Font Awesome splits the same idea across `fa7-solid`/`fa7-regular`/…
340
+ * (which is what `prefixToMapKey` collapses).
341
+ *
342
+ * The first entry is the unsuffixed default — asking for it adds nothing.
343
+ */
344
+ const WEIGHTED_SETS = { ph: [
345
+ "regular",
346
+ "bold",
347
+ "duotone",
348
+ "fill",
349
+ "light",
350
+ "thin"
351
+ ] };
332
352
  const iconMap = {
333
353
  fa7: {
334
354
  menu: "bars",
@@ -336,12 +356,14 @@ const iconMap = {
336
356
  home: "home",
337
357
  settings: "gear",
338
358
  search: "magnifying-glass",
339
- "expand-more": "chevron-down",
340
- "expand-less": "chevron-up",
341
- "chevron-right": "chevron-right",
342
359
  "chevron-left": "chevron-left",
343
- "arrow-back": "arrow-left",
344
- "arrow-forward": "arrow-right",
360
+ "chevron-right": "chevron-right",
361
+ "chevron-up": "chevron-up",
362
+ "chevron-down": "chevron-down",
363
+ "arrow-left": "arrow-left",
364
+ "arrow-right": "arrow-right",
365
+ "arrow-up": "arrow-up",
366
+ "arrow-down": "arrow-down",
345
367
  "external-link": "arrow-up-right-from-square",
346
368
  "more-vert": "ellipsis-vertical",
347
369
  "more-horiz": "ellipsis",
@@ -359,8 +381,8 @@ const iconMap = {
359
381
  bookmark: "bookmark",
360
382
  favorite: "heart",
361
383
  star: "star",
362
- login: "login",
363
- logout: "logout",
384
+ login: "right-to-bracket",
385
+ logout: "right-from-bracket",
364
386
  email: "envelope",
365
387
  phone: "phone",
366
388
  location: "location-dot",
@@ -379,7 +401,7 @@ const iconMap = {
379
401
  image: "image",
380
402
  export: "file-export",
381
403
  import: "file-import",
382
- backup: "backup",
404
+ backup: "cloud-arrow-up",
383
405
  compress: "compress",
384
406
  check: "check",
385
407
  error: "circle-exclamation",
@@ -413,7 +435,8 @@ const iconMap = {
413
435
  print: "print",
414
436
  code: "code",
415
437
  palette: "palette",
416
- lightbulb: "lightbulb"
438
+ lightbulb: "lightbulb",
439
+ lightning: "bolt"
417
440
  },
418
441
  "fa7-brands": {
419
442
  facebook: "facebook",
@@ -504,12 +527,14 @@ const iconMap = {
504
527
  home: "home",
505
528
  settings: "settings",
506
529
  search: "search",
507
- "expand-more": "expand-more",
508
- "expand-less": "expand-less",
509
- "chevron-right": "chevron-right",
510
530
  "chevron-left": "chevron-left",
511
- "arrow-back": "arrow-back",
512
- "arrow-forward": "arrow-forward",
531
+ "chevron-right": "chevron-right",
532
+ "chevron-up": "expand-less",
533
+ "chevron-down": "expand-more",
534
+ "arrow-left": "arrow-back",
535
+ "arrow-right": "arrow-forward",
536
+ "arrow-up": "arrow-upward",
537
+ "arrow-down": "arrow-downward",
513
538
  "external-link": "open-in-new",
514
539
  "more-vert": "more-vert",
515
540
  "more-horiz": "more-horiz",
@@ -581,7 +606,8 @@ const iconMap = {
581
606
  print: "print",
582
607
  code: "code",
583
608
  palette: "palette",
584
- lightbulb: "lightbulb"
609
+ lightbulb: "lightbulb",
610
+ lightning: "bolt"
585
611
  },
586
612
  lucide: {
587
613
  menu: "menu",
@@ -589,12 +615,14 @@ const iconMap = {
589
615
  home: "house",
590
616
  settings: "settings",
591
617
  search: "search",
592
- "expand-more": "chevron-down",
593
- "expand-less": "chevron-up",
594
- "chevron-right": "chevron-right",
595
618
  "chevron-left": "chevron-left",
596
- "arrow-back": "arrow-left",
597
- "arrow-forward": "arrow-right",
619
+ "chevron-right": "chevron-right",
620
+ "chevron-up": "chevron-up",
621
+ "chevron-down": "chevron-down",
622
+ "arrow-left": "arrow-left",
623
+ "arrow-right": "arrow-right",
624
+ "arrow-up": "arrow-up",
625
+ "arrow-down": "arrow-down",
598
626
  "external-link": "external-link",
599
627
  "more-vert": "ellipsis-vertical",
600
628
  "more-horiz": "ellipsis",
@@ -666,7 +694,96 @@ const iconMap = {
666
694
  print: "printer",
667
695
  code: "code",
668
696
  palette: "palette",
669
- lightbulb: "lightbulb"
697
+ lightbulb: "lightbulb",
698
+ lightning: "zap"
699
+ },
700
+ ph: {
701
+ menu: "list",
702
+ close: "x",
703
+ home: "house",
704
+ settings: "gear",
705
+ search: "magnifying-glass",
706
+ "chevron-left": "caret-left",
707
+ "chevron-right": "caret-right",
708
+ "chevron-up": "caret-up",
709
+ "chevron-down": "caret-down",
710
+ "arrow-left": "arrow-left",
711
+ "arrow-right": "arrow-right",
712
+ "arrow-up": "arrow-up",
713
+ "arrow-down": "arrow-down",
714
+ "external-link": "arrow-square-out",
715
+ "more-vert": "dots-three-vertical",
716
+ "more-horiz": "dots-three",
717
+ add: "plus",
718
+ remove: "minus",
719
+ delete: "trash",
720
+ edit: "pencil",
721
+ save: "floppy-disk",
722
+ copy: "copy",
723
+ paste: "clipboard",
724
+ attach: "paperclip",
725
+ upload: "upload",
726
+ download: "download",
727
+ share: "share",
728
+ bookmark: "bookmark",
729
+ favorite: "heart",
730
+ star: "star",
731
+ login: "sign-in",
732
+ logout: "sign-out",
733
+ email: "envelope",
734
+ phone: "phone",
735
+ location: "map-pin",
736
+ globe: "globe",
737
+ link: "link",
738
+ clock: "clock",
739
+ calendar: "calendar",
740
+ schedule: "calendar-dots",
741
+ person: "person",
742
+ group: "users",
743
+ file: "file",
744
+ "file-add": "file-plus",
745
+ folder: "folder",
746
+ "folder-open": "folder-open",
747
+ "folder-add": "folder-plus",
748
+ image: "image",
749
+ export: "upload-simple",
750
+ import: "download-simple",
751
+ backup: "cloud-arrow-up",
752
+ compress: "arrows-in",
753
+ check: "check",
754
+ error: "warning-circle",
755
+ warning: "warning",
756
+ info: "info",
757
+ help: "question",
758
+ cart: "shopping-cart",
759
+ tag: "tag",
760
+ "credit-card": "credit-card",
761
+ receipt: "receipt",
762
+ leaf: "leaf",
763
+ tree: "tree",
764
+ seedling: "plant",
765
+ trowel: "shovel",
766
+ droplet: "drop",
767
+ snowflake: "snowflake",
768
+ sun: "sun",
769
+ cloud: "cloud",
770
+ grid: "squares-four",
771
+ list: "list",
772
+ filter: "funnel",
773
+ sort: "arrows-down-up",
774
+ fullscreen: "corners-out",
775
+ collapse: "corners-in",
776
+ building: "building",
777
+ video: "video",
778
+ lock: "lock",
779
+ unlock: "lock-open",
780
+ eye: "eye",
781
+ "eye-off": "eye-slash",
782
+ print: "printer",
783
+ code: "code",
784
+ palette: "palette",
785
+ lightbulb: "lightbulb",
786
+ lightning: "lightning"
670
787
  }
671
788
  };
672
789
  /**
@@ -674,15 +791,27 @@ const iconMap = {
674
791
  * Names containing ':' are returned as-is (pass-through for fully-qualified names).
675
792
  *
676
793
  * @param name - Semantic name ('email') or fully-qualified ('fa7-solid:envelope')
677
- * @param prefix - astro-icon prefix, e.g. 'fa7-solid', 'material-symbols', 'lucide'
794
+ * @param prefix - astro-icon prefix, e.g. 'fa7-solid', 'material-symbols', 'lucide', 'ph'
795
+ * @param opts.weight - Weight for suffix-weighted sets (see `WEIGHTED_SETS`), e.g.
796
+ * `resolveIcon('menu', 'ph', { weight: 'bold' })` → `'ph:list-bold'`. Ignored for
797
+ * sets that aren't suffix-weighted; an unknown weight throws rather than silently
798
+ * resolving to the regular glyph, which would be indistinguishable from a typo.
678
799
  */
679
- function resolveIcon(name, prefix) {
800
+ function resolveIcon(name, prefix, opts) {
680
801
  if (name.includes(":")) return name;
681
802
  const mapKey = prefixToMapKey[prefix];
682
803
  if (!mapKey) throw new Error(`Unknown icon prefix "${prefix}". Valid: ${Object.keys(prefixToMapKey).join(", ")}`);
683
804
  const resolved = iconMap[mapKey][name];
684
805
  if (!resolved) throw new Error(`Icon "${name}" not found in set "${prefix}" (map key: "${mapKey}")`);
685
- return `${prefix}:${resolved}`;
806
+ return `${prefix}:${applyWeight(resolved, prefix, opts?.weight)}`;
807
+ }
808
+ /** Append a weight suffix for suffix-weighted sets. The first weight is the bare default. */
809
+ function applyWeight(name, prefix, weight) {
810
+ if (!weight) return name;
811
+ const weights = WEIGHTED_SETS[prefix];
812
+ if (!weights) return name;
813
+ if (!weights.includes(weight)) throw new Error(`Unknown weight "${weight}" for icon set "${prefix}". Valid: ${weights.join(", ")}.`);
814
+ return weight === weights[0] ? name : `${name}-${weight}`;
686
815
  }
687
816
  /**
688
817
  * Resolve a brand icon name to a fully-qualified astro-icon string.
@@ -719,7 +848,7 @@ function generateIconInclude(iconsConfig) {
719
848
  const addToInclude = (prefix, names) => {
720
849
  include[prefix] = [...new Set([...include[prefix] ?? [], ...names])];
721
850
  };
722
- for (const [key, val] of Object.entries(iconsConfig ?? {})) if (key !== "ui" && key !== "brand" && key !== "semantic" && Array.isArray(val)) addToInclude(key, val);
851
+ for (const [key, val] of Object.entries(iconsConfig ?? {})) if (key !== "ui" && key !== "brand" && key !== "weight" && key !== "semantic" && Array.isArray(val)) addToInclude(key, val);
723
852
  const uiMapKey = prefixToMapKey[uiPrefix];
724
853
  const brandMapKey = prefixToMapKey[brandPrefix];
725
854
  const unknownPrefix = [];
@@ -730,8 +859,8 @@ function generateIconInclude(iconsConfig) {
730
859
  for (const name of iconsConfig?.semantic ?? []) {
731
860
  const ui = iconMap[uiMapKey][name];
732
861
  const brand = iconMap[brandMapKey][name];
733
- if (ui) addToInclude(uiPrefix, [ui]);
734
- if (brand) addToInclude(brandPrefix, [brand]);
862
+ if (ui) addToInclude(uiPrefix, [applyWeight(ui, uiPrefix, iconsConfig?.weight)]);
863
+ if (brand) addToInclude(brandPrefix, [applyWeight(brand, brandPrefix, iconsConfig?.weight)]);
735
864
  if (!ui && !brand) unresolved.push(name);
736
865
  }
737
866
  if (unresolved.length) throw new Error(`[vitops icons] ${unresolved.length} semantic icon name(s) not found in ui set '${uiPrefix}' or brand set '${brandPrefix}': ${unresolved.join(", ")}.\nEither add them to iconMap in @getvitops/core, or drop them from the \`semantic\` list. Available in '${uiPrefix}': ${Object.keys(iconMap[uiMapKey]).slice(0, 12).join(", ")}…`);
@@ -739,5 +868,216 @@ function generateIconInclude(iconsConfig) {
739
868
  }
740
869
 
741
870
  //#endregion
742
- export { articleGraph, breadcrumbGraph, faqGraph, faviconLinks, faviconManifest, generateFavicons, generateIconInclude, getAttribute, iconMap, isElmnt, isImageLink, isLink, isTextLink, nodesToHtml, organizationGraph, parseRenderedSlots, partAttrs, prefixToMapKey, resolveBrandIcon, resolveIcon, serialize, styleList, t, toHtml, writeFaviconManifest };
871
+ //#region src/scan.ts
872
+ /**
873
+ * Consumer source discovery.
874
+ *
875
+ * Lifted verbatim out of `@getvitops/cli`, where it was a private helper behind
876
+ * `vitops lint`. It lives here now because a second caller needs it: the Astro
877
+ * integration scans for icon usage during `astro:config:setup`, and it cannot
878
+ * import the CLI — `@getvitops/cli` exposes only a `bin`, and its entry calls
879
+ * `main()` at module scope, so importing it would run the CLI.
880
+ *
881
+ * Deliberately hand-rolled rather than a glob dependency: the whole job is one
882
+ * `readdirSync` recursion with two allow/deny sets, and it runs synchronously
883
+ * inside a config hook where an async glob would have to be awaited anyway.
884
+ */
885
+ /**
886
+ * Extensions worth scanning: every template language the framework's classes
887
+ * and icon names can appear in, plus the markdown that can embed them.
888
+ */
889
+ const SCAN_EXT = new Set([
890
+ ".astro",
891
+ ".html",
892
+ ".htm",
893
+ ".js",
894
+ ".jsx",
895
+ ".ts",
896
+ ".tsx",
897
+ ".vue",
898
+ ".svelte",
899
+ ".php",
900
+ ".md",
901
+ ".mdx"
902
+ ]);
903
+ /** Directories that only ever hold dependencies or build output. */
904
+ const SCAN_SKIP = new Set([
905
+ "node_modules",
906
+ "dist",
907
+ ".git",
908
+ ".astro",
909
+ "build",
910
+ "coverage"
911
+ ]);
912
+ /**
913
+ * Recursively read every scannable file under `dir`.
914
+ *
915
+ * Dotfiles and dot-directories are skipped wholesale — they hold tooling config,
916
+ * not templates, and `.astro`/`.git` would otherwise be walked twice over.
917
+ */
918
+ function scanFiles(dir, opts) {
919
+ const exts = opts?.exts ? new Set(opts.exts) : SCAN_EXT;
920
+ const skip = opts?.skipDirs ? new Set(opts.skipDirs) : SCAN_SKIP;
921
+ const acc = [];
922
+ walk(dir, exts, skip, acc);
923
+ return acc;
924
+ }
925
+ function walk(dir, exts, skip, acc) {
926
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
927
+ if (e.name.startsWith(".") && e.name !== ".") continue;
928
+ const p = join(dir, e.name);
929
+ if (e.isDirectory()) {
930
+ if (!skip.has(e.name)) walk(p, exts, skip, acc);
931
+ } else if (exts.has(extname(e.name))) acc.push({
932
+ path: p,
933
+ text: readFileSync(p, "utf8")
934
+ });
935
+ }
936
+ }
937
+
938
+ //#endregion
939
+ //#region src/icons-scan.ts
940
+ /**
941
+ * Props that carry an icon name. `icon` covers the component convention
942
+ * (`Popover`/`Drawer`/`Details` triggers); the start/end pair covers adornments
943
+ * on links and buttons.
944
+ */
945
+ const DEFAULT_PROPS = [
946
+ "icon",
947
+ "startIcon",
948
+ "endIcon",
949
+ "leadingIcon",
950
+ "trailingIcon"
951
+ ];
952
+ /**
953
+ * `<Icon …>` and any capitalised component whose name ends in `Icon`.
954
+ * Requires a capital or the literal `Icon`, so `<iconButton>` and plain HTML
955
+ * never match.
956
+ */
957
+ const COMPONENT = /<([A-Z][\w.]*Icon|Icon)\b([^>]*?)\/?>/g;
958
+ /** Sprite references: `<use href="…/icons.svg#menu">`, incl. legacy `xlink:href`. */
959
+ const SPRITE_USE = /<use\b[^>]*?\b(?:xlink:)?href\s*=\s*(?:"([^"#]*)#([^"]*)"|'([^'#]*)#([^']*)')/g;
960
+ /**
961
+ * An attribute value: a quoted literal, a braced single literal, or a braced
962
+ * expression. The final alternative is what marks a value as dynamic — it is
963
+ * matched on purpose so the reference can be REPORTED rather than skipped.
964
+ *
965
+ * That last alternative stops at the first `}`, so a reported expression
966
+ * containing its own braces (`{\`a:${b}\`}`) is truncated in the warning text.
967
+ * Deliberate: balancing braces with a regex buys nothing here, because the
968
+ * actionable part of the report is the file and line, not the expression.
969
+ */
970
+ const VALUE = String.raw`(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|\`([^\`$]*)\`)\s*\}|(\{[^}]*\}))`;
971
+ /**
972
+ * `\b` is not enough of a left boundary here: `-` is a non-word character, so
973
+ * `\bicon` happily matches inside `data-icon=` and `aria-icon=`. Requiring that
974
+ * the previous character be neither a word character nor a hyphen keeps the
975
+ * match to a genuine standalone prop.
976
+ */
977
+ const LEFT = String.raw`(?<![\w-])`;
978
+ const nameAttr = () => new RegExp(String.raw`${LEFT}name\s*=\s*${VALUE}`);
979
+ const iconProp = (props) => new RegExp(String.raw`${LEFT}(${props.join("|")})\s*=\s*${VALUE}`, "g");
980
+ /** Object-literal form: `trigger={{ icon: 'bars' }}`, or a data array of `{ icon: '…' }`. */
981
+ const iconKey = (props) => new RegExp(String.raw`${LEFT}(${props.join("|")})\s*:\s*(?:"([^"]*)"|'([^']*)'|\`([^\`$]*)\`)`, "g");
982
+ const lineOf = (src, index) => src.slice(0, index).split("\n").length;
983
+ /**
984
+ * Pull the literal out of a VALUE match, or report it as dynamic.
985
+ * Groups run: "double", 'single', {"double"}, {'single'}, {`template`}, {expr}.
986
+ */
987
+ function readValue(groups) {
988
+ const [dq, sq, bdq, bsq, btpl, expr] = groups;
989
+ if (expr !== void 0) return { expr };
990
+ const literal = dq ?? sq ?? bdq ?? bsq ?? btpl;
991
+ if (literal === void 0) return {};
992
+ if (literal.includes("${")) return { expr: literal };
993
+ return { literal };
994
+ }
995
+ /** Extract every icon reference from one file's text. */
996
+ function extractIconRefs(src, file = "<source>", opts) {
997
+ const props = [...new Set([...DEFAULT_PROPS, ...opts?.props ?? []])];
998
+ const refs = [];
999
+ const dynamic = [];
1000
+ const NAME = nameAttr();
1001
+ for (const m of src.matchAll(COMPONENT)) {
1002
+ const attrs = m[2] ?? "";
1003
+ const hit = NAME.exec(attrs);
1004
+ if (!hit) continue;
1005
+ const line = lineOf(src, m.index);
1006
+ const { literal, expr } = readValue(hit.slice(1));
1007
+ if (literal) refs.push({
1008
+ name: literal,
1009
+ kind: "component",
1010
+ attr: "name",
1011
+ file,
1012
+ line
1013
+ });
1014
+ else if (expr) dynamic.push({
1015
+ expr,
1016
+ attr: "name",
1017
+ file,
1018
+ line
1019
+ });
1020
+ }
1021
+ for (const m of src.matchAll(iconProp(props))) {
1022
+ const line = lineOf(src, m.index);
1023
+ const { literal, expr } = readValue(m.slice(2));
1024
+ if (literal) refs.push({
1025
+ name: literal,
1026
+ kind: "prop",
1027
+ attr: m[1],
1028
+ file,
1029
+ line
1030
+ });
1031
+ else if (expr) dynamic.push({
1032
+ expr,
1033
+ attr: m[1],
1034
+ file,
1035
+ line
1036
+ });
1037
+ }
1038
+ for (const m of src.matchAll(iconKey(props))) {
1039
+ const literal = m[2] ?? m[3] ?? m[4];
1040
+ if (!literal || literal.includes("${")) continue;
1041
+ refs.push({
1042
+ name: literal,
1043
+ kind: "object",
1044
+ attr: m[1],
1045
+ file,
1046
+ line: lineOf(src, m.index)
1047
+ });
1048
+ }
1049
+ for (const m of src.matchAll(SPRITE_USE)) {
1050
+ const id = m[2] ?? m[4];
1051
+ if (!id) continue;
1052
+ refs.push({
1053
+ name: id,
1054
+ kind: "sprite",
1055
+ attr: "href",
1056
+ file,
1057
+ line: lineOf(src, m.index)
1058
+ });
1059
+ }
1060
+ return {
1061
+ refs,
1062
+ dynamic
1063
+ };
1064
+ }
1065
+ /** Run `extractIconRefs` across a file set and collect unique names. */
1066
+ function collectIconRefs(files, opts) {
1067
+ const refs = [];
1068
+ const dynamic = [];
1069
+ for (const f of files) {
1070
+ const r = extractIconRefs(f.text, f.path, opts);
1071
+ refs.push(...r.refs);
1072
+ dynamic.push(...r.dynamic);
1073
+ }
1074
+ return {
1075
+ refs,
1076
+ dynamic,
1077
+ names: [...new Set(refs.map((r) => r.name))]
1078
+ };
1079
+ }
1080
+
1081
+ //#endregion
1082
+ export { SCAN_EXT, SCAN_SKIP, WEIGHTED_SETS, articleGraph, breadcrumbGraph, collectIconRefs, extractIconRefs, faqGraph, faviconLinks, faviconManifest, generateFavicons, generateIconInclude, getAttribute, iconMap, isElmnt, isImageLink, isLink, isTextLink, nodesToHtml, organizationGraph, parseRenderedSlots, partAttrs, prefixToMapKey, resolveBrandIcon, resolveIcon, scanFiles, serialize, styleList, t, toHtml, writeFaviconManifest };
743
1083
  //# sourceMappingURL=index.mjs.map