Skip to content

Commit 97a3f50

Browse files
authored
Add ESM bundle support (#1280)
* add ESM support * address copilot feedback * fix icons * fix linter * fix octicons externals * fix typing erroprs * fix cjs path for astro * upgrade astro to v6 and switch to ESM * update astro to support matrix strategy for cjs and esm * add stylesheets file to side effects * upgrade to vite 8 * update package-lock * fix build * update vite config * pin postcss * fix side-effects path
1 parent 74db366 commit 97a3f50

36 files changed

Lines changed: 6990 additions & 4632 deletions

.changeset/free-planets-fold.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@primer/react-brand': minor
3+
---
4+
5+
Added ESM library support.
6+
7+
A new `@primer/react-brand/esm` entry point is now available for modern bundlers (E.g. Vite, Next.js, etc).
8+
9+
Components imported from this path automatically include the minimum CSS needed. Separate global stylesheet imports are no longer required.
10+
11+
```js
12+
import {Hero, ThemeProvider} from '@primer/react-brand/esm'
13+
```
14+
15+
ESM features are opt-in, and the previous UMD bundle (`@primer/react-brand`) and global CSS (`@primer/react-brand/lib/css/main.css`) continue to work as before.
16+
17+
We recommend switching to ESM as soon as possible, as it will eventually become the default in future.
18+
19+
🔗 [Get started with ESM](https://primer.style/brand/getting-started/esm)

.github/workflows/bundle_size.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Check bundle size
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
size:
8+
if: ${{ github.repository == 'primer/brand' }}
9+
name: Report bundle size
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout PR branch
13+
uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
17+
- name: Create main branch worktree
18+
run: git worktree add /tmp/main ${{ github.event.pull_request.base.sha }}
19+
20+
- name: Set up Node
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: 24
24+
25+
- name: Caching dependencies
26+
uses: actions/cache@v4
27+
with:
28+
path: ~/.npm
29+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
30+
restore-keys: |
31+
${{ runner.os }}-node-
32+
33+
- name: Install and build (PR)
34+
run: |
35+
npm ci
36+
npm run build:lib
37+
38+
- name: Measure bundle size (PR)
39+
run: npx size-limit --json > /tmp/pr-sizes.json
40+
41+
- name: Install and build (main branch)
42+
working-directory: /tmp/main
43+
run: |
44+
npm ci
45+
cp ${{ github.workspace }}/.size-limit.js .size-limit.js
46+
npm run build:lib
47+
48+
- name: Measure bundle size (main branch)
49+
working-directory: /tmp/main
50+
run: npx size-limit --json > /tmp/main-sizes.json 2>/dev/null || echo "[]" > /tmp/main-sizes.json
51+
52+
- name: Generate size report
53+
id: size-report
54+
run: |
55+
REPORT=$(node packages/repo-configs/scripts/bundle-size-report.js /tmp/pr-sizes.json /tmp/main-sizes.json)
56+
echo "comment-body<<EOF" >> "$GITHUB_OUTPUT"
57+
echo "$REPORT" >> "$GITHUB_OUTPUT"
58+
echo "EOF" >> "$GITHUB_OUTPUT"
59+
60+
- name: Add or update bundle size comment
61+
uses: phulsechinmay/rewritable-pr-comment@v0.3.0
62+
with:
63+
message: ${{ steps.size-report.outputs.comment-body }}
64+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65+
COMMENT_IDENTIFIER: 'bundle-size-report'

.github/workflows/integration_test_astro.yml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ on:
88
- cron: '0 0 * * *' # end of each day (UTC)
99
pull_request:
1010

11-
env:
12-
ASTRO_FOLDER: astro-test
13-
1411
jobs:
1512
test:
1613
if: ${{ github.repository == 'primer/brand' }}
17-
name: Astro - React starter
14+
name: Astro (${{ matrix.bundle }})
1815
runs-on:
1916
labels: ubuntu-latest-16-cores
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
bundle: [umd, esm]
21+
include:
22+
- bundle: umd
23+
astro_folder: astro-test
24+
- bundle: esm
25+
astro_folder: astro-esm-test
26+
env:
27+
ASTRO_FOLDER: ${{ matrix.astro_folder }}
2028
steps:
2129
- name: Checkout repository
2230
uses: actions/checkout@v6
@@ -88,13 +96,14 @@ jobs:
8896
cp ../packages/react/primer-react-brand-${{ steps.package-version.outputs.current-version}}.tgz ./
8997
npm install primer-react-brand-${{ steps.package-version.outputs.current-version}}.tgz
9098
91-
- name: Prefer CommonJS imports
99+
- name: Prepare barrel file for bundle type
92100
run: |
93-
mv packages/e2e/integration-tests/fixtures/index.cjs.ts packages/e2e/integration-tests/fixtures/index.ts
94-
rm packages/e2e/integration-tests/fixtures/index.esm.ts
101+
cp ./packages/e2e/integration-tests/fixtures/index.${{ matrix.bundle }}.ts ./packages/e2e/integration-tests/fixtures/index.ts
102+
rm ./packages/e2e/integration-tests/fixtures/index.umd.ts ./packages/e2e/integration-tests/fixtures/index.esm.ts
95103
96104
- name: Copying required files
97105
run: |
106+
cp packages/e2e/integration-tests/astro/astro.config.mjs ./${{env.ASTRO_FOLDER}}/astro.config.mjs
98107
cp packages/e2e/integration-tests/astro/index.astro ./${{env.ASTRO_FOLDER}}/src/pages
99108
cp packages/e2e/cypress.config.js ./${{env.ASTRO_FOLDER}}/cypress.config.cjs
100109
cp -r packages/e2e/integration-tests/fixtures ./${{env.ASTRO_FOLDER}}/src

.github/workflows/integration_test_nextjs.yml

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Integration test
1+
name: Integration test (Next.js)
22

33
on:
44
push:
@@ -8,15 +8,23 @@ on:
88
- cron: '0 0 * * *' # end of each day (UTC)
99
pull_request:
1010

11-
env:
12-
NEXTJS_FOLDER: next-js-test
13-
1411
jobs:
1512
test:
1613
if: ${{ github.repository == 'primer/brand' }}
17-
name: Next.js
14+
name: Next.js (${{ matrix.bundle }})
1815
runs-on:
1916
labels: ubuntu-latest-16-cores
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
bundle: [umd, esm]
21+
include:
22+
- bundle: umd
23+
nextjs_folder: next-js-test
24+
- bundle: esm
25+
nextjs_folder: next-js-esm-test
26+
env:
27+
NEXTJS_FOLDER: ${{ matrix.nextjs_folder }}
2028
steps:
2129
- name: Checkout repository
2230
uses: actions/checkout@v6
@@ -34,10 +42,6 @@ jobs:
3442
restore-keys: |
3543
${{ runner.os }}-node-
3644
37-
# Restore the previous NPM modules and Cypress binary archives.
38-
# Any updated archives will be saved automatically after the entire
39-
# workflow successfully finishes.
40-
# See https://github.com/actions/cache
4145
- name: Cache Cypress binary
4246
uses: actions/cache@v5
4347
with:
@@ -51,9 +55,7 @@ jobs:
5155

5256
- name: install dependencies and verify Cypress
5357
env:
54-
# make sure every Cypress install prints minimal information
5558
CI: 1
56-
# print Cypress and OS info
5759
run: |
5860
npx cypress@15.13.0 install --force
5961
npx cypress@15.13.0 verify
@@ -81,45 +83,40 @@ jobs:
8183

8284
- name: Installing local build
8385
run: |
84-
cd ${{env.NEXTJS_FOLDER}}
85-
cp ../packages/react/primer-react-brand-${{ steps.package-version.outputs.current-version}}.tgz ./
86-
npm install primer-react-brand-${{ steps.package-version.outputs.current-version}}.tgz
86+
cd ${{ env.NEXTJS_FOLDER }}
87+
cp ../packages/react/primer-react-brand-${{ steps.package-version.outputs.current-version }}.tgz ./
88+
npm install primer-react-brand-${{ steps.package-version.outputs.current-version }}.tgz
8789
88-
- name: Prefer ESM imports
90+
- name: Prepare barrel file for bundle type
8991
run: |
90-
mv ./packages/e2e/integration-tests/fixtures/index.esm.ts ./packages/e2e/integration-tests/fixtures/index.ts
91-
rm ./packages/e2e/integration-tests/fixtures/index.cjs.ts
92+
cp ./packages/e2e/integration-tests/fixtures/index.${{ matrix.bundle }}.ts ./packages/e2e/integration-tests/fixtures/index.ts
93+
rm ./packages/e2e/integration-tests/fixtures/index.umd.ts ./packages/e2e/integration-tests/fixtures/index.esm.ts
9294
9395
- name: Copying required files
9496
run: |
95-
rm ./${{env.NEXTJS_FOLDER}}/app/page.tsx
96-
cp ./packages/e2e/integration-tests/nextjs/page.tsx ./${{env.NEXTJS_FOLDER}}/app
97-
98-
- name: Copying required files
99-
run: |
100-
cp ./packages/e2e/integration-tests/nextjs/page.tsx ./${{env.NEXTJS_FOLDER}}/app
101-
cp ./packages/e2e/cypress.config.js ./${{env.NEXTJS_FOLDER}}
102-
mkdir ${{env.NEXTJS_FOLDER}}/integration-tests
103-
cp -r ./packages/e2e/integration-tests/fixtures ./${{env.NEXTJS_FOLDER}}/integration-tests
104-
cp -r ./packages/e2e/integration-tests/tests ./${{env.NEXTJS_FOLDER}}/integration-tests
97+
rm ./${{ env.NEXTJS_FOLDER }}/app/page.tsx
98+
cp ./packages/e2e/integration-tests/nextjs/page.tsx ./${{ env.NEXTJS_FOLDER }}/app
99+
cp ./packages/e2e/cypress.config.js ./${{ env.NEXTJS_FOLDER }}
100+
mkdir ${{ env.NEXTJS_FOLDER }}/integration-tests
101+
cp -r ./packages/e2e/integration-tests/fixtures ./${{ env.NEXTJS_FOLDER }}/integration-tests
102+
cp -r ./packages/e2e/integration-tests/tests ./${{ env.NEXTJS_FOLDER }}/integration-tests
105103
106104
- name: Excluded cypress tests in-place
107105
# includes temp workaround for cypress bug. remove when fixed
108106
# https://github.com/cypress-io/cypress/issues/27448
109107
run: npx json@11.0.0 -I -f ${{env.NEXTJS_FOLDER}}/tsconfig.json -e 'this.exclude=["node_modules", "**/*.cy.ts"]; this.compilerOptions.moduleResolution="node"'
110108

111109
- name: Fix monorepo-related linting issues
112-
run: |
113-
npm run lint -- --fix
110+
run: npm run lint -- --fix
114111

115112
# Needed to fix compilation errors and conflicts between TS and React type defs.
116113
# All subsequent steps should not be dependent on the primer/brand repo contents.
117114
- name: Clean workspace before testing
118115
run: |
119-
find . -maxdepth 1 -mindepth 1 -not -name ${{env.NEXTJS_FOLDER}} -exec rm -rf {} +
116+
find . -maxdepth 1 -mindepth 1 -not -name ${{ env.NEXTJS_FOLDER }} -exec rm -rf {} +
120117
121118
- name: Testing compile-time build
122-
run: cd ${{env.NEXTJS_FOLDER}} && npm run build
119+
run: cd ${{ env.NEXTJS_FOLDER }} && npm run build
123120

124121
- name: Test runtime for errors and warnings
125122
run: cd ${{env.NEXTJS_FOLDER}} && npx start-server-and-test@3.0.0 'start' 3000 'npx cypress@15.13.0 run --spec "./integration-tests/tests/*" --config video=false'

.github/workflows/integration_test_remix.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- name: Prefer ESM imports
6464
run: |
6565
mv ./packages/e2e/integration-tests/fixtures/index.esm.ts ./packages/e2e/integration-tests/fixtures/index.ts
66-
rm ./packages/e2e/integration-tests/fixtures/index.cjs.ts
66+
rm ./packages/e2e/integration-tests/fixtures/index.umd.ts
6767
6868
- name: Copying required files
6969
run: |

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ node_modules
33

44
#output
55
lib/
6+
esm/
67
storybook-static
78

89
# testing
@@ -36,4 +37,5 @@ debug-storybook.log
3637
next-js-test/
3738
cra-test/
3839
astro-test/
39-
remix-test/
40+
remix-test/
41+
esm-test/

.size-limit.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module.exports = [
2+
{
3+
name: 'UMD — full bundle (JS)',
4+
path: 'packages/react/lib/index.js',
5+
},
6+
{
7+
name: 'UMD — full bundle (CSS)',
8+
path: 'packages/react/lib/css/main.css',
9+
},
10+
{
11+
name: 'ESM — full bundle (JS + CSS)',
12+
path: 'packages/react/esm/**/*.{js,css}',
13+
},
14+
{
15+
name: 'ESM — tree-shaken simple (Button)',
16+
path: 'packages/react/esm/index.esm.js',
17+
import: '{ Button }',
18+
modifyEsbuildConfig(config) {
19+
config.external = ['react', 'react-dom']
20+
return config
21+
},
22+
},
23+
{
24+
name: 'ESM — tree-shaken complex (ActionMenu)',
25+
path: 'packages/react/esm/index.esm.js',
26+
import: '{ ActionMenu }',
27+
modifyEsbuildConfig(config) {
28+
config.external = ['react', 'react-dom']
29+
return config
30+
},
31+
},
32+
]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: Getting started
3+
description: Primer Brand is open-sourced on GitHub and available on npm.
4+
menu-position: 1
5+
keywords: ['accessibility', 'introduction', 'simple']
6+
show-tabs: true
7+
tab-label: ESM
8+
---
9+
10+
Using ESM, you will load only the React components and CSS you need.
11+
12+
We recommend using ESM if your project uses compatible bundlers or frameworks like Vite or Next.js.
13+
14+
### 1. Install Primer Brand
15+
16+
```shell
17+
npm install @primer/react-brand
18+
```
19+
20+
### 2. Import components from the ESM folder
21+
22+
```js
23+
import {Hero, ThemeProvider} from '@primer/react-brand/esm'
24+
```
25+
26+
Each component's styles are automatically included when you import the component.
27+
28+
<Note>
29+
<p>
30+
Do not combine this approach with the global <code>lib/css/main.css</code> import.
31+
</p>
32+
33+
<p>Choose one approach or the other to avoid loading duplicate styles.</p>
34+
</Note>
35+
36+
### 3. Import the font assets
37+
38+
The Mona Sans typeface still needs to be loaded separately:
39+
40+
```js
41+
import '@primer/react-brand/fonts/fonts.css'
42+
```
43+
44+
### 4. Use the `ThemeProvider`
45+
46+
```js
47+
import {ThemeProvider} from '@primer/react-brand/esm'
48+
49+
function App() {
50+
return (
51+
<ThemeProvider>
52+
<div>...</div>
53+
</ThemeProvider>
54+
)
55+
}
56+
```

apps/next-docs/content/introduction/getting-started.mdx renamed to apps/next-docs/content/getting-started/index.mdx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
title: Getting started
33
description: Primer Brand is open-sourced on GitHub and available on npm.
44
menu-position: 1
5-
keywords: ['accessibility', 'introduction', 'simple']
5+
keywords: ['introduction', 'simple', 'get started', 'tutorials']
6+
show-tabs: true
7+
tab-label: Default
68
---
79

810
Primer Brand is [open-sourced on GitHub](https://github.com/primer/brand) and [available on npm](https://www.npmjs.com/package/@primer/react-brand).
@@ -19,6 +21,11 @@ npm install @primer/react-brand
1921

2022
Primer Brand requires a global stylesheet to be loaded ahead-of-time.
2123

24+
<Note>
25+
Refer to [this separate guide if you are using an ESM-compatible bundler](./esm.mdx). Component styles are included
26+
with the React component imports.
27+
</Note>
28+
2229
#### a. Using an application bundler (e.g. Webpack)
2330

2431
Import the `main` stylesheet at the earliest rendering opportunity:

apps/next-docs/content/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export default function HomepageComponent() {
4343
<Stack padding="none" gap="normal" className={styles.CardGridContainer}>
4444
<Grid>
4545
<Grid.Column span={{xsmall: 12, large: 6, xlarge: 4}}>
46-
<NextLink href="/introduction/getting-started" legacyBehavior passHref>
47-
<Card href="/introduction/getting-started" hasBorder fullWidth>
46+
<NextLink href="/getting-started" legacyBehavior passHref>
47+
<Card href="/getting-started" hasBorder fullWidth>
4848
<Card.Heading as="h2">Getting started</Card.Heading>
4949
<Card.Description>
5050
Standards, guidelines, and tools to get started with Primer Brand.

0 commit comments

Comments
 (0)