Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
007d610
perf: add sideEffects:false, exports field with ESM/CJS conditions an…
mrkpatchaa Apr 15, 2026
42bcb0f
test: add RED tests for icon-base memo and lib/module package.json
mrkpatchaa Apr 15, 2026
6d66c01
perf: add postbuild script to emit lib/module/package.json with type:…
mrkpatchaa Apr 15, 2026
a599de4
perf: wrap IconBase in React.memo to prevent unnecessary re-renders
mrkpatchaa Apr 15, 2026
4658393
perf: add per-weight subpath builds with single-weight IconBase
mrkpatchaa Apr 15, 2026
a2e0165
feat(example): update perf demo tab with per-weight and memo scenarios
mrkpatchaa Apr 15, 2026
6d0a2cc
fix: yarn.lock
mrkpatchaa Apr 15, 2026
3845a1e
feat: add bundle-size script to measure tree-shaken output per import…
mrkpatchaa Apr 21, 2026
c18a763
feat: add bundle-bench script — Metro bundle size via real Expo project
mrkpatchaa Apr 21, 2026
3a5244b
fix: remove unused React import in test; use npm pack --ignore-script…
mrkpatchaa Apr 21, 2026
03955bd
chore: remove bundle-size.mjs — superseded by bundle-bench
mrkpatchaa Apr 21, 2026
7ca43f2
fix: per-weight index.tsx exports from ./lib (not ../lib) and drops I…
mrkpatchaa Apr 21, 2026
c8f64c6
chore: update .npmignore and tsconfig.build.json to exclude additiona…
mrkpatchaa Apr 21, 2026
4c567a0
fix: duotone icon template — cast paths via unknown to avoid TS2349 o…
mrkpatchaa Apr 21, 2026
c1fa1a5
feat(bundle-bench): add src deep-import scenario and --no-pack flag
mrkpatchaa Apr 21, 2026
bee4aa9
feat: subpath deep-import scenario + wildcard exports for icons/*
mrkpatchaa Apr 21, 2026
b1b49ed
fix(bundle-bench): polyfill Metro wildcard exports; always rewrite me…
mrkpatchaa Apr 21, 2026
9b028f2
fix(bundle-bench): silence src/icons/* exports warning in Metro resolver
mrkpatchaa Apr 21, 2026
0c67a89
feat(bundle-bench): add --tree-shake flag for Expo experimental tree …
mrkpatchaa Apr 21, 2026
c2b0e90
feat(bundle-bench): redesign report with groups, bar chart, and overh…
mrkpatchaa Apr 21, 2026
7e1c785
fix(bundle-bench): dynamic label width so number columns always align
mrkpatchaa Apr 21, 2026
26d008f
style(bundle-bench): format code for improved readability
mrkpatchaa Apr 21, 2026
7e42fc0
fix: nest types under import/require conditions in exports map
mrkpatchaa Apr 21, 2026
b88355c
docs: document subpath and deep-subpath import patterns
mrkpatchaa Apr 21, 2026
e88dd0e
refactor: remove unused build-weights script and clean up package.json
mrkpatchaa Apr 21, 2026
26837eb
refactor(tests): improve readability and structure of test cases
mrkpatchaa Apr 21, 2026
5e389fa
refactor: remove unnecessary global process declaration
mrkpatchaa Apr 21, 2026
6d4b606
fix: refactor code structure for improved readability and maintainabi…
mrkpatchaa Apr 21, 2026
30b782d
chore: update example app
mrkpatchaa Apr 21, 2026
fdb0bb6
refactor: update icon color initialization and improve color handling…
mrkpatchaa Apr 21, 2026
3f0f652
refactor: remove console log from HomeScreen for cleaner output
mrkpatchaa Apr 21, 2026
85693aa
refactor: improve icon color generation logic for better randomness
mrkpatchaa Apr 21, 2026
1ea2f4b
3.1.0
mrkpatchaa Apr 23, 2026
13020ea
refactor: remove per-weight subpath exports and update related docume…
mrkpatchaa Apr 23, 2026
6ed5bd0
refactor: remove IconBaseSingle component to streamline icon handling
mrkpatchaa Apr 23, 2026
b28b238
docs: update README with improved icon import instructions and examples
mrkpatchaa Apr 23, 2026
706feb5
fix: upgrade deps
mrkpatchaa Apr 23, 2026
558b197
fix: update IconProps style type to exclude cursor from TextStyle
mrkpatchaa Apr 23, 2026
3740be1
3.0.6
mrkpatchaa Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ src/*

# generated by bob
lib/

# bundle-bench tool
bundle-bench/
phosphor-react-native-*.tgz
7 changes: 7 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/src
lib/**/*.map
/example
/bundle-bench
/.vscode
/__tests__
/__mocks__
/.circleci
/.github
/.cursor
6 changes: 6 additions & 0 deletions __mocks__/react-native-svg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const React = require('react');
module.exports = {
__esModule: true,
default: (props) => React.createElement('Svg', props),
Path: (props) => React.createElement('Path', props),
};
10 changes: 10 additions & 0 deletions __mocks__/react-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const React = require('react');
const createContext = React.createContext;
const useContext = React.useContext;
module.exports = {
createContext,
useContext,
StyleSheet: { create: (s) => s },
View: (props) => React.createElement('View', props),
Text: (props) => React.createElement('Text', props),
};
16 changes: 16 additions & 0 deletions __tests__/icon-base-memo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
describe('icon-base memo', () => {
it('IconBase export is wrapped in React.memo', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require('../src/lib/icon-base');
const IconBase = mod.default;
// React.memo returns an object with $$typeof === Symbol.for('react.memo')
expect(IconBase.$$typeof).toBe(Symbol.for('react.memo'));
});

it('React.memo wrapped component renders without throwing', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { default: IconBase } = require('../src/lib/icon-base');
// The inner type should be a function (the actual component)
expect(typeof IconBase.type).toBe('function');
});
});
11 changes: 11 additions & 0 deletions __tests__/lib-module-package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as path from 'path';
import * as fs from 'fs';

