@@ -14,52 +14,65 @@ import { kebabCaseToCamelCase } from "../../utils/string-utils";
1414import path from "node:path" ;
1515import iconsData from "ionicons/dist/ionicons.json" ;
1616
17+ // パフォーマンス最適化: モジュールレベルで一度だけ作成
18+ const IONIC_COMPONENTS_SET = new Set ( IONIC_COMPONENTS ) ;
19+ const ICON_NAME_REGEX = / { { \s * ' ( [ ^ ' ] + ) ' \s * } } / ;
20+
1721export const generateUseIcons = async (
1822 project : Project ,
1923 cliOptions : CliOptions ,
2024) : Promise < boolean > => {
21- const skippedIconsHtmlAll : string [ ] = [ ] ;
22- const ionIconsAll : string [ ] = [ ] ;
23- const sourceIonIcons = iconsData . icons . map ( ( icon ) => icon . name ) ;
25+ const skippedIconsHtmlAll = new Set < string > ( ) ;
26+ const ionIconsAll = new Set < string > ( ) ;
27+ const sourceIonIcons = new Set ( iconsData . icons . map ( ( icon ) => icon . name ) ) ;
2428
2529 for ( const sourceFile of project . getSourceFiles ( ) ) {
26- if ( sourceFile . getFilePath ( ) . includes ( "node_modules" ) ) {
30+ const filePath = sourceFile . getFilePath ( ) ;
31+
32+ // node_modulesを早期除外(パス解析を使用)
33+ if ( filePath . includes ( "node_modules" ) ) {
2734 continue ;
2835 }
2936
30- if ( sourceFile . getFilePath ( ) . endsWith ( ".html" ) ) {
37+ if ( filePath . endsWith ( ".html" ) ) {
3138 const htmlAsString = sourceFile . getFullText ( ) ;
3239
3340 const { skippedIconsHtml, ionIcons } = detectIonicComponentsAndIcons (
3441 htmlAsString ,
35- sourceFile . getFilePath ( ) ,
36- ) ;
37- skippedIconsHtmlAll . push (
38- ...skippedIconsHtml ,
39- ...ionIcons . filter ( ( icon ) => ! sourceIonIcons . includes ( icon ) ) ,
42+ filePath ,
4043 ) ;
41- ionIconsAll . push (
42- ...ionIcons . filter ( ( icon ) => sourceIonIcons . includes ( icon ) ) ,
43- ) ;
44- } else if ( sourceFile . getFilePath ( ) . endsWith ( ".ts" ) ) {
44+ for ( const icon of skippedIconsHtml ) {
45+ skippedIconsHtmlAll . add ( icon ) ;
46+ }
47+ for ( const icon of ionIcons ) {
48+ if ( sourceIonIcons . has ( icon ) ) {
49+ ionIconsAll . add ( icon ) ;
50+ } else {
51+ skippedIconsHtmlAll . add ( icon ) ;
52+ }
53+ }
54+ } else if ( filePath . endsWith ( ".ts" ) ) {
4555 const templateAsString = getComponentTemplateAsString ( sourceFile ) ;
4656 if ( templateAsString ) {
4757 const { skippedIconsHtml, ionIcons } = detectIonicComponentsAndIcons (
4858 templateAsString ,
49- sourceFile . getFilePath ( ) ,
50- ) ;
51- skippedIconsHtmlAll . push (
52- ...skippedIconsHtml ,
53- ...ionIcons . filter ( ( icon ) => ! sourceIonIcons . includes ( icon ) ) ,
54- ) ;
55- ionIconsAll . push (
56- ...ionIcons . filter ( ( icon ) => sourceIonIcons . includes ( icon ) ) ,
59+ filePath ,
5760 ) ;
61+ for ( const icon of skippedIconsHtml ) {
62+ skippedIconsHtmlAll . add ( icon ) ;
63+ }
64+ for ( const icon of ionIcons ) {
65+ if ( sourceIonIcons . has ( icon ) ) {
66+ ionIconsAll . add ( icon ) ;
67+ } else {
68+ skippedIconsHtmlAll . add ( icon ) ;
69+ }
70+ }
5871 }
5972 }
6073 }
6174
62- const uniqueSkippedIconsHtmlAll = Array . from ( new Set ( skippedIconsHtmlAll ) ) ;
75+ const uniqueSkippedIconsHtmlAll = Array . from ( skippedIconsHtmlAll ) ;
6376 uniqueSkippedIconsHtmlAll . sort ( ) ;
6477 if ( uniqueSkippedIconsHtmlAll . length > 0 ) {
6578 console . warn (
@@ -68,7 +81,7 @@ export const generateUseIcons = async (
6881 ) ;
6982 }
7083
71- const uniqueIonIconsAll = Array . from ( new Set ( ionIconsAll ) ) ;
84+ const uniqueIonIconsAll = Array . from ( ionIconsAll ) ;
7285 uniqueIonIconsAll . sort ( ) ;
7386 const uniqueIconCamelCase = uniqueIonIconsAll . map ( ( ionIcon ) =>
7487 kebabCaseToCamelCase ( ionIcon ) ,
@@ -88,8 +101,9 @@ export const generateUseIcons = async (
88101 /**
89102 * If the number of exported icons is the same as the number of source icons
90103 */
104+ const exportItemsSet = new Set ( exportItems ) ;
91105 const newIcons = uniqueIconCamelCase . filter (
92- ( icon ) => ! exportItems . includes ( icon ) ,
106+ ( icon ) => ! exportItemsSet . has ( icon ) ,
93107 ) ;
94108 if ( newIcons . length === 0 ) {
95109 console . info ( `[Dev] No new icons to add or change to use-icons.ts` ) ;
@@ -122,8 +136,8 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
122136 const ast = parse ( htmlAsString , { filePath } ) ;
123137 const nodes = ast . templateNodes ;
124138
125- const ionicComponents : string [ ] = [ ] ;
126- const ionIcons : string [ ] = [ ] ;
139+ const ionicComponents = new Set < string > ( ) ;
140+ const ionIcons = new Set < string > ( ) ;
127141 const skippedIconsHtml : string [ ] = [ ] ;
128142
129143 let hasRouterLinkWithHref = false ;
@@ -137,10 +151,8 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
137151 ) {
138152 const tagName = node . type === "Template" ? node . tagName : node . name ;
139153
140- if ( IONIC_COMPONENTS . includes ( tagName ) ) {
141- if ( ! ionicComponents . includes ( tagName ) ) {
142- ionicComponents . push ( tagName ) ;
143- }
154+ if ( IONIC_COMPONENTS_SET . has ( tagName ) ) {
155+ ionicComponents . add ( tagName ) ;
144156
145157 const routerLink =
146158 node . attributes . find (
@@ -163,9 +175,7 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
163175
164176 if ( staticNameAttribute ) {
165177 const iconName = staticNameAttribute . value ;
166- if ( ! ionIcons . includes ( iconName ) ) {
167- ionIcons . push ( iconName ) ;
168- }
178+ ionIcons . add ( iconName ) ;
169179 } else {
170180 const boundNameAttribute = node . inputs . find (
171181 ( a : any ) => a . name === attribute ,
@@ -174,40 +184,36 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
174184 if ( boundNameAttribute ) {
175185 const skippedIcon = node . sourceSpan . toString ( ) ;
176186
177- const iconNameRegex = / { { \s * ' ( [ ^ ' ] + ) ' \s * } } / ;
178187 /**
179188 * Attempt to find the icon name from the bound name attribute
180189 * when the developer has a template like this:
181190 * <ion-icon name="'user'"></ion-icon>
182191 */
183- const iconNameMatch = skippedIcon . match ( iconNameRegex ) ;
192+ const iconNameMatch = skippedIcon . match ( ICON_NAME_REGEX ) ;
184193
185194 const deepGetIconConditional = (
186195 ast : typeof boundNameAttribute . value . ast ,
187- icons : string [ ] ,
188- ) : string [ ] => {
196+ iconsSet : Set < string > ,
197+ ) : void => {
189198 if ( ast . trueExp . type === "LiteralPrimitive" ) {
190- icons . push ( ast . trueExp . value ) ;
199+ iconsSet . add ( ast . trueExp . value ) ;
191200 } else if ( ast . trueExp . type === "Conditional" ) {
192- deepGetIconConditional ( ast . trueExp , icons ) ;
201+ deepGetIconConditional ( ast . trueExp , iconsSet ) ;
193202 } else {
194203 skippedIconsHtml . push ( skippedIcon ) ;
195204 }
196205
197206 if ( ast . falseExp . type === "LiteralPrimitive" ) {
198- icons . push ( ast . falseExp . value ) ;
207+ iconsSet . add ( ast . falseExp . value ) ;
199208 } else if ( ast . falseExp . type === "Conditional" ) {
200- deepGetIconConditional ( ast . falseExp , icons ) ;
209+ deepGetIconConditional ( ast . falseExp , iconsSet ) ;
201210 } else {
202211 skippedIconsHtml . push ( skippedIcon ) ;
203212 }
204- return icons ;
205213 } ;
206214
207215 if ( iconNameMatch ) {
208- if ( ! ionIcons . includes ( iconNameMatch [ 1 ] ) ) {
209- ionIcons . push ( iconNameMatch [ 1 ] ) ;
210- }
216+ ionIcons . add ( iconNameMatch [ 1 ] ) ;
211217 } else if ( boundNameAttribute . value . ast . type === "Conditional" ) {
212218 deepGetIconConditional ( boundNameAttribute . value . ast , ionIcons ) ;
213219 } else {
@@ -270,8 +276,8 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
270276 }
271277
272278 return {
273- ionicComponents,
274- ionIcons,
279+ ionicComponents : Array . from ( ionicComponents ) ,
280+ ionIcons : Array . from ( ionIcons ) ,
275281 skippedIconsHtml,
276282 hasRouterLinkWithHref,
277283 hasRouterLink,
0 commit comments