@astryxdesign/cli 0.7.11 → 0.7.12-canary.ccb4a6d
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/api/build/kit/rank.mjs +21 -4
- package/api/build/kit/words.d.mts +1 -0
- package/api/build/kit/words.mjs +1 -1
- package/api/icon/_adapter.d.mts +3 -2
- package/api/icon/_adapter.mjs +8 -2
- package/api/icon/_authoring.d.mts +1 -0
- package/api/icon/_authoring.mjs +586 -66
- package/api/icon/_default-look.d.mts +155 -0
- package/api/icon/_default-look.mjs +474 -0
- package/api/icon/_records.d.mts +1 -1
- package/api/icon/_records.mjs +1 -0
- package/api/icon/add/add.mjs +224 -38
- package/api/icon/icon.type.d.mts +22 -2
- package/api/icon/icon.type.mjs +6 -2
- package/api/icon/iconAdd.doc.mjs +2 -2
- package/api/icon/iconNewDefinition.doc.mjs +3 -2
- package/api/icon/iconNewSet.doc.mjs +4 -4
- package/api/icon/search/search.mjs +10 -9
- package/api/integration/add-helpers.d.mts +11 -1
- package/api/integration/add-helpers.mjs +5 -2
- package/api/integration/authoring-checks.d.mts +0 -2
- package/api/integration/authoring-checks.mjs +1 -6
- package/api/logger.mjs +2 -1
- package/api/swizzle/_adapter.d.mts +1 -0
- package/api/swizzle/_adapter.mjs +4 -1
- package/api/swizzle/_moduleGraph.d.mts +171 -0
- package/api/swizzle/_moduleGraph.mjs +795 -0
- package/api/swizzle/copy/copy.d.mts +2 -43
- package/api/swizzle/copy/copy.mjs +195 -314
- package/api/swizzle/swizzle.d.mts +0 -2
- package/api/swizzle/swizzle.doc.mjs +9 -4
- package/api/swizzle/swizzle.mjs +3 -7
- package/api/swizzle/swizzle.type.d.mts +2 -1
- package/api/swizzle/swizzle.type.mjs +3 -2
- package/assets/discover/official-packages.json +20 -0
- package/assets/docs/tree/icon-sets.doc.mjs +11 -5
- package/assets/templates/blocks/components/Table/TableRowIndexTable.doc.mjs +1 -1
- package/assets/templates/blocks/components/Table/TableRowIndexTable.tsx +2 -2
- package/assets/templates/pages/course-detail/page.tsx +699 -0
- package/assets/templates/pages/course-detail/template.doc.mjs +37 -0
- package/assets/templates/pages/file-explorer/page.tsx +103 -79
- package/assets/templates/pages/table-grouped/page.tsx +160 -232
- package/assets/templates/pages/table-grouped/template.doc.mjs +0 -2
- package/assets/templates/pages/theme-showcase/page.tsx +724 -792
- package/assets/templates/pages/theme-showcase/template.doc.mjs +4 -5
- package/authoring/doctypes/template/type.ts +1 -0
- package/clients/cli/commands/experimental/icon.mjs +31 -10
- package/clients/cli/commands/experimental-icon-add.doc.mjs +1 -1
- package/clients/cli/commands/experimental-icon-new-definition.doc.mjs +2 -1
- package/clients/cli/commands/experimental-icon-new-set.doc.mjs +2 -2
- package/clients/cli/commands/swizzle.doc.mjs +8 -3
- package/clients/cli/commands/swizzle.mjs +21 -9
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/doc-types.mjs +9 -2
- package/foundation/discovery/icon-ranking.d.mts +27 -3
- package/foundation/discovery/icon-ranking.mjs +125 -8
- package/foundation/discovery/icon-set-checks.mjs +44 -35
- package/foundation/discovery/icon-set-discovery.d.mts +13 -0
- package/foundation/discovery/icon-set-discovery.mjs +25 -0
- package/foundation/discovery/icon-set-writer.d.mts +238 -16
- package/foundation/discovery/icon-set-writer.mjs +676 -79
- package/foundation/discovery/module-exports.d.mts +3 -2
- package/foundation/discovery/module-exports.mjs +167 -37
- package/foundation/fs/paths.d.mts +2 -1
- package/foundation/fs/paths.mjs +3 -4
- package/foundation/text/shell-arg.d.mts +14 -0
- package/foundation/text/shell-arg.mjs +17 -0
- package/package.json +6 -6
|
@@ -723,20 +723,405 @@ export function modulePrefix(file, options) {
|
|
|
723
723
|
return libraryPrefix(prefixNames(iconCandidates(file, options)));
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
/**
|
|
727
|
+
* How a module names its icons: the library prefix every icon export shares
|
|
728
|
+
* ({@link modulePrefix}) and the look words its names end in
|
|
729
|
+
* ({@link lookPositions}), read from its icon exports once. A large module
|
|
730
|
+
* reports its reading, as {@link libraryIcons} does.
|
|
731
|
+
* @param {string} file
|
|
732
|
+
* @param {{adapter?: boolean, module?: string, progress?: (line: string) => void}} [options]
|
|
733
|
+
* `module`: the module as the author named it, for progress; `progress`:
|
|
734
|
+
* hears the reading of a large module
|
|
735
|
+
* @returns {{prefix: string[], positions: Array<Set<string>>}}
|
|
736
|
+
*/
|
|
737
|
+
export function moduleNaming(file, options = {}) {
|
|
738
|
+
const label = options.module ?? path.basename(file);
|
|
739
|
+
let count = 0;
|
|
740
|
+
return readingLibrary(
|
|
741
|
+
file,
|
|
742
|
+
{label, progress: options.progress},
|
|
743
|
+
() => {
|
|
744
|
+
const candidates = iconCandidates(file, options);
|
|
745
|
+
count = candidates.length;
|
|
746
|
+
const prefix = libraryPrefix(prefixNames(candidates));
|
|
747
|
+
const positions = lookPositions(
|
|
748
|
+
candidates.map(each => iconNaming(each.name, prefix).id),
|
|
749
|
+
);
|
|
750
|
+
return {prefix, positions};
|
|
751
|
+
},
|
|
752
|
+
() => `Found ${count} icon exports in ${label}.`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* An export's id and set name ({@link iconNaming}) without the look words
|
|
758
|
+
* its library's names end in ({@link lookPositions}):
|
|
759
|
+
* `XOutline24WGHT500Icon` is `x`, `XIcon`.
|
|
760
|
+
* @param {string} name
|
|
761
|
+
* @param {{prefix: string[], positions: Array<Set<string>>}} naming
|
|
762
|
+
* @returns {{id: string, name: string}}
|
|
763
|
+
*/
|
|
764
|
+
export function iconNamingWithoutLook(name, {prefix, positions}) {
|
|
765
|
+
const named = iconNaming(name, prefix);
|
|
766
|
+
const {base} = splitLook(named.id.split('-'), positions);
|
|
767
|
+
if (base.length === named.id.split('-').length) return named;
|
|
768
|
+
const pascal = base
|
|
769
|
+
.map(word => word[0].toUpperCase() + word.slice(1))
|
|
770
|
+
.join('');
|
|
771
|
+
return {id: base.join('-'), name: iconExportName(pascal)};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** A library with at least this many exports reports its reading. */
|
|
775
|
+
const PROGRESS_FROM = 1000;
|
|
776
|
+
|
|
777
|
+
/** Fewer icons than this never show a library's look positions. */
|
|
778
|
+
const LOOK_MIN_ICONS = 20;
|
|
779
|
+
|
|
780
|
+
/** More words than this in one trailing position are glyph names. */
|
|
781
|
+
const LOOK_MAX_VALUES = 12;
|
|
782
|
+
|
|
783
|
+
/** Words that name a look's style, never a glyph. */
|
|
784
|
+
const LOOK_STYLE_WORDS = new Set([
|
|
785
|
+
'default',
|
|
786
|
+
'regular',
|
|
787
|
+
'outline',
|
|
788
|
+
'outlined',
|
|
789
|
+
'line',
|
|
790
|
+
'linear',
|
|
791
|
+
'stroke',
|
|
792
|
+
'filled',
|
|
793
|
+
'fill',
|
|
794
|
+
'solid',
|
|
795
|
+
'bold',
|
|
796
|
+
'light',
|
|
797
|
+
'thin',
|
|
798
|
+
'duotone',
|
|
799
|
+
'twotone',
|
|
800
|
+
'mini',
|
|
801
|
+
'micro',
|
|
802
|
+
'sharp',
|
|
803
|
+
'round',
|
|
804
|
+
'rounded',
|
|
805
|
+
'bulk',
|
|
806
|
+
'broken',
|
|
807
|
+
]);
|
|
808
|
+
|
|
809
|
+
/** Words that name a glyph's direction (`ArrowUp`), never a look. */
|
|
810
|
+
const DIRECTION_WORDS = new Set([
|
|
811
|
+
'up',
|
|
812
|
+
'down',
|
|
813
|
+
'left',
|
|
814
|
+
'right',
|
|
815
|
+
'top',
|
|
816
|
+
'bottom',
|
|
817
|
+
'start',
|
|
818
|
+
'end',
|
|
819
|
+
'north',
|
|
820
|
+
'south',
|
|
821
|
+
'east',
|
|
822
|
+
'west',
|
|
823
|
+
]);
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Whether the words that fill a trailing position name a look: each is
|
|
827
|
+
* numbered (`24`, `wght500`) or one is a style word, and none is a
|
|
828
|
+
* direction word. `ArrowUp` and `ArrowDown` are two glyphs, not two looks.
|
|
829
|
+
* @param {string[]} words
|
|
830
|
+
*/
|
|
831
|
+
function namesLook(words) {
|
|
832
|
+
if (words.some(word => DIRECTION_WORDS.has(word))) return false;
|
|
833
|
+
return (
|
|
834
|
+
words.every(word => /^[a-z]*\d+$/u.test(word)) ||
|
|
835
|
+
words.some(word => LOOK_STYLE_WORDS.has(word))
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* The trailing words a library encodes its looks with: a style, a size, a
|
|
841
|
+
* weight (`XOutline24WGHT500Icon` is `x` in the `outline`, `24`, `wght500`
|
|
842
|
+
* look). A trailing position is a look position when, across at least
|
|
843
|
+
* {@link LOOK_MIN_ICONS} icons, at most {@link LOOK_MAX_VALUES} words fill
|
|
844
|
+
* it, each on at least 5% of the icons and together on at least 90%, and
|
|
845
|
+
* those words name a look ({@link namesLook}); the positions are read from
|
|
846
|
+
* the end while they hold. Glyph names never meet that bar (`trash-2` and
|
|
847
|
+
* `clock-10` are a few icons each, and `up` and `down` are directions), so
|
|
848
|
+
* a library that encodes no looks has none. Reads each id once per
|
|
849
|
+
* position and keeps only counts.
|
|
850
|
+
* @param {string[]} ids every icon's id
|
|
851
|
+
* @returns {Array<Set<string>>} the look positions, the last word first
|
|
852
|
+
*/
|
|
853
|
+
export function lookPositions(ids) {
|
|
854
|
+
if (ids.length < LOOK_MIN_ICONS) return [];
|
|
855
|
+
/** @type {Array<Set<string>>} */
|
|
856
|
+
const positions = [];
|
|
857
|
+
const least = Math.max(2, Math.ceil(ids.length * 0.05));
|
|
858
|
+
while (positions.length < 4) {
|
|
859
|
+
/** @type {Map<string, number>} */
|
|
860
|
+
const counts = new Map();
|
|
861
|
+
for (const id of ids) {
|
|
862
|
+
const words = id.split('-');
|
|
863
|
+
const {base} = splitLook(words, positions);
|
|
864
|
+
if (base.length < 2 || base.length < words.length - positions.length) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const last = /** @type {string} */ (base.at(-1));
|
|
868
|
+
counts.set(last, (counts.get(last) ?? 0) + 1);
|
|
869
|
+
}
|
|
870
|
+
const values = [...counts].filter(([, count]) => count >= least);
|
|
871
|
+
if (values.length === 0 || values.length > LOOK_MAX_VALUES) break;
|
|
872
|
+
const covered = values.reduce((sum, [, count]) => sum + count, 0);
|
|
873
|
+
if (covered < ids.length * 0.9) break;
|
|
874
|
+
if (!namesLook(values.map(([value]) => value))) break;
|
|
875
|
+
positions.push(new Set(values.map(([value]) => value)));
|
|
876
|
+
}
|
|
877
|
+
return positions;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* An id's glyph words and its look words, by {@link lookPositions}:
|
|
882
|
+
* `x-outline-24-wght500` is `x` with `[wght500, 24, outline]`, null where
|
|
883
|
+
* the id has no look word.
|
|
884
|
+
* @param {string[]} words
|
|
885
|
+
* @param {Array<Set<string>>} positions
|
|
886
|
+
* @returns {{base: string[], look: Array<string | null>}}
|
|
887
|
+
*/
|
|
888
|
+
export function splitLook(words, positions) {
|
|
889
|
+
let end = words.length;
|
|
890
|
+
/** @type {Array<string | null>} */
|
|
891
|
+
const look = positions.map(() => null);
|
|
892
|
+
for (const [index, position] of positions.entries()) {
|
|
893
|
+
const last = words[end - 1];
|
|
894
|
+
if (end < 2 || last == null || !position.has(last)) break;
|
|
895
|
+
look[index] = last;
|
|
896
|
+
end--;
|
|
897
|
+
}
|
|
898
|
+
return {base: end === words.length ? words : words.slice(0, end), look};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Style words a library's default look is usually named by. */
|
|
902
|
+
const DEFAULT_STYLE_WORDS = ['default', 'regular', 'outline', 'outlined'];
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* The word each look position prefers: a word of `prefer` it has (the set's
|
|
906
|
+
* default variant), else a usual default style word, else the middle of a
|
|
907
|
+
* series of numbered words (`wght300` … `wght700` gives `wght500`), else
|
|
908
|
+
* its first word in order.
|
|
909
|
+
* @param {Array<Set<string>>} positions
|
|
910
|
+
* @param {string[]} [prefer]
|
|
911
|
+
* @returns {string[]}
|
|
912
|
+
*/
|
|
913
|
+
export function preferredLook(positions, prefer = []) {
|
|
914
|
+
const wanted = prefer.map(word => word.toLowerCase());
|
|
915
|
+
return positions.map(position => {
|
|
916
|
+
const values = [...position].sort();
|
|
917
|
+
const chosen =
|
|
918
|
+
wanted.find(word => position.has(word)) ??
|
|
919
|
+
DEFAULT_STYLE_WORDS.find(word => position.has(word));
|
|
920
|
+
if (chosen) return chosen;
|
|
921
|
+
const numbered = values.map(value => /^([a-z]*)(\d+)$/u.exec(value));
|
|
922
|
+
const lead = numbered[0]?.[1];
|
|
923
|
+
if (
|
|
924
|
+
values.length > 1 &&
|
|
925
|
+
numbered.every(match => match != null && match[1] === lead)
|
|
926
|
+
) {
|
|
927
|
+
const series = values
|
|
928
|
+
.map((value, index) => ({
|
|
929
|
+
value,
|
|
930
|
+
n: Number(/** @type {RegExpExecArray} */ (numbered[index])[2]),
|
|
931
|
+
}))
|
|
932
|
+
.sort((a, b) => a.n - b.n);
|
|
933
|
+
return series[Math.floor((series.length - 1) / 2)].value;
|
|
934
|
+
}
|
|
935
|
+
return values[0];
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* One icon a library gives.
|
|
941
|
+
* @typedef {object} LibraryIcon
|
|
942
|
+
* @property {string} export the library export it reads
|
|
943
|
+
* @property {string} id
|
|
944
|
+
* @property {string} name the name the set exports it as
|
|
945
|
+
* @property {boolean} adapt
|
|
946
|
+
* @property {string[]} aliases the ids of the other names the library
|
|
947
|
+
* exports the icon as, leaving out the library's own name (`lucide-trash`)
|
|
948
|
+
* @property {Array<string | null>} [look] in a library that encodes looks:
|
|
949
|
+
* this export's look words by position, the last word first
|
|
950
|
+
* @property {string[]} [looks] in a library that encodes looks: every
|
|
951
|
+
* export that draws the glyph, this one included
|
|
952
|
+
*/
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* The words a library's looks are named by, and the look a set takes from
|
|
956
|
+
* it: what {@link libraryIcons} found, for the receipt.
|
|
957
|
+
* @typedef {object} LibraryLookSummary
|
|
958
|
+
* @property {string[][]} positions each position's words, sorted, the last
|
|
959
|
+
* word of a name first
|
|
960
|
+
* @property {string[]} preferred the word taken in each position
|
|
961
|
+
* @property {number} exports how many exports draw the icons
|
|
962
|
+
*/
|
|
963
|
+
|
|
726
964
|
/**
|
|
727
965
|
* Every icon a whole library exports, for wrapping it at once: one entry per
|
|
728
966
|
* component (and, with an adapter, per data export), read by its `…Icon`
|
|
729
967
|
* export when it has one (an alias of the same value is recorded once), with
|
|
730
968
|
* its id and the name the set exports it as from the name, the library
|
|
731
969
|
* prefix left out ({@link libraryPrefix}).
|
|
970
|
+
*
|
|
971
|
+
* A library that encodes looks in its export names ({@link lookPositions})
|
|
972
|
+
* gives one icon per glyph: the look {@link preferredLook} prefers, its id
|
|
973
|
+
* and name without the look words (`XOutline24WGHT500Icon` is `x`,
|
|
974
|
+
* `XIcon`), and its `looks`, every export that draws the glyph.
|
|
732
975
|
* @param {string} file
|
|
733
|
-
* @param {{adapter?: boolean, exclude?: string[]}} [options]
|
|
734
|
-
* whether the set declares one; `exclude`: export names that
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
976
|
+
* @param {{adapter?: boolean, exclude?: string[], prefer?: string[] | ((positions: Array<Set<string>>) => string[]), module?: string, progress?: (line: string) => void, summary?: (summary: LibraryLookSummary) => void}} [options]
|
|
977
|
+
* `adapter`: whether the set declares one; `exclude`: export names that
|
|
978
|
+
* are not icons; `prefer`: look words to prefer (the set's default
|
|
979
|
+
* variant), or a function that gives them from the library's look
|
|
980
|
+
* positions before any glyph is picked; `module`: the module as the author named it, for progress;
|
|
981
|
+
* `progress`: hears the reading of a large module;
|
|
982
|
+
* `summary`: hears the looks found, when the library has any
|
|
983
|
+
* @returns {LibraryIcon[]}
|
|
738
984
|
*/
|
|
739
985
|
export function libraryIcons(file, options = {}) {
|
|
986
|
+
const label = options.module ?? path.basename(file);
|
|
987
|
+
/** @type {LibraryIcon[]} */
|
|
988
|
+
let all = [];
|
|
989
|
+
return readingLibrary(
|
|
990
|
+
file,
|
|
991
|
+
{label, progress: options.progress},
|
|
992
|
+
() => {
|
|
993
|
+
all = collectLibraryIcons(file, options);
|
|
994
|
+
const prefer =
|
|
995
|
+
typeof options.prefer === 'function'
|
|
996
|
+
? options.prefer(lookPositions(all.map(icon => icon.id)))
|
|
997
|
+
: options.prefer;
|
|
998
|
+
const grouped = libraryLooks(all, prefer);
|
|
999
|
+
if (grouped.positions.length > 0) {
|
|
1000
|
+
options.summary?.({
|
|
1001
|
+
positions: grouped.positions.map(position => [...position].sort()),
|
|
1002
|
+
preferred: grouped.preferred,
|
|
1003
|
+
exports: all.length,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
return grouped.icons;
|
|
1007
|
+
},
|
|
1008
|
+
icons =>
|
|
1009
|
+
icons.length === all.length
|
|
1010
|
+
? `Found ${icons.length} icons in ${label}.`
|
|
1011
|
+
: `Found ${icons.length} icons in ${label}, drawn by ${all.length} exports in their looks.`,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Read a library with its progress reported: when `progress` is given and
|
|
1017
|
+
* the module has {@link PROGRESS_FROM} exports or more, a line before `read`
|
|
1018
|
+
* runs and the line `found` gives after.
|
|
1019
|
+
* @template T
|
|
1020
|
+
* @param {string} file
|
|
1021
|
+
* @param {{label: string, progress?: (line: string) => void}} reading
|
|
1022
|
+
* `label`: the module as the author named it
|
|
1023
|
+
* @param {() => T} read
|
|
1024
|
+
* @param {(result: T) => string} found
|
|
1025
|
+
* @returns {T}
|
|
1026
|
+
*/
|
|
1027
|
+
function readingLibrary(file, {label, progress}, read, found) {
|
|
1028
|
+
const exportCount = progress ? followedExportNames(file).length : 0;
|
|
1029
|
+
if (progress == null || exportCount < PROGRESS_FROM) return read();
|
|
1030
|
+
progress(`Reading ${exportCount} exports of ${label}...`);
|
|
1031
|
+
const result = read();
|
|
1032
|
+
progress(found(result));
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* The library's icons grouped by glyph when its export names encode looks
|
|
1038
|
+
* ({@link libraryIcons}), with the look positions and the words preferred.
|
|
1039
|
+
* Without look positions the icons are returned as they are.
|
|
1040
|
+
* @param {LibraryIcon[]} all every icon, one per export
|
|
1041
|
+
* @param {string[]} [prefer]
|
|
1042
|
+
* @returns {{icons: LibraryIcon[], positions: Array<Set<string>>, preferred: string[]}}
|
|
1043
|
+
*/
|
|
1044
|
+
export function libraryLooks(all, prefer = []) {
|
|
1045
|
+
const positions = lookPositions(all.map(icon => icon.id));
|
|
1046
|
+
if (positions.length === 0) return {icons: all, positions, preferred: []};
|
|
1047
|
+
const preferred = preferredLook(positions, prefer);
|
|
1048
|
+
// How far a look is from the preferred one, as a sortable key: the
|
|
1049
|
+
// position first in the name (the style) counts most.
|
|
1050
|
+
const distance = (/** @type {Array<string | null>} */ look) => {
|
|
1051
|
+
let key = '';
|
|
1052
|
+
for (let index = look.length - 1; index >= 0; index--) {
|
|
1053
|
+
key += look[index] === preferred[index] ? '0' : '1';
|
|
1054
|
+
}
|
|
1055
|
+
return key;
|
|
1056
|
+
};
|
|
1057
|
+
/** @type {Map<string, {best: LibraryIcon, look: Array<string | null>, key: string, looks: string[]}>} */
|
|
1058
|
+
const byGlyph = new Map();
|
|
1059
|
+
for (const icon of all) {
|
|
1060
|
+
const {base, look} = splitLook(icon.id.split('-'), positions);
|
|
1061
|
+
const id = base.join('-');
|
|
1062
|
+
const key = distance(look);
|
|
1063
|
+
const group = byGlyph.get(id);
|
|
1064
|
+
if (!group) {
|
|
1065
|
+
byGlyph.set(id, {best: icon, look, key, looks: [icon.export]});
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
group.looks.push(icon.export);
|
|
1069
|
+
if (
|
|
1070
|
+
key < group.key ||
|
|
1071
|
+
(key === group.key && icon.export < group.best.export)
|
|
1072
|
+
) {
|
|
1073
|
+
group.best = icon;
|
|
1074
|
+
group.look = look;
|
|
1075
|
+
group.key = key;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/** @type {LibraryIcon[]} */
|
|
1079
|
+
const icons = [];
|
|
1080
|
+
for (const [id, {best, look, looks}] of byGlyph) {
|
|
1081
|
+
const pascal = id
|
|
1082
|
+
.split('-')
|
|
1083
|
+
.map(word => word[0].toUpperCase() + word.slice(1))
|
|
1084
|
+
.join('');
|
|
1085
|
+
icons.push({
|
|
1086
|
+
...best,
|
|
1087
|
+
id,
|
|
1088
|
+
name: iconExportName(pascal),
|
|
1089
|
+
aliases: best.aliases.filter(alias => !byGlyph.has(alias)),
|
|
1090
|
+
look,
|
|
1091
|
+
looks: looks.sort(),
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
icons.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1095
|
+
return {icons, positions, preferred};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Every icon a library exports, one per export: {@link libraryIcons} before
|
|
1100
|
+
* looks are grouped. A large module reports its reading, as
|
|
1101
|
+
* {@link libraryIcons} does.
|
|
1102
|
+
* @param {string} file
|
|
1103
|
+
* @param {{adapter?: boolean, exclude?: string[], module?: string, progress?: (line: string) => void}} [options]
|
|
1104
|
+
* `module`: the module as the author named it, for progress; `progress`:
|
|
1105
|
+
* hears the reading of a large module
|
|
1106
|
+
* @returns {LibraryIcon[]}
|
|
1107
|
+
*/
|
|
1108
|
+
export function everyLibraryIcon(file, options = {}) {
|
|
1109
|
+
const label = options.module ?? path.basename(file);
|
|
1110
|
+
return readingLibrary(
|
|
1111
|
+
file,
|
|
1112
|
+
{label, progress: options.progress},
|
|
1113
|
+
() => collectLibraryIcons(file, options),
|
|
1114
|
+
icons => `Found ${icons.length} exports in ${label}.`,
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* {@link everyLibraryIcon} without progress.
|
|
1120
|
+
* @param {string} file
|
|
1121
|
+
* @param {{adapter?: boolean, exclude?: string[]}} [options]
|
|
1122
|
+
* @returns {LibraryIcon[]}
|
|
1123
|
+
*/
|
|
1124
|
+
function collectLibraryIcons(file, options = {}) {
|
|
740
1125
|
const candidates = iconCandidates(file, options);
|
|
741
1126
|
/** @type {Map<string, {names: string[], adapt: boolean, scalar: string | null}>} */
|
|
742
1127
|
const byOrigin = new Map();
|
|
@@ -830,16 +1215,29 @@ export function iconDefinitionSource(doc) {
|
|
|
830
1215
|
}
|
|
831
1216
|
|
|
832
1217
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
1218
|
+
* A new definition's name as written in a command: camelCase as given, or
|
|
1219
|
+
* kebab-case as the docs list definitions (`ai-helper` is `aiHelper`).
|
|
1220
|
+
* @param {string} name
|
|
1221
|
+
*/
|
|
1222
|
+
export function definitionNameFromInput(name) {
|
|
1223
|
+
return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/u.test(name)
|
|
1224
|
+
? name.replace(/-([a-z0-9])/gu, (_, c) => c.toUpperCase())
|
|
1225
|
+
: name;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Check a new icon definition for a set: a camelCase name (or its kebab-case
|
|
1230
|
+
* form, {@link definitionNameFromInput}) no Astryx definition has, a
|
|
1231
|
+
* description, and at least one lower-case word.
|
|
835
1232
|
* @param {DiscoveredIconSet} set
|
|
836
|
-
* @param {{name: string, description?: string, words?: string[]}}
|
|
1233
|
+
* @param {{name: string, description?: string, words?: string[]}} given
|
|
837
1234
|
* @returns {{name: string, description: string, words: string[]}}
|
|
838
1235
|
*/
|
|
839
|
-
export function checkNewDefinition(set,
|
|
1236
|
+
export function checkNewDefinition(set, given) {
|
|
1237
|
+
const doc = {...given, name: definitionNameFromInput(given.name)};
|
|
840
1238
|
if (!/^[a-z][a-zA-Z0-9]*$/u.test(doc.name)) {
|
|
841
1239
|
throw new IconSetAuthoringError(
|
|
842
|
-
`Invalid icon definition name "${doc.name}": use camelCase starting with a lower-case letter.`,
|
|
1240
|
+
`Invalid icon definition name "${doc.name}": use camelCase (aiHelper) or kebab-case (ai-helper), starting with a lower-case letter.`,
|
|
843
1241
|
'ERR_INVALID_ARGUMENT',
|
|
844
1242
|
);
|
|
845
1243
|
}
|
|
@@ -919,8 +1317,11 @@ const GENERATED_HEADER = [
|
|
|
919
1317
|
|
|
920
1318
|
/**
|
|
921
1319
|
* The top-level statements of a generated module, one normalized line each,
|
|
922
|
-
* however the file
|
|
923
|
-
*
|
|
1320
|
+
* however a formatter left the file (Prettier's line breaks, double quotes,
|
|
1321
|
+
* trailing commas, semicolons or none): whitespace collapsed, no padding
|
|
1322
|
+
* inside braces or parentheses, no trailing comma, type members separated by
|
|
1323
|
+
* `; ` with none before the closing brace, no statement semicolon, and
|
|
1324
|
+
* single-quoted strings. Comments are dropped.
|
|
924
1325
|
* @param {string} text
|
|
925
1326
|
* @param {string} label the module's path, for the error
|
|
926
1327
|
* @returns {string[]}
|
|
@@ -935,17 +1336,110 @@ function generatedStatements(text, label) {
|
|
|
935
1336
|
'ERR_INVALID_ARGUMENT',
|
|
936
1337
|
);
|
|
937
1338
|
}
|
|
938
|
-
return ast.program.body.map(node =>
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
)
|
|
944
|
-
.
|
|
945
|
-
.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1339
|
+
return ast.program.body.map(node => {
|
|
1340
|
+
const start = /** @type {number} */ (node.start);
|
|
1341
|
+
// A formatter without semicolons ends each type member with a line
|
|
1342
|
+
// break, which collapses to a space: give each member its separator.
|
|
1343
|
+
const separators = typeMemberEnds(node)
|
|
1344
|
+
.filter(end => !/^\s*[;,]/u.test(text.slice(end, end + 1)))
|
|
1345
|
+
.filter(end => !/[;,]$/u.test(text.slice(end - 1, end)))
|
|
1346
|
+
.sort((a, b) => b - a);
|
|
1347
|
+
let statement = text.slice(start, /** @type {number} */ (node.end));
|
|
1348
|
+
for (const end of separators) {
|
|
1349
|
+
const at = end - start;
|
|
1350
|
+
statement = `${statement.slice(0, at)};${statement.slice(at)}`;
|
|
1351
|
+
}
|
|
1352
|
+
return normalizedStatement(statement);
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* One statement's text in the normalized form {@link generatedStatements}
|
|
1358
|
+
* gives.
|
|
1359
|
+
* @param {string} statement
|
|
1360
|
+
*/
|
|
1361
|
+
function normalizedStatement(statement) {
|
|
1362
|
+
return statement
|
|
1363
|
+
.replace(/\s+/gu, ' ')
|
|
1364
|
+
.replace(/"((?:[^"'\\]|\\.)*)"/gu, "'$1'")
|
|
1365
|
+
.replace(/([{(]) /gu, '$1')
|
|
1366
|
+
.replace(/[,;]? ?([})])/gu, '$1')
|
|
1367
|
+
.trim()
|
|
1368
|
+
.replace(/ ?;$/u, '');
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Where each type member of a statement ends (the members of every type
|
|
1373
|
+
* literal and interface in it).
|
|
1374
|
+
* @param {any} node
|
|
1375
|
+
* @returns {number[]}
|
|
1376
|
+
*/
|
|
1377
|
+
function typeMemberEnds(node) {
|
|
1378
|
+
/** @type {number[]} */
|
|
1379
|
+
const ends = [];
|
|
1380
|
+
/** @param {any} each */
|
|
1381
|
+
const visit = each => {
|
|
1382
|
+
if (each == null || typeof each !== 'object') return;
|
|
1383
|
+
if (Array.isArray(each)) {
|
|
1384
|
+
for (const item of each) visit(item);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof each.type !== 'string') return;
|
|
1388
|
+
const members =
|
|
1389
|
+
each.type === 'TSTypeLiteral'
|
|
1390
|
+
? each.members
|
|
1391
|
+
: each.type === 'TSInterfaceBody'
|
|
1392
|
+
? each.body
|
|
1393
|
+
: null;
|
|
1394
|
+
if (members) {
|
|
1395
|
+
for (const member of members) ends.push(member.end);
|
|
1396
|
+
}
|
|
1397
|
+
for (const key of Object.keys(each)) {
|
|
1398
|
+
if (
|
|
1399
|
+
key === 'loc' ||
|
|
1400
|
+
key === 'leadingComments' ||
|
|
1401
|
+
key === 'trailingComments' ||
|
|
1402
|
+
key === 'innerComments'
|
|
1403
|
+
) {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
visit(each[key]);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
visit(node);
|
|
1410
|
+
return ends;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* A normalized statement as the generated module writes it: with its
|
|
1415
|
+
* semicolon, except after a declaration that ends in its own block.
|
|
1416
|
+
* @param {string} statement
|
|
1417
|
+
*/
|
|
1418
|
+
function terminated(statement) {
|
|
1419
|
+
return /^(?:export (?:default )?)?(?:declare )?(?:abstract )?(?:async )?(?:function\b|class\b|interface\b|enum\b|namespace\b|module\b)/u.test(
|
|
1420
|
+
statement,
|
|
1421
|
+
)
|
|
1422
|
+
? statement
|
|
1423
|
+
: `${statement};`;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Whether a generated declaration file declares what its generated module
|
|
1428
|
+
* gives, statement by statement however a formatter left either file
|
|
1429
|
+
* ({@link generatedStatements}); the header comment is ignored.
|
|
1430
|
+
* @param {string} actual the declaration file as it is
|
|
1431
|
+
* @param {string} expected {@link generatedTypesText} for the module
|
|
1432
|
+
* @param {string} label the declaration file's path, for a parse error
|
|
1433
|
+
*/
|
|
1434
|
+
export function generatedTypesMatch(actual, expected, label) {
|
|
1435
|
+
let have;
|
|
1436
|
+
try {
|
|
1437
|
+
have = generatedStatements(actual, label);
|
|
1438
|
+
} catch {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
const want = generatedStatements(expected, label);
|
|
1442
|
+
return have.length === want.length && have.every((s, i) => s === want[i]);
|
|
949
1443
|
}
|
|
950
1444
|
|
|
951
1445
|
/** The local name the generated module gives the set's adapter. */
|
|
@@ -955,7 +1449,7 @@ const ADAPTER_LOCAL = 'adaptIcon';
|
|
|
955
1449
|
export const THEME_MAP_NAME = 'themeIcons';
|
|
956
1450
|
|
|
957
1451
|
/** How the generated module imports React for its theme map. */
|
|
958
|
-
const REACT_IMPORT = "import {createElement} from 'react'
|
|
1452
|
+
const REACT_IMPORT = "import {createElement} from 'react'";
|
|
959
1453
|
|
|
960
1454
|
/** The built-in icon definitions: the shared names a theme map covers. */
|
|
961
1455
|
const BUILT_IN = new Set(ICON_NAMES);
|
|
@@ -987,10 +1481,15 @@ export function generatedModulePath(set, packageDir) {
|
|
|
987
1481
|
* component
|
|
988
1482
|
*/
|
|
989
1483
|
|
|
990
|
-
/**
|
|
1484
|
+
/**
|
|
1485
|
+
* The props every theme-map element gets: hidden from assistive tech, and
|
|
1486
|
+
* sized to its box by CSS as well as by attributes. A theme's icon sits in a
|
|
1487
|
+
* box whose font size is the icon size; CSS width and height override the
|
|
1488
|
+
* svg's own size attributes, so a component that writes its own (`size`
|
|
1489
|
+
* 24, ignoring `width` and `height`) still draws at the box's size.
|
|
1490
|
+
*/
|
|
991
1491
|
const THEME_ELEMENT_PROPS =
|
|
992
|
-
"{'aria-hidden': true, 'width': '1em', 'height': '1em'}";
|
|
993
|
-
|
|
1492
|
+
"{'aria-hidden': true, 'width': '1em', 'height': '1em', 'style': {'width': '1em', 'height': '1em'}}";
|
|
994
1493
|
|
|
995
1494
|
/**
|
|
996
1495
|
* The declaration file's text for a generated module's statements: each
|
|
@@ -1003,10 +1502,11 @@ export function generatedTypesText(statements) {
|
|
|
1003
1502
|
/** @type {string[]} */
|
|
1004
1503
|
const lines = [];
|
|
1005
1504
|
const react = new Set();
|
|
1006
|
-
for (const
|
|
1505
|
+
for (const given of statements) {
|
|
1506
|
+
const statement = normalizedStatement(given);
|
|
1007
1507
|
if (statement.startsWith('import ')) continue;
|
|
1008
1508
|
if (/^export (?:\*|\{[^}]*\}) from /u.test(statement)) {
|
|
1009
|
-
lines.push(statement);
|
|
1509
|
+
lines.push(terminated(statement));
|
|
1010
1510
|
continue;
|
|
1011
1511
|
}
|
|
1012
1512
|
const adapted = new RegExp(
|
|
@@ -1020,7 +1520,7 @@ export function generatedTypesText(statements) {
|
|
|
1020
1520
|
);
|
|
1021
1521
|
continue;
|
|
1022
1522
|
}
|
|
1023
|
-
const theme = /^export const (\w+) = \{(.*)\}
|
|
1523
|
+
const theme = /^export const (\w+) = \{(.*)\}$/u.exec(statement);
|
|
1024
1524
|
if (
|
|
1025
1525
|
theme &&
|
|
1026
1526
|
(theme[1] === THEME_MAP_NAME || statement.includes('createElement('))
|
|
@@ -1091,46 +1591,47 @@ export function generatedModuleText(
|
|
|
1091
1591
|
};
|
|
1092
1592
|
const themeName = data.themeMap === THEME_MAP_NAME ? THEME_MAP_NAME : null;
|
|
1093
1593
|
const lines = before == null ? [] : generatedStatements(before, generated);
|
|
1594
|
+
const lineSet = new Set(lines);
|
|
1094
1595
|
// Written once, below: the adapter's import, React's createElement, and
|
|
1095
|
-
// the theme map.
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
name =>
|
|
1099
|
-
new RegExp(
|
|
1100
|
-
`(?:\\bas ${name}(?:Source)?\\}|^export const ${name} = |^export \\{${name}\\} |^import ${name}Source )`,
|
|
1101
|
-
'u',
|
|
1102
|
-
),
|
|
1103
|
-
);
|
|
1596
|
+
// the theme map. Every lookup below is by name, so a library of twenty
|
|
1597
|
+
// thousand icons is written in one pass.
|
|
1598
|
+
const removed = new Set(remove);
|
|
1104
1599
|
// A re-export binds no local name, so the theme map reads each re-exported
|
|
1105
1600
|
// icon through an import that mirrors its re-export. Those imports are
|
|
1106
1601
|
// rewritten with the theme map.
|
|
1107
1602
|
const themeImport = (/** @type {string} */ line) =>
|
|
1108
1603
|
themeName != null &&
|
|
1109
1604
|
line.startsWith('import {') &&
|
|
1110
|
-
|
|
1111
|
-
const managed = (/** @type {string} */ line) =>
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1605
|
+
lineSet.has(`export${line.slice('import'.length)}`);
|
|
1606
|
+
const managed = (/** @type {string} */ line) => {
|
|
1607
|
+
const name = statementName(line);
|
|
1608
|
+
if (name != null) {
|
|
1609
|
+
if (removed.has(name)) return true;
|
|
1610
|
+
if (name.endsWith('Source') && removed.has(name.slice(0, -6))) {
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
if (name === ADAPTER_LOCAL) return true;
|
|
1614
|
+
}
|
|
1615
|
+
return (
|
|
1616
|
+
line === REACT_IMPORT ||
|
|
1617
|
+
themeImport(line) ||
|
|
1618
|
+
(themeName != null && line.startsWith(`export const ${themeName} = `))
|
|
1619
|
+
);
|
|
1620
|
+
};
|
|
1117
1621
|
const imports = new Set(
|
|
1118
1622
|
lines.filter(line => line.startsWith('import ') && !managed(line)),
|
|
1119
1623
|
);
|
|
1120
1624
|
const exports = new Set(
|
|
1121
1625
|
lines.filter(line => !line.startsWith('import ') && !managed(line)),
|
|
1122
1626
|
);
|
|
1627
|
+
/** @type {Map<string, string>} each exported name → its statement */
|
|
1628
|
+
const exported = new Map();
|
|
1629
|
+
for (const line of exports) {
|
|
1630
|
+
const name = statementName(line);
|
|
1631
|
+
if (name != null && !exported.has(name)) exported.set(name, line);
|
|
1632
|
+
}
|
|
1123
1633
|
/** @param {string} name */
|
|
1124
|
-
const existing = name =>
|
|
1125
|
-
[...exports].find(line =>
|
|
1126
|
-
// The name the module exports: after `as`, alone in braces, or a
|
|
1127
|
-
// const; never the source name of another export (`TrashIcon as
|
|
1128
|
-
// TrashSolidIcon` exports TrashSolidIcon).
|
|
1129
|
-
new RegExp(
|
|
1130
|
-
`(?:\\bas\\s+${name}\\s*\\}|^export\\s+\\{\\s*${name}\\s*\\}|^export const\\s+${name}\\b)`,
|
|
1131
|
-
'u',
|
|
1132
|
-
).test(line),
|
|
1133
|
-
);
|
|
1634
|
+
const existing = name => exported.get(name);
|
|
1134
1635
|
for (const icon of icons) {
|
|
1135
1636
|
const specifier = specifierOf(icon.from);
|
|
1136
1637
|
/** @type {string[]} */
|
|
@@ -1141,29 +1642,32 @@ export function generatedModuleText(
|
|
|
1141
1642
|
const local = `${icon.name}Source`;
|
|
1142
1643
|
wantImports.push(
|
|
1143
1644
|
icon.source === 'default'
|
|
1144
|
-
? `import ${local} from '${specifier}'
|
|
1145
|
-
: `import {${icon.source === local ? local : `${icon.source} as ${local}`}} from '${specifier}'
|
|
1645
|
+
? `import ${local} from '${specifier}'`
|
|
1646
|
+
: `import {${icon.source === local ? local : `${icon.source} as ${local}`}} from '${specifier}'`,
|
|
1146
1647
|
);
|
|
1147
|
-
want.push(`export const ${icon.name} = ${ADAPTER_LOCAL}(${local})
|
|
1648
|
+
want.push(`export const ${icon.name} = ${ADAPTER_LOCAL}(${local})`);
|
|
1148
1649
|
} else {
|
|
1149
1650
|
want.push(
|
|
1150
1651
|
icon.source === icon.name
|
|
1151
|
-
? `export {${icon.name}} from '${specifier}'
|
|
1152
|
-
: `export {${icon.source} as ${icon.name}} from '${specifier}'
|
|
1652
|
+
? `export {${icon.name}} from '${specifier}'`
|
|
1653
|
+
: `export {${icon.source} as ${icon.name}} from '${specifier}'`,
|
|
1153
1654
|
);
|
|
1154
1655
|
}
|
|
1155
1656
|
const prior = existing(icon.name);
|
|
1156
1657
|
if (prior) {
|
|
1157
1658
|
if (prior !== want[0]) {
|
|
1158
1659
|
throw new IconSetAuthoringError(
|
|
1159
|
-
`The generated icons module already exports ${icon.name} as \`${prior}\`. Pass another name, or edit ${generated}.`,
|
|
1660
|
+
`The generated icons module already exports ${icon.name} as \`${terminated(prior)}\`. Pass another name, or edit ${generated}.`,
|
|
1160
1661
|
'ERR_FILE_EXISTS',
|
|
1161
1662
|
);
|
|
1162
1663
|
}
|
|
1163
1664
|
continue;
|
|
1164
1665
|
}
|
|
1165
1666
|
for (const line of wantImports) imports.add(line);
|
|
1166
|
-
for (const line of want)
|
|
1667
|
+
for (const line of want) {
|
|
1668
|
+
exports.add(line);
|
|
1669
|
+
exported.set(icon.name, line);
|
|
1670
|
+
}
|
|
1167
1671
|
}
|
|
1168
1672
|
const needsAdapter = [...exports].some(line =>
|
|
1169
1673
|
line.includes(`= ${ADAPTER_LOCAL}(`),
|
|
@@ -1186,12 +1690,9 @@ export function generatedModuleText(
|
|
|
1186
1690
|
theme.push(
|
|
1187
1691
|
` ${name}: createElement(${exportName}, ${THEME_ELEMENT_PROPS}),`,
|
|
1188
1692
|
);
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
'u',
|
|
1193
|
-
).test(line),
|
|
1194
|
-
);
|
|
1693
|
+
const line = exported.get(exportName);
|
|
1694
|
+
const reexport =
|
|
1695
|
+
line != null && line.startsWith('export {') ? line : undefined;
|
|
1195
1696
|
if (reexport) {
|
|
1196
1697
|
themeImports.add(`import${reexport.slice('export'.length)}`);
|
|
1197
1698
|
}
|
|
@@ -1201,15 +1702,17 @@ export function generatedModuleText(
|
|
|
1201
1702
|
const adapterLine =
|
|
1202
1703
|
needsAdapter && data.adapter
|
|
1203
1704
|
? [
|
|
1204
|
-
`import {${data.adapter.export} as ${ADAPTER_LOCAL}} from '${specifierOf(data.adapter.module)}'
|
|
1705
|
+
`import {${data.adapter.export} as ${ADAPTER_LOCAL}} from '${specifierOf(data.adapter.module)}'`,
|
|
1205
1706
|
]
|
|
1206
1707
|
: [];
|
|
1207
1708
|
const js = [
|
|
1208
1709
|
...GENERATED_HEADER,
|
|
1209
|
-
...
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1710
|
+
...[
|
|
1711
|
+
...(themeName ? [REACT_IMPORT] : []),
|
|
1712
|
+
...adapterLine,
|
|
1713
|
+
...[...imports, ...themeImports].sort(),
|
|
1714
|
+
...[...exports].sort(),
|
|
1715
|
+
].map(terminated),
|
|
1213
1716
|
...(themeName
|
|
1214
1717
|
? theme.length > 0
|
|
1215
1718
|
? [`export const ${themeName} = {`, ...theme, '};']
|
|
@@ -1226,6 +1729,25 @@ export function generatedModuleText(
|
|
|
1226
1729
|
};
|
|
1227
1730
|
}
|
|
1228
1731
|
|
|
1732
|
+
/**
|
|
1733
|
+
* The name one generated statement binds: the name an export exports
|
|
1734
|
+
* (`export {A as B} from …` is `B`, `export {A} from …` is `A`, `export
|
|
1735
|
+
* const A = …` is `A`), or the local name an import binds (`import {A as
|
|
1736
|
+
* BSource} from …` is `BSource`, `import BSource from …` is `BSource`).
|
|
1737
|
+
* Null for anything else.
|
|
1738
|
+
* @param {string} line
|
|
1739
|
+
* @returns {string | null}
|
|
1740
|
+
*/
|
|
1741
|
+
function statementName(line) {
|
|
1742
|
+
const named =
|
|
1743
|
+
/^(?:export|import) \{\s*([$\w]+)(?:\s+as\s+([$\w]+))?\s*\}/u.exec(line);
|
|
1744
|
+
if (named) return named[2] ?? named[1];
|
|
1745
|
+
const constant = /^export const ([$\w]+)\b/u.exec(line);
|
|
1746
|
+
if (constant) return constant[1];
|
|
1747
|
+
const plain = /^import ([$\w]+) from /u.exec(line);
|
|
1748
|
+
return plain ? plain[1] : null;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1229
1751
|
/**
|
|
1230
1752
|
* Every name the generated module already exports one library export as: `export {featherSun as SunIcon} from '…'` or the import an adapted
|
|
1231
1753
|
* icon reads (`import {featherSun as SunIconSource} from '…'`). Adding the
|
|
@@ -1256,7 +1778,7 @@ export function generatedNamesOf(packageDir, generated, from, source) {
|
|
|
1256
1778
|
.join('/');
|
|
1257
1779
|
if (!specifier.startsWith('.')) specifier = `./${specifier}`;
|
|
1258
1780
|
}
|
|
1259
|
-
const tail = ` from '${specifier}'
|
|
1781
|
+
const tail = ` from '${specifier}'`;
|
|
1260
1782
|
for (const statement of statements) {
|
|
1261
1783
|
if (!statement.endsWith(tail)) continue;
|
|
1262
1784
|
const head = statement.slice(0, -tail.length);
|
|
@@ -1318,7 +1840,7 @@ export function generatedOrigins(packageDir, generated) {
|
|
|
1318
1840
|
return `./${rel}`;
|
|
1319
1841
|
};
|
|
1320
1842
|
for (const statement of statements) {
|
|
1321
|
-
const reexport = /^export \{(\w+)(?: as (\w+))?\} from '([^']+)'
|
|
1843
|
+
const reexport = /^export \{(\w+)(?: as (\w+))?\} from '([^']+)'$/u.exec(
|
|
1322
1844
|
statement,
|
|
1323
1845
|
);
|
|
1324
1846
|
if (reexport) {
|
|
@@ -1328,7 +1850,7 @@ export function generatedOrigins(packageDir, generated) {
|
|
|
1328
1850
|
});
|
|
1329
1851
|
continue;
|
|
1330
1852
|
}
|
|
1331
|
-
const adapted = /^import \{(\w+) as (\w+)Source\} from '([^']+)'
|
|
1853
|
+
const adapted = /^import \{(\w+) as (\w+)Source\} from '([^']+)'$/u.exec(
|
|
1332
1854
|
statement,
|
|
1333
1855
|
);
|
|
1334
1856
|
if (adapted) {
|
|
@@ -1338,7 +1860,7 @@ export function generatedOrigins(packageDir, generated) {
|
|
|
1338
1860
|
});
|
|
1339
1861
|
continue;
|
|
1340
1862
|
}
|
|
1341
|
-
const adaptedDefault = /^import (\w+)Source from '([^']+)'
|
|
1863
|
+
const adaptedDefault = /^import (\w+)Source from '([^']+)'$/u.exec(
|
|
1342
1864
|
statement,
|
|
1343
1865
|
);
|
|
1344
1866
|
if (adaptedDefault) {
|
|
@@ -1376,6 +1898,63 @@ export function iconModulePackage(specifier) {
|
|
|
1376
1898
|
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
1377
1899
|
}
|
|
1378
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* The set's exports that read one file of a package, where a refresh from
|
|
1903
|
+
* that package's root can read them from the root instead. An export moves
|
|
1904
|
+
* when the root exports the same icon: an export that resolves to the same
|
|
1905
|
+
* file and binding. When the subpath export cannot be resolved, a root
|
|
1906
|
+
* export of the same name counts. Each maps to the root's
|
|
1907
|
+
* export and the module it reads now. Empty unless `root` is a package
|
|
1908
|
+
* root, not a subpath or a ./ path.
|
|
1909
|
+
* @param {Map<string, {module: string, source: string}>} origins the
|
|
1910
|
+
* set's exports ({@link generatedOrigins})
|
|
1911
|
+
* @param {{file: string, module: string}} root the refreshed module
|
|
1912
|
+
* @param {string[]} rootExports the root's icon exports
|
|
1913
|
+
* @param {{packageDir: string, pkg: Record<string, any>}} where
|
|
1914
|
+
* @returns {Map<string, {source: string, from: string}>}
|
|
1915
|
+
*/
|
|
1916
|
+
export function packageRootMoves(origins, root, rootExports, where) {
|
|
1917
|
+
/** @type {Map<string, {source: string, from: string}>} */
|
|
1918
|
+
const moves = new Map();
|
|
1919
|
+
if (iconModulePackage(root.module) !== root.module) return moves;
|
|
1920
|
+
if (root.module.startsWith('.')) return moves;
|
|
1921
|
+
const subpaths = [...origins].filter(
|
|
1922
|
+
([, origin]) =>
|
|
1923
|
+
!origin.module.startsWith('.') &&
|
|
1924
|
+
origin.module !== root.module &&
|
|
1925
|
+
iconModulePackage(origin.module) === root.module,
|
|
1926
|
+
);
|
|
1927
|
+
if (subpaths.length === 0) return moves;
|
|
1928
|
+
/** @param {{file: string, name: string}} origin */
|
|
1929
|
+
const keyOf = origin => `${origin.file}\0${origin.name}`;
|
|
1930
|
+
/** @type {Map<string, string>} */
|
|
1931
|
+
const byOrigin = new Map();
|
|
1932
|
+
for (const name of rootExports) {
|
|
1933
|
+
const origin = moduleExportOrigin(root.file, name);
|
|
1934
|
+
if (origin && !byOrigin.has(keyOf(origin))) {
|
|
1935
|
+
byOrigin.set(keyOf(origin), name);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
const names = new Set(rootExports);
|
|
1939
|
+
const self = {
|
|
1940
|
+
name: typeof where.pkg.name === 'string' ? where.pkg.name : '',
|
|
1941
|
+
dir: where.packageDir,
|
|
1942
|
+
};
|
|
1943
|
+
for (const [name, origin] of subpaths) {
|
|
1944
|
+
const file = resolveIconModule(origin.module, where.packageDir, self);
|
|
1945
|
+
const resolved = file ? moduleExportOrigin(file, origin.source) : null;
|
|
1946
|
+
// A name match counts only when the subpath export cannot be resolved.
|
|
1947
|
+
// A resolved export is the same icon only when it reads the same file.
|
|
1948
|
+
const source = resolved
|
|
1949
|
+
? byOrigin.get(keyOf(resolved))
|
|
1950
|
+
: names.has(origin.source)
|
|
1951
|
+
? origin.source
|
|
1952
|
+
: undefined;
|
|
1953
|
+
if (source) moves.set(name, {source, from: origin.module});
|
|
1954
|
+
}
|
|
1955
|
+
return moves;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1379
1958
|
/**
|
|
1380
1959
|
* Write the set's generated module and its declaration file
|
|
1381
1960
|
* ({@link generatedModuleText}). Returns whether either changed, and a
|
|
@@ -1562,12 +2141,30 @@ export function entryWiringNote(packageDir, pkg, generated, options = {}) {
|
|
|
1562
2141
|
inside(typesFile) &&
|
|
1563
2142
|
fs.existsSync(typesFile) &&
|
|
1564
2143
|
!isBuiltOrCommonJs(packageDir, typesFile) &&
|
|
1565
|
-
!
|
|
2144
|
+
!hasStatement(typesFile, lineFor(typesFile))
|
|
1566
2145
|
? ` and \`${lineFor(typesFile)}\` to ${rel(typesFile)}`
|
|
1567
2146
|
: '';
|
|
1568
2147
|
return `Add \`${lineFor(entry)}\` to ${rel(entry)}${alsoTypes}, so the icons import from ${module}.`;
|
|
1569
2148
|
}
|
|
1570
2149
|
|
|
2150
|
+
/**
|
|
2151
|
+
* Whether a file has a statement, however a formatter left it
|
|
2152
|
+
* ({@link generatedStatements}); a file that does not parse is searched for
|
|
2153
|
+
* the text as written.
|
|
2154
|
+
* @param {string} file
|
|
2155
|
+
* @param {string} statement
|
|
2156
|
+
*/
|
|
2157
|
+
function hasStatement(file, statement) {
|
|
2158
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
2159
|
+
try {
|
|
2160
|
+
return generatedStatements(text, file).includes(
|
|
2161
|
+
normalizedStatement(statement),
|
|
2162
|
+
);
|
|
2163
|
+
} catch {
|
|
2164
|
+
return text.includes(statement);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1571
2168
|
/**
|
|
1572
2169
|
* Whether the set's own module exports `name`, read without running it, and
|
|
1573
2170
|
* the line its entry needs when it does not ({@link entryWiringNote}).
|