describe('lib/module/package.json', () => {
it('declares type: module so .js files in module output are treated as ESM', () => {
const pkgPath = path.resolve(__dirname, '../lib/module/package.json');
expect(fs.existsSync(pkgPath)).toBe(true);
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
expect(pkg.type).toBe('module');
});
});
12 changes: 12 additions & 0 deletions __tests__/lib-weight-subpaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as path from 'path';
import * as fs from 'fs';

const WEIGHTS = ['regular', 'bold', 'thin', 'light', 'fill', 'duotone'];
const root = path.resolve(__dirname, '..');

describe('per-weight subpath outputs', () => {
it.each(WEIGHTS)('src/%s is not generated anymore', (weight) => {
const filePath = path.join(root, 'src', weight);
expect(fs.existsSync(filePath)).toBe(false);
});
});
39 changes: 39 additions & 0 deletions __tests__/package-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pkg from '../package.json';

describe('package.json metadata', () => {
it('has sideEffects: false for tree shaking', () => {
expect((pkg as Record<string, unknown>).sideEffects).toBe(false);
});

it('has an exports field for modern bundler resolution', () => {
const exports = (pkg as Record<string, unknown>).exports as
| Record<string, unknown>
| undefined;
expect(exports).toBeDefined();
expect(exports!['.']).toBeDefined();
});

it('exports field has import (ESM) and require (CJS) conditions', () => {
const exports = (pkg as Record<string, unknown>).exports as Record<
string,
Record<string, Record<string, string>>
>;
const main = exports['.'];
expect(main.import).toBeDefined();
expect(main.require).toBeDefined();
expect(main.import.types).toBeDefined();
expect(main.require.types).toBeDefined();
});

it('does not expose weight-specific subpath exports', () => {
const exports = (pkg as Record<string, unknown>).exports as Record<
string,
unknown
>;
const weights = ['regular', 'bold', 'thin', 'light', 'fill', 'duotone'];
for (const weight of weights) {
expect(exports[`./${weight}`]).toBeUndefined();
expect(exports[`./${weight}/icons/*`]).toBeUndefined();
}
});
});
1 change: 0 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line no-undef
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};
22 changes: 14 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@ import tseslint from 'typescript-eslint';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';

