Skip to content

Commit

Permalink
feat: icons (#10202)
Browse files Browse the repository at this point in the history
* feat: icons

* chore: improve test

* feat: base build implemention

* chore: comments

* feat: collect icons with build

* feat: support generate svgr from collection and icon name

* feat: complete the basic version, fa iconset builtin only

* fix: don't generate bundle output, it's conflicted with mfsu eager mode
  • Loading branch information
sorrycc authored Jan 5, 2023
1 parent 3a89a62 commit 88efefc
Show file tree
Hide file tree
Showing 18 changed files with 681 additions and 48 deletions.
1 change: 1 addition & 0 deletions packages/preset-umi/fixtures/icons/normal/@/alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const alias = 'alias-with-@';
1 change: 1 addition & 0 deletions packages/preset-umi/fixtures/icons/normal/alias-3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const alias3 = 'alias-3';
1 change: 1 addition & 0 deletions packages/preset-umi/fixtures/icons/normal/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = 'foo';
33 changes: 33 additions & 0 deletions packages/preset-umi/fixtures/icons/normal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { foo } from './foo';
// @ts-ignore
import { jsx } from './jsx';
// @ts-ignore
import { bar } from 'bar';
import './a.less';
import './a.less?global';
import './a.jpg';
import './a.png';
import './a.sass';
import './a.css';
import './a.json';
// @ts-ignore
import json from 'ggg/a.json';
// @ts-ignore
import antd from '@alipay/bigfish/antd';
// @ts-ignore
import { alias } from '@/alias';
import 'alias-1';
// @ts-ignore
import { alias3, Icon } from 'alias-3';

console.log(1, foo, jsx, bar, json, antd, alias, alias3);
export function App() {
return (
<>
<Icon name="xxx" />
<Icon name="xxx2" />
<Icon name="xxx222" />
</>
);
}
2 changes: 2 additions & 0 deletions packages/preset-umi/fixtures/icons/normal/jsx.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

export const jsx = 'jsx';
3 changes: 3 additions & 0 deletions packages/preset-umi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"test": "umi-scripts jest-turbo"
},
"dependencies": {
"@iconify-json/fa": "1",
"@iconify/utils": "2.0.9",
"@svgr/core": "6.2.1",
"@umijs/ast": "4.0.0-canary.20221230.1",
"@umijs/babel-preset-umi": "4.0.0-canary.20221230.1",
"@umijs/bundler-utils": "4.0.0-canary.20221230.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-umi/src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default (api: IApi) => {
key: 'onGenerateFiles',
args: {
files: null,
isFirstTime: false,
isFirstTime: true,
},
});
},
Expand Down
79 changes: 79 additions & 0 deletions packages/preset-umi/src/features/icons/buildForIconExtract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import esbuild from '@umijs/bundler-utils/compiled/esbuild';
import { esbuildExternalPlugin } from './esbuildExternalPlugin';
import path from 'path';
import { esbuildAliasPlugin } from './esbuildAliasPlugin';
import { esbuildCollectIconPlugin } from './esbuildCollectIconPlugin';
import { logger } from '@umijs/utils';

export async function buildForIconExtract(opts: {
entryPoints: string[];
watch?:
| {
onRebuildSuccess(): void;
}
| false;
config?: { alias?: any };
}) {
const icons: Set<string> = new Set();
await esbuild.build({
format: 'esm',
platform: 'browser',
target: 'esnext',
loader: {
'.js': 'jsx',
'.jsx': 'jsx',
'.ts': 'ts',
'.tsx': 'tsx',
},
watch: !!opts.watch && {
onRebuild(err) {
if (err) {
logger.error(`[icons] build failed: ${err}`);
} else {
if (opts.watch) {
opts.watch.onRebuildSuccess();
}
}
},
},
// do I need this?
// incremental: true,
bundle: true,
logLevel: 'error',
entryPoints: opts.entryPoints,
write: false,
outdir: path.join(path.dirname(opts.entryPoints[0]), 'out'),
plugins: [
esbuildAliasPlugin({ alias: opts.config?.alias || {} }),
esbuildExternalPlugin(),
esbuildCollectIconPlugin({
icons,
}),
],
});
return icons;
}

// const baseDir = path.join(__dirname, '../../../fixtures/icons/normal');
// buildForIconExtract({
// entryPoints: [path.join(baseDir, 'index.tsx')],
// config: {
// alias: {
// '@': path.join(baseDir, '@'),
// 'alias-1': 'alias-2',
// 'alias-3$': path.join(baseDir, 'alias-3.ts'),
// },
// },
// watch: {
// onRebuildSuccess(icons) {
// console.log('icons', icons);
// },
// },
// })
// .then((icons) => {
// console.log('done');
// console.log(icons);
// })
// .catch((e) => {
// console.error(e);
// });
83 changes: 83 additions & 0 deletions packages/preset-umi/src/features/icons/esbuildAliasPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Plugin } from '@umijs/bundler-utils/compiled/esbuild';
import { existsSync, statSync } from 'fs';
import enhancedResolve from 'enhanced-resolve';

