Skip to content

Commit 25c728d

Browse files
committed
feat: lazy load functional and distinctive icons
1 parent c29850d commit 25c728d

7 files changed

Lines changed: 148 additions & 82 deletions

File tree

scripts/build.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { globSvgPlugin } from './esbuild-plugin-glob-svg.js';
1010

1111
const banner = `/*! ${pkg.name} v${pkg.version} | ${pkg.homepage} */`;
1212

13-
// Build ESM version
13+
// Build ESM version with code splitting for lazy-loaded icon sets
1414
await esbuild.build({
1515
entryPoints: ['src/index.ts'],
16-
outfile: 'dist/esm/index.js',
16+
outdir: 'dist/esm',
1717
banner: { js: banner },
1818
bundle: true,
19+
splitting: true,
1920
minify: true,
2021
write: true,
2122
format: 'esm',

scripts/esbuild-plugin-glob-svg.js

Lines changed: 59 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFileSync, readdirSync } from 'fs';
2-
import { resolve as pathResolve, dirname } from 'path';
2+
import { resolve as pathResolve, dirname, basename } from 'path';
33

44
/**
55
* ESBuild plugin to handle import.meta.glob for SVG files and ?raw imports
@@ -10,10 +10,8 @@ export const globSvgPlugin = () => ({
1010
setup(build) {
1111
// Handle ?raw imports for SVG files
1212
build.onResolve({ filter: /\.svg\?raw$/ }, (args) => {
13-
console.log('Resolving SVG raw import:', args.path);
1413
const pathWithoutQuery = args.path.replace('?raw', '');
1514
const resolvedPath = pathResolve(args.resolveDir, pathWithoutQuery);
16-
console.log('Resolved path:', resolvedPath);
1715
return {
1816
path: resolvedPath,
1917
namespace: 'svg-raw',
@@ -22,10 +20,8 @@ export const globSvgPlugin = () => ({
2220

2321
// Load SVG files as raw strings
2422
build.onLoad({ filter: /.*/, namespace: 'svg-raw' }, (args) => {
25-
console.log('Loading SVG raw file:', args.path);
2623
try {
2724
const content = readFileSync(args.path, 'utf8');
28-
console.log('Successfully loaded SVG content, length:', content.length);
2925
return {
3026
contents: `export default ${JSON.stringify(content)};`,
3127
loader: 'js',
@@ -39,83 +35,85 @@ export const globSvgPlugin = () => ({
3935
}
4036
});
4137

42-
// Handle the icons.ts file specifically
38+
// Helper function to read SVG files from a directory
39+
const readSvgFiles = (dirPath) => {
40+
try {
41+
const files = readdirSync(dirPath).filter((f) => f.endsWith('.svg'));
42+
return files.reduce((acc, file) => {
43+
const key = file.replace('.svg', '');
44+
const content = readFileSync(pathResolve(dirPath, file), 'utf8');
45+
acc[key] = content;
46+
return acc;
47+
}, {});
48+
} catch (err) {
49+
console.warn(`Could not read directory: ${dirPath}`);
50+
return {};
51+
}
52+
};
53+
54+
// Handle the icons.ts file - only includes standard icons
55+
// (functional/distinctive are lazy-loaded via separate chunks)
4356
build.onLoad({ filter: /icons\.ts$/ }, async (args) => {
4457
try {
45-
// Read the original file
4658
const source = readFileSync(args.path, 'utf8');
4759

48-
// Check if this file contains import.meta.glob
4960
if (!source.includes('import.meta.glob')) {
50-
return null; // Let esbuild handle it normally
61+
return null;
62+
}
63+
64+
const fileName = basename(args.path);
65+
66+
// Handle functional-icons.ts
67+
if (fileName === 'functional-icons.ts') {
68+
const functionalDir = pathResolve(dirname(args.path), 'functional');
69+
const functionalObj = readSvgFiles(functionalDir);
70+
71+
return {
72+
contents: `export const functionalIcons = ${JSON.stringify(functionalObj)};`,
73+
loader: 'ts',
74+
resolveDir: dirname(args.path),
75+
};
5176
}
5277

53-
// Define the icon directories relative to the icons.ts file
78+
// Handle distinctive-icons.ts
79+
if (fileName === 'distinctive-icons.ts') {
80+
const distinctiveDir = pathResolve(dirname(args.path), 'distinctive');
81+
const distinctiveObj = readSvgFiles(distinctiveDir);
82+
83+
return {
84+
contents: `export const distinctiveIcons = ${JSON.stringify(distinctiveObj)};`,
85+
loader: 'ts',
86+
resolveDir: dirname(args.path),
87+
};
88+
}
89+
90+
// Handle main icons.ts - standard icons only, with keys/types for all sets
5491
const iconsDir = pathResolve(dirname(args.path), 'icons');
5592
const functionalDir = pathResolve(dirname(args.path), 'functional');
5693
const distinctiveDir = pathResolve(dirname(args.path), 'distinctive');
5794

58-
// Helper function to read SVG files from a directory
59-
const readSvgFiles = (dirPath) => {
60-
try {
61-
const files = readdirSync(dirPath).filter((f) =>
62-
f.endsWith('.svg'),
63-
);
64-
return files.reduce((acc, file) => {
65-
const key = file.replace('.svg', '');
66-
const content = readFileSync(pathResolve(dirPath, file), 'utf8');
67-
acc[key] = content;
68-
return acc;
69-
}, {});
70-
} catch (err) {
71-
console.warn(`Could not read directory: ${dirPath}`);
72-
return {};
73-
}
74-
};
75-
76-
// Read all SVG files
7795
const iconsObj = readSvgFiles(iconsDir);
78-
const functionalObj = readSvgFiles(functionalDir);
79-
const distinctiveObj = readSvgFiles(distinctiveDir);
80-
81-
// Generate the replacement code
82-
const replacementCode = `/** eslint-disable spaced-comment */
83-
// This file is generated by esbuild-plugin-glob-svg for production builds
84-
// The original file uses Vite's import.meta.glob for development
85-
86-
const formatIcons = (imports) =>
87-
Object.entries(imports).reduce<Record<string, string>>(
88-
(acc, [path, content]) => {
89-
const key = path
90-
.replace(/\\.\\/\\(icons|functional|distinctive\\)\\//, '')
91-
.replace('.svg', '');
92-
return { ...acc, [key]: content as string };
93-
},
94-
{},
95-
);
96-
97-
export const icons = ${JSON.stringify(iconsObj, null, 2)};
98-
export const functionalIcons = ${JSON.stringify(functionalObj, null, 2)};
99-
export const distinctiveIcons = ${JSON.stringify(distinctiveObj, null, 2)};
96+
const functionalKeys = Object.keys(readSvgFiles(functionalDir));
97+
const distinctiveKeys = Object.keys(readSvgFiles(distinctiveDir));
98+
99+
const replacementCode = `// Generated by esbuild-plugin-glob-svg for production builds
100+
101+
export const icons = ${JSON.stringify(iconsObj)};
100102
101103
export const iconKeys = ${JSON.stringify(Object.keys(iconsObj))} as (keyof typeof icons)[];
102-
export const functionalIconKeys = ${JSON.stringify(Object.keys(functionalObj))} as (keyof typeof functionalIcons)[];
103-
export const distinctiveIconKeys = ${JSON.stringify(Object.keys(distinctiveObj))} as (keyof typeof distinctiveIcons)[];
104+
export const functionalIconKeys = ${JSON.stringify(functionalKeys)} as string[];
105+
export const distinctiveIconKeys = ${JSON.stringify(distinctiveKeys)} as string[];
104106
105107
export type IconName = ${
106108
Object.keys(iconsObj)
107109
.map((k) => `"${k}"`)
108110
.join(' | ') || 'string'
109111
};
110112
export type FunctionalIconName = ${
111-
Object.keys(functionalObj)
112-
.map((k) => `"${k}"`)
113-
.join(' | ') || 'string'
113+
functionalKeys.map((k) => `"${k}"`).join(' | ') || 'string'
114114
};
115115
export type DistinctiveIconName = ${
116-
Object.keys(distinctiveObj)
117-
.map((k) => `"${k}"`)
118-
.join(' | ') || 'string'
116+
distinctiveKeys.map((k) => `"${k}"`).join(' | ') || 'string'
119117
};
120118
121119
export default icons;
@@ -128,10 +126,10 @@ export default icons;
128126
};
129127
} catch (err) {
130128
console.warn(
131-
'Could not process icons.ts with glob-svg plugin:',
129+
'Could not process icons file with glob-svg plugin:',
132130
err.message,
133131
);
134-
return null; // Let esbuild handle it normally
132+
return null;
135133
}
136134
});
137135
},