export default [
{ files: ['**/*.{js,mjs,cjs,ts}'] },
{ languageOptions: { globals: globals.browser } },
{ files: ['**/*.{js,mjs,cjs,ts,tsx}'] },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: [
'node_modules/',
'lib/',
'src/bold/',
'src/duotone/',
'src/fill/',
'bundle-bench/',
'src/icons/',
'src/light/',
'src/regular/',
'src/thin/',
'src/index.tsx',
'example/',
'core/',
],
},
// CommonJS files: allow require() and module.exports
{
files: ['**/*.cjs', 'jest.config.js', '__mocks__/**/*.js'],
languageOptions: {
sourceType: 'commonjs',
globals: globals.commonjs,
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
eslintPluginPrettierRecommended,
];
10 changes: 5 additions & 5 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ node_modules/
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
Expand All @@ -34,8 +36,6 @@ yarn-error.*
# typescript
*.tsbuildinfo

# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli

expo-env.d.ts
# @end expo-cli
# generated native folders
/ios
/android
21 changes: 12 additions & 9 deletions example/app.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"expo": {
"name": "phosphor-react-native-example",
"slug": "phosphor-react-native-example",
"name": "example",
"slug": "example",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "phosphor-react-native-example",
"userInterfaceStyle": "light",
"scheme": "example",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
Expand All @@ -17,9 +17,12 @@
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false
},
"web": {
"bundler": "metro",
Expand All @@ -33,4 +36,4 @@
"typedRoutes": true
}
}
}
}
70 changes: 47 additions & 23 deletions example/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,68 @@
import { Tabs } from 'expo-router';
import React from 'react';
import { SymbolView } from 'expo-symbols';
import { Link, Tabs } from 'expo-router';
import { Platform, Pressable } from 'react-native';

import { ListIcon, TestTubeIcon } from '@/components/icons';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import Colors from '@/constants/Colors';
import { useColorScheme } from '@/components/useColorScheme';
import { useClientOnlyValue } from '@/components/useClientOnlyValue';

export default function TabLayout() {
const colorScheme = useColorScheme();

return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
}}
>
tabBarActiveTintColor: Colors[colorScheme].tint,
// Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, false),
}}>
<Tabs.Screen
name="index"
options={{
title: 'All icons',
tabBarIcon: ({ color, focused }) => (
<ListIcon weight={focused ? 'fill' : 'light'} color={color} />
title: 'Tab One',
tabBarIcon: ({ color }) => (
<SymbolView
name={{
ios: 'chevron.left.forwardslash.chevron.right',
android: 'code',
web: 'code',
}}
tintColor={color}
size={28}
/>
),
}}
/>
<Tabs.Screen
name="test-lab"
options={{
title: 'Test Lab',
tabBarIcon: ({ color, focused }) => (
<TestTubeIcon weight={focused ? 'fill' : 'light'} color={color} />
headerRight: () => (
<Link href="/modal" asChild>
<Pressable style={{ marginRight: 15 }}>
{({ pressed }) => (
<SymbolView
name={{ ios: 'info.circle', android: 'info', web: 'info' }}
size={25}
tintColor={Colors[colorScheme].text}
style={{ opacity: pressed ? 0.5 : 1 }}
/>
)}
</Pressable>
</Link>
),
}}
/>
<Tabs.Screen
name="single-imports"
name="two"
options={{
title: 'Single',
tabBarIcon: ({ color, focused }) => (
<TestTubeIcon weight={focused ? 'fill' : 'light'} color={color} />
title: 'Tab Two',
tabBarIcon: ({ color }) => (
<SymbolView
name={{
ios: 'chevron.left.forwardslash.chevron.right',
android: 'code',
web: 'code',
}}
tintColor={color}
size={28}
/>
),
}}
/>
Expand Down
1 change: 1 addition & 0 deletions example/app/(tabs)/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/screens/home';
32 changes: 31 additions & 1 deletion example/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
export { default } from '@/components/home';
import { StyleSheet } from 'react-native';

import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';

export default function TabOneScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Tab One</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/index.tsx" />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});
2 changes: 1 addition & 1 deletion example/app/(tabs)/single-imports.tsx
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default } from '@/components/single-imports';
export { default } from '@/screens/single-imports';
2 changes: 1 addition & 1 deletion example/app/(tabs)/test-lab.tsx
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default } from '@/components/test-lab';
export { default } from '@/screens/test-lab';
31 changes: 31 additions & 0 deletions example/app/(tabs)/two.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { StyleSheet } from 'react-native';

import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';

export default function TabTwoScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Tab Two</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/two.tsx" />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});
Loading
Loading