Skip to content

Commit

Permalink
refactor: split icons to packages (#255)
Browse files Browse the repository at this point in the history
* refactor: split icons to packages

This will reduce the bundle size as `icons.ts` non-gzipped weights
around 1.2MB and is always being imported when `frog/ui` package is
used.
We can improve the bundle size by splitting the icons into different
packages, having just one collection imported by default – lucide.

* chore: changesets

* nit: unnecessary biome change

* nit: lint

* fix: simplify typing to let the build pass

* chore: add `frog/ui/icons` proxy package with all icon sets

* nit: apply suggested changes

Co-authored-by: awkweb <[email protected]>

* Update many-schools-reflect.md

* refactor: gen icons

* fix: icon type inference

* chore: imports

---------

Co-authored-by: awkweb <[email protected]>
  • Loading branch information
dalechyn and tmm authored May 16, 2024
1 parent 1e5b2c6 commit 752ccab
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 45 deletions.
20 changes: 20 additions & 0 deletions .changeset/many-schools-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"frog": minor
---

**Breaking change** Frog UI `icon` property requires an icon map imported from the `'frog/ui/icons'` entrypoint. This also makes it easier for you to supply your own custom icons.

```diff
+ import { lucide } from 'frog/ui/icons'

export const system = createSystem({
- icons: 'lucide',
+ icons: lucide,
})
```

In addition, the following separate entrypoints were added for resource constrained environments.

- `frog/ui/icons/heroicons`
- `frog/ui/icons/lucide`
- `frog/ui/icons/radix-icons`
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ create-frog/templates
node_modules
site/dist
src/ui/.frog
src/ui/icons.ts
src/ui/icons/**
!src/ui/icons/package.json
tsconfig.*.tsbuildinfo
tsconfig.tsbuildinfo
44 changes: 34 additions & 10 deletions .scripts/gen-icons.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import path from 'node:path'
import { IconifyJSONIconsData } from '@iconify/types'
import { getIconData, iconToHTML, iconToSVG } from '@iconify/utils'
import { camelize, getIconData, iconToHTML, iconToSVG } from '@iconify/utils'
import { glob } from 'fast-glob'
import { ensureDir } from 'fs-extra'

console.log('Copying icons to package.')

Expand All @@ -16,31 +17,54 @@ const collections = (await glob('**/@iconify/json/json/*.json'))
})
.filter((collection) => collectionSet.has(collection.name))

const iconMap: Record<string, Record<string, string>> = {}
for (const collection of collectionSet) {
iconMap[collection] = {}
}

const iconCollectionExports: string[] = []
let count = 0
for (const collection of collections) {
const file = Bun.file(collection.path)
const json = (await file.json()) as IconifyJSONIconsData

const iconMap: Record<string, string> = {}
for (const key of Object.keys(json.icons)) {
const item = getIconData(json, key)
if (!item) throw new TypeError(`Invalid icon: ${key}`)
const svg = iconToSVG(item)
const text = iconToHTML(svg.body, svg.attributes)
iconMap[collection.name][key] = encodeURIComponent(text)
iconMap[key] = encodeURIComponent(text)
}

const iconsContent = `export const ${camelize(
collection.name,
)} = ${JSON.stringify(iconMap, null, 2)}`
const iconsDist = path.resolve(
import.meta.dirname,
`../src/ui/icons/${collection.name}`,
)
await ensureDir(iconsDist)
await Bun.write(`${iconsDist}/index.ts`, `${iconsContent}\n`)

const proxyPackageContent = JSON.stringify(
{
type: 'module',
types: `../../_lib/ui/icons/${collection.name}/index.d.ts`,
module: `../../_lib/ui/icons/${collection.name}/index.js`,
},
null,
2,
)
await Bun.write(`${iconsDist}/package.json`, proxyPackageContent)

iconCollectionExports.push(
`export { ${camelize(collection.name)} } from './${
collection.name
}/index.js'`,
)

console.log(collection.name)
count += 1
}

const iconsExport = `export const icons = ${JSON.stringify(iconMap, null, 2)}`
const dist = path.resolve(import.meta.dirname, '../src/ui')
await Bun.write(`${dist}/icons.ts`, `${iconsExport}\n`)
const dist = path.resolve(import.meta.dirname, '../src/ui/icons')
await Bun.write(`${dist}/index.ts`, `${iconCollectionExports.join('\n')}\n`)

console.log(
`Done. Copied ${count} ${count === 1 ? 'collection' : 'collections'}.`,
Expand Down
7 changes: 4 additions & 3 deletions playground/src/ui-system.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, Frog } from 'frog'
import { serveStatic } from 'frog/serve-static'
import { heroicons, lucide, radixIcons } from 'frog/ui/icons'

import {
Box,
Expand Down Expand Up @@ -186,19 +187,19 @@ export const app = new Frog({
<Icon color="green800" name="zap" size="64" />
<Icon
color="green800"
collection="lucide"
collection={lucide}
name="zap"
size="64"
/>
<Icon
color="green800"
collection="heroicons"
collection={heroicons}
name="bolt"
size="64"
/>
<Icon
color="green800"
collection="radix-icons"
collection={radixIcons}
name="lightning-bolt"
size="64"
/>
Expand Down
11 changes: 6 additions & 5 deletions site/pages/ui/Icon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,20 @@ function Example() {

### `collection`

Icon collection to use for resolving icons. Defaults to `'lucide'`.
Icon collection to use for resolving icons. Defaults to `lucide` imported from `'frog/ui/icons'`.

The following collections are available:
- [Heroicons](https://heroicons.com)
- [Lucide](https://lucide.dev)
- [Radix Icons](https://www.radix-ui.com/icons)
- [Heroicons - `frog/ui/icons/heroicons`](https://heroicons.com)
- [Lucide - `frog/ui/icons/lucide`](https://lucide.dev)
- [Radix Icons - `frog/ui/icons/radix-icons`](https://www.radix-ui.com/icons)

Collection is mapped to the [`icons` property](/ui/ui-system#icons) on the UI System Variables.

:::code-group
```tsx twoslash [Code]
/** @jsxImportSource frog/jsx */
import { createSystem } from 'frog/ui'
import { heroicons } from 'frog/ui/icons' // [!code focus]
const { Icon } = createSystem()

Expand All @@ -137,7 +138,7 @@ function Example() {
// ---cut---
<Icon
name="bolt"
collection="heroicons" // [!code focus]
collection={heroicons} // [!code focus]
// ^?
/>

Expand Down
7 changes: 4 additions & 3 deletions site/pages/ui/createSystem.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export const system = createSystem({
### icons
- **Type:** `'heroicons' | 'lucide' | 'radix-icons'`
- **Default:** `'lucide'`
- **Type:** `Record<string, string>`
- **Default:** lucide
Icon collection to use for resolving icons. The following collections are available:
Expand All @@ -170,9 +170,10 @@ Icon collection to use for resolving icons. The following collections are availa
```tsx twoslash
import { createSystem } from 'frog/ui'
import { lucide } from 'frog/ui/icons' // [!code focus]
// ---cut---
export const system = createSystem({
icons: 'lucide', // [!code focus]
icons: lucide, // [!code focus]
})
```
Expand Down
3 changes: 2 additions & 1 deletion site/pages/ui/ui-system.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,10 @@ The `icons` variable is used to set the icon collection for the [`<Icon>` compon
/** @jsxImportSource frog/jsx */
// ---cut---
import { createSystem } from 'frog/ui'
import { lucide } from 'frog/ui/icons'

const { Icon } = createSystem({
icons: 'lucide',
icons: lucide,
})

function Example() {
Expand Down
21 changes: 17 additions & 4 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@
"types": "./_lib/ui/index.d.ts",
"default": "./_lib/ui/index.js"
},
"./ui/icons": {
"types": "./_lib/ui/icons/index.d.ts",
"default": "./_lib/ui/icons/index.js"
},
"./ui/icons/heroicons": {
"types": "./_lib/ui/icons/heroicons/index.d.ts",
"default": "./_lib/ui/icons/heroicons/index.js"
},
"./ui/icons/lucide": {
"types": "./_lib/ui/icons/lucide/index.d.ts",
"default": "./_lib/ui/icons/lucide/index.js"
},
"./ui/icons/radix-icons": {
"types": "./_lib/ui/icons/radix-icons/index.d.ts",
"default": "./_lib/ui/icons/radix-icons/index.js"
},
"./vercel": {
"types": "./_lib/vercel/index.d.ts",
"default": "./_lib/vercel/index.js"
Expand Down Expand Up @@ -105,10 +121,7 @@
"license": "MIT",
"homepage": "https://frog.fm",
"repository": "wevm/frog",
"authors": [
"awkweb.eth",
"jxom.eth"
],
"authors": ["awkweb.eth", "jxom.eth"],
"funding": [
{
"type": "github",
Expand Down
31 changes: 31 additions & 0 deletions src/ui/Icon.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expectTypeOf, test } from 'vitest'

import { createSystem } from './createSystem.js'
import { heroicons, lucide } from './icons/index.js'

test('defaults', () => {
const { Icon } = createSystem()
type IconProps = Parameters<typeof Icon>[0]
expectTypeOf<IconProps['collection']>().toEqualTypeOf<
Record<string, string> | undefined
>()
expectTypeOf<IconProps['name']>().toEqualTypeOf<keyof typeof lucide>()
})

test('custom system collection', () => {
const { Icon } = createSystem({ icons: heroicons })
type IconProps = Parameters<typeof Icon>[0]
expectTypeOf<IconProps['collection']>().toEqualTypeOf<
Record<string, string> | undefined
>()
expectTypeOf<IconProps['name']>().toEqualTypeOf<keyof typeof heroicons>()
})

test('custom system collection', () => {
const { Icon, vars } = createSystem()
type IconProps = Parameters<typeof Icon<typeof vars, typeof heroicons>>[0]
expectTypeOf<IconProps['collection']>().toEqualTypeOf<
typeof heroicons | Record<string, string> | undefined
>()
expectTypeOf<IconProps['name']>().toEqualTypeOf<keyof typeof heroicons>()
})
17 changes: 8 additions & 9 deletions src/ui/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Box, type BoxProps, resolveColorToken } from './Box.js'
import { icons } from './icons.js'
import { lucide } from './icons/lucide/index.js'
import { type DefaultVars, type Vars, defaultVars } from './vars.js'

export type IconProps<
vars extends Vars = DefaultVars,
collection extends Vars['icons'] = DefaultVars['icons'],
collection extends Vars['icons'] = vars['icons'],
> = {
__context?: { vars?: Vars | undefined } | undefined
/**
Expand All @@ -22,13 +22,13 @@ export type IconProps<
/**
* Icon collection to use for resolving icons.
*
* @default 'lucide'
* @default lucide (from 'frog/ui/icons')
*/
collection?: collection | Vars['icons'] | undefined
/** Icon name in the current icon collection. */
name: keyof (typeof icons)[collection extends keyof typeof icons
? collection
: never]
name: Record<string, any> | undefined extends collection
? keyof vars['icons']
: keyof collection
/** Sets the size of the icon. */
size?: BoxProps<vars>['width']
}
Expand All @@ -39,14 +39,13 @@ export function Icon<
>(props: IconProps<vars, collection>) {
const {
__context,
collection = __context?.vars?.icons ?? 'lucide',
collection = __context?.vars?.icons ?? lucide,
mode = 'auto',
name,
size = '24',
} = props

const iconMap = icons[collection]
let text: string = iconMap[name as keyof typeof iconMap]
let text: string = collection[name as keyof typeof collection]
if (!text) throw new TypeError(`Invalid set: ${collection}`)

const resolvedMode = (() => {
Expand Down
14 changes: 14 additions & 0 deletions src/ui/createSystem.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { expectTypeOf, test } from 'vitest'

import { createSystem } from './createSystem.js'
import { heroicons, lucide } from './icons/index.js'

test('defaults', () => {
const { vars } = createSystem()
expectTypeOf(vars.icons).toEqualTypeOf<typeof lucide>()
})

test('custom', () => {
const { vars } = createSystem({ icons: heroicons })
expectTypeOf(vars.icons).toEqualTypeOf<typeof heroicons>()
})
19 changes: 13 additions & 6 deletions src/ui/createSystem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Assign } from '../types/utils.js'
import type { Assign, Pretty } from '../types/utils.js'
import { Box } from './Box.js'
import { Column, Columns } from './Columns.js'
import { Divider } from './Divider.js'
Expand Down Expand Up @@ -32,15 +32,22 @@ import { type DefaultVars, type Vars, defaultVars } from './vars.js'
* })
* ```
*/
export function createSystem<vars extends Vars = DefaultVars>(
export function createSystem<const vars extends Vars = DefaultVars>(

Check failure on line 35 in src/ui/createSystem.tsx

View workflow job for this annotation

GitHub Actions / Verify / Types

The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
vars?: vars | undefined,
) {
type Icons = unknown extends vars['icons']
? DefaultVars['icons']
: vars['icons']
type MergedVars = Pretty<
Omit<Assign<DefaultVars, vars>, 'icons'> & {
icons: Icons
}
>

const mergedVars = {
...defaultVars,
...vars,
}

type MergedVars = Assign<DefaultVars, vars>
} as MergedVars

function createComponent<
const component extends (...args: any[]) => JSX.Element,
Expand Down Expand Up @@ -149,7 +156,7 @@ export function createSystem<vars extends Vars = DefaultVars>(
*/
Icon: <
vars extends MergedVars,
collection extends Vars['icons'] = DefaultVars['icons'],
collection extends Vars['icons'] = MergedVars['icons'],
>(
props: IconProps<vars, collection>,
) => <Icon __context={{ vars: mergedVars }} {...props} />,
Expand Down
5 changes: 5 additions & 0 deletions src/ui/icons/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "module",
"types": "../../_lib/ui/icons/index.d.ts",
"module": "../../_lib/ui/icons/index.js"
}
Loading

0 comments on commit 752ccab

Please sign in to comment.