scripts/export-components.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ function findTsFiles(dir, extensions = ['.ts', '.tsx']) {
2929
entry.isFile() &&
3030
extensions.some((ext) => entry.name.endsWith(ext))
3131
) {
32-
// Skip story files, test files, and spec files
32+
// Skip story files, test files, spec files, and lazy-loaded icon data files
3333
if (
3434
!entry.name.includes('.stories.') &&
3535
!entry.name.includes('.test.') &&
36-
!entry.name.includes('.spec.')
36+
!entry.name.includes('.spec.') &&
37+
!/^(functional|distinctive)-icons\.ts$/.test(entry.name)
3738
) {
3839
files.push(fullPath);
3940
}

src/components/content/Icon/Icon.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {
99
IconName,
1010
FunctionalIconName,
1111
DistinctiveIconName,
12-
functionalIcons,
13-
distinctiveIcons,
1412
} from './icons';
1513

1614
export interface IconAttributes {
@@ -61,20 +59,56 @@ export class Icon extends LitElement {
6159
}
6260
`;
6361

62+
// Static cache shared across all Icon instances
63+
private static _functionalIcons?: Record<string, string>;
64+
private static _distinctiveIcons?: Record<string, string>;
65+
private static _functionalPromise?: Promise<Record<string, string>>;
66+
private static _distinctivePromise?: Promise<Record<string, string>>;
67+
6468
constructor() {
6569
super();
6670
}
6771

68-
private get iconSet() {
72+
private get iconSet(): Record<string, string> {
6973
if (this.set === 'functional') {
70-
return functionalIcons;
74+
return Icon._functionalIcons ?? {};
7175
}
7276
if (this.set === 'distinctive') {
73-
return distinctiveIcons;
77+
return Icon._distinctiveIcons ?? {};
7478
}
7579
return icons;
7680
}
7781

82+
private async loadIconSet() {
83+
if (this.set === 'functional' && !Icon._functionalIcons) {
84+
if (!Icon._functionalPromise) {
85+
Icon._functionalPromise = import('./functional-icons').then(
86+
(m) => m.functionalIcons,
87+
);
88+
}
89+
Icon._functionalIcons = await Icon._functionalPromise;
90+
this.requestUpdate();
91+
} else if (this.set === 'distinctive' && !Icon._distinctiveIcons) {
92+
if (!Icon._distinctivePromise) {
93+
Icon._distinctivePromise = import('./distinctive-icons').then(
94+
(m) => m.distinctiveIcons,
95+
);
96+
}
97+
Icon._distinctiveIcons = await Icon._distinctivePromise;
98+
this.requestUpdate();
99+
}
100+
}
101+
102+
updated(changedProperties: Map<string, unknown>) {
103+
super.updated(changedProperties);
104+
if (
105+
(changedProperties.has('set') || changedProperties.has('icon')) &&
106+
(this.set === 'functional' || this.set === 'distinctive')
107+
) {
108+
this.loadIconSet();
109+
}
110+
}
111+
78112
render() {
79113
const { label, icon } = this;
80114

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Using Vite's import.meta.glob to import SVG files
2+
// eslint-disable-next-line spaced-comment
3+
/// <reference types="vite/client" />
4+
5+
const formatIcons = (imports) =>
6+
Object.entries(imports).reduce<Record<string, string>>(
7+
(acc, [path, content]) => {
8+
const key = path.replace(/\.\/distinctive\//, '').replace('.svg', '');
9+
return { ...acc, [key]: content as string };
10+
},
11+
{},
12+
);
13+
14+
const distinctiveSvgImport = import.meta.glob('./distinctive/*.svg', {
15+
eager: true,
16+
query: '?raw',
17+
import: 'default',
18+
});
19+
20+
export const distinctiveIcons = formatIcons(distinctiveSvgImport);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Using Vite's import.meta.glob to import SVG files
2+
// eslint-disable-next-line spaced-comment
3+
/// <reference types="vite/client" />
4+
5+
const formatIcons = (imports) =>
6+
Object.entries(imports).reduce<Record<string, string>>(
7+
(acc, [path, content]) => {
8+
const key = path.replace(/\.\/functional\//, '').replace('.svg', '');
9+
return { ...acc, [key]: content as string };
10+
},
11+
{},
12+
);
13+
14+
const functionalSvgImport = import.meta.glob('./functional/*.svg', {
15+
eager: true,
16+
query: '?raw',
17+
import: 'default',
18+
});
19+
20+
export const functionalIcons = formatIcons(functionalSvgImport);

src/components/content/Icon/icons.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// eslint-disable-next-line spaced-comment
33
/// <reference types="vite/client" />
44

5+
import { distinctiveIcons } from './distinctive-icons';
6+
import { functionalIcons } from './functional-icons';
7+
58
const formatIcons = (imports) =>
69
Object.entries(imports).reduce<Record<string, string>>(
710
(acc, [path, content]) => {
@@ -18,20 +21,9 @@ const svgImports = import.meta.glob('./icons/*.svg', {
1821
query: '?raw',
1922
import: 'default',
2023
});
21-
const functionalSvgImport = import.meta.glob('./functional/*.svg', {
22-
eager: true,
23-
query: '?raw',
24-
import: 'default',
25-
});
26-
const distinctiveSvgImport = import.meta.glob('./distinctive/*.svg', {
27-
eager: true,
28-
query: '?raw',
29-
import: 'default',
30-
});
3124

3225
export const icons = formatIcons(svgImports);
33-
export const functionalIcons = formatIcons(functionalSvgImport);
34-
export const distinctiveIcons = formatIcons(distinctiveSvgImport);
26+
export { functionalIcons, distinctiveIcons };
3527

3628
export const iconKeys = Object.keys(icons) as (keyof typeof icons)[];
3729
export const functionalIconKeys = Object.keys(

0 commit comments

Comments
 (0)