const resolver = enhancedResolve.create({
mainFields: ['module', 'browser', 'main'],
extensions: ['.json', '.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs'],
// TODO: support exports
exportsFields: [],
});

async function resolve(context: string, path: string): Promise<string> {
return new Promise((resolve, reject) => {
resolver(context, path, (err: Error, result: string) =>
err ? reject(err) : resolve(result),
);
});
}

function sortByAffix(opts: { keys: string[]; affix: string }) {
return opts.keys.sort((a, b) => {
if (a.endsWith(opts.affix) && b.endsWith(opts.affix)) return 0;
if (a.endsWith(opts.affix)) return -1;
if (b.endsWith(opts.affix)) return 1;
else return 0;
});
}

function addSlashAffix(key: string) {
return key.endsWith('/') ? key : `${key}/`;
}

export function esbuildAliasPlugin(opts: {
alias: Record<string, string>;
}): Plugin {
return {
name: 'esbuildExternalPlugin',
setup(build) {
// only absolute alias should be resolved
// node deps alias should be filtered, and mark as externals with other plugins
sortByAffix({ keys: Object.keys(opts.alias), affix: '$' })
.filter((key) => {
return (
opts.alias[key].startsWith('/') &&
!opts.alias[key].includes('node_modules')
);
})
.forEach((key) => {
const value = opts.alias[key];

const filter = key.endsWith('$')
? new RegExp(key)
: new RegExp(`${key}$`);
build.onResolve({ filter }, async (args) => {
const path = await resolve(
args.importer,
args.path.replace(filter, value),
);
return {
path,
};
});

if (
!key.endsWith('/') &&
existsSync(value) &&
statSync(value).isDirectory()
) {
const filter = new RegExp(`^${addSlashAffix(key)}`);
build.onResolve({ filter }, async (args) => {
const path = await resolve(
args.importer,
args.path.replace(filter, addSlashAffix(value)),
);
return {
path,
};
});
}
});
},
};
}
27 changes: 27 additions & 0 deletions packages/preset-umi/src/features/icons/esbuildCollectIconPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Plugin, Loader } from '@umijs/bundler-utils/compiled/esbuild';
import fs from 'fs';
import { extractIcons } from './extractIcons';

export function esbuildCollectIconPlugin(opts: { icons: Set<string> }): Plugin {
return {
name: 'esbuildCollectIconPlugin',
setup(build) {
const loaders: Loader[] = ['js', 'jsx', 'ts', 'tsx'];
loaders.forEach((loader) => {
const filter = new RegExp(`\\.(${loader})$`);
build.onLoad({ filter }, async (args) => {
const contents = fs.readFileSync(args.path, 'utf-8');
extractIcons(contents).forEach((icon) => {
// just add
// don't handle delete for dev
opts.icons.add(icon);
});
return {
contents,
loader,
};
});
});
},
};
}
63 changes: 63 additions & 0 deletions packages/preset-umi/src/features/icons/esbuildExternalPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Plugin } from '@umijs/bundler-utils/compiled/esbuild';

export function esbuildExternalPlugin(): Plugin {
return {
name: 'esbuildExternalPlugin',
setup(build) {
// externals extensions
externalsExtensions.forEach((ext) => {
// /\.abc?query$/
const filter = new RegExp(`\.${ext}(\\?.*)?$`);
build.onResolve({ filter }, () => {
return {
external: true,
};
});
});
// external deps
build.onResolve({ filter: /.*/ }, (args) => {
if (args.path.startsWith('.')) {
return null;
}
if (args.kind === 'entry-point') {
return null;
}
if (args.path.startsWith('/') && !args.path.includes('node_modules')) {
return null;
}
return {
external: true,
};
});
},
};
}

const externalsExtensions = [
'aac',
'css',
'less',
'sass',
'scss',
'eot',
'flac',
'gif',
'ico',
'jpeg',
'jpg',
'json',
'md',
'mdx',
'mp3',
'mp4',
'ogg',
'otf',
'png',
'svg',
'ttf',
'wav',
'webm',
'webp',
'woff',
'woff2',
];
23 changes: 23 additions & 0 deletions packages/preset-umi/src/features/icons/extractIcons.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { extractIcons } from './extractIcons';

test('normal', () => {
expect(extractIcons(`<Icon icon="foo" />`)).toEqual(['foo']);
});

test('with chaos', () => {
expect(extractIcons(`<Icon icon="foo" />`)).toEqual(['foo']);
expect(extractIcons(`<Icon icon='foo' />`)).toEqual(['foo']);
expect(extractIcons(`<Icon bar='bar' icon="foo" hoo />`)).toEqual(['foo']);
expect(extractIcons(`<Icon icon="foo bar" />`)).toEqual(['foo bar']);
});

test('multiple', () => {
expect(extractIcons(`<Icon icon="foo" /><Icon icon="bar" />`)).toEqual([
'foo',
'bar',
]);
});

test('only the first icon attribute is valid', () => {
expect(extractIcons(`<Icon icon="foo" icon="bar" />`)).toEqual(['foo']);
});
Loading

0 comments on commit 88efefc

Please sign in to comment.