Skip to content

Commit 5504d6c

Browse files
authored
Merge pull request #1105 from mbifulco/feat/newsletter-hello-world
testing: resend broadcast preview
2 parents 912d825 + cdebd5f commit 5504d6c

File tree

5 files changed

+187
-13
lines changed

5 files changed

+187
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"prettier-plugin-tailwindcss": "^0.7.1",
121121
"schema-dts": "^1.1.5",
122122
"tailwindcss": "^4.1.17",
123+
"tsx": "^4.20.6",
123124
"typescript": "5.9.3",
124125
"typescript-eslint": "^8.46.3",
125126
"vite-tsconfig-paths": "^5.1.4",

pnpm-lock.yaml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
title: "Patching NPM Dependencies with pnpm patch"
3+
excerpt: "Learn how to patch NPM dependencies with pnpm patch to fix issues with your project."
4+
tags: [javascript, oss, devtools]
5+
coverImagePublicId: "posts/patching-npm-dependencies-with-pnpm-patch/cover"
6+
slug: patching-npm-dependencies-with-pnpm-patch
7+
date: 11-11-2025
8+
published: true
9+
---
10+
11+
## Introduction
12+
13+
Sometimes a third-party package breaks, and you can't wait for a PR to make its way into a release. Fortunately, `pnpm patch` makes it dead simple to hotfix packages locally while keeping your version control clean.
14+
15+
Recently, we needed to patch the [`@payloadcms/storage-uploadthing` package](https://www.npmjs.com/package/@payloadcms/storage-uploadthing) to fix a bug with image loading, so that we could continue to use it as our image hosting provider for [Craftwork](https://craftwork.com)'s implementation of [Payload CMS](https://payloadcms.com).
16+
17+
Here's how we handled it, using `pnpm patch`.
18+
19+
20+
### What is pnpm patch?
21+
22+
[`pnpm patch`](https://pnpm.io/cli/patch) is a tool provided by the pnpm package manager that allows you to patch packages locally—and commit those changes cleanly. It works by creating a patch file that contains the changes you want to make to the package. You can then apply the patch file to the package using `pnpm install`.
23+
24+
## The process
25+
26+
Patching a package is a three-step process:
27+
1. Track down the bug
28+
2. Create a patch file
29+
3. Apply the patch file to the package
30+
31+
Let's go through each step in detail.
32+
33+
### Step 1: Track down the bug
34+
35+
In our case, we tracked the bug down by making changes to the `handleUpload` function in `@payloadcms/storage-uploadthing`. You can do this in two ways:
36+
37+
1. Dig into `node_modules/@payloadcms/storage-uploadthing/src/...` directly
38+
2. Or, clone the repo and search from there if you want to keep your project folder clean
39+
40+
We worked directly inside `node_modules` to validate the fix quickly. One thing to keep in mind here is that running `pnpm install` will overwrite your changes to the package -- which is eactly why we need to use `pnpm patch` to create a persistent patch!
41+
42+
### Step 2: Make a Local Fix
43+
44+
Once you've got a fix in mind, try it out directly in your project. You will do this by making temporary edits right inside the installed version of the package within your IDE.
45+
46+
<Aside type="note">
47+
In our case, the fix for `@payloadcms/storage-uploadthing` involved some minor changes:
48+
49+
- the `handleUpload` function wasn't setting a `url` field based on the UploadThing API's response, so our Payload db didn't have any image URLs stored.
50+
- the `disablePayloadAccessControl` setting was commented out, which caused payload to store _local_ image URLs instead of UploadThing's public URLs.
51+
</Aside>
52+
53+
Once we saw that this fix solved the issue in development, it was time to commit it to source control using `pnpm patch`.
54+
55+
### Step 3: Create a Persistent Patch with pnpm patch
56+
57+
<Image
58+
publicId="posts/patching-npm-dependencies-with-pnpm-patch/patch-process-1"
59+
alt="A screenshot of the terminal showing the output of the `pnpm patch` command"
60+
caption="The `pnpm patch` command opens the dependency's code in a temporary directory for you to edit."
61+
/>
62+
63+
To preserve your fix and commit it to source control:
64+
65+
1. run: `pnpm patch <package-name>`. This opens the dependency's code in a temporary directory for you to edit.
66+
2. Reapply the fix in the temporary folder.
67+
3. Test it - do everything you would typically do to make sure the fix works: test functionality, run linters and test suites, etc.
68+
4. To complete your patch and ready it for addition to source control on your project, run: `pnpm patch-commit <package-name>`
69+
70+
That saves the patch to your repo, and pnpm will re-apply it automatically every time you install dependencies. It also modifies your `package.json` to reference the patched version.
71+
72+
This will add a new entry to your `package.json` like this:
73+
```json
74+
"@payloadcms/storage-uploadthing": {
75+
"patches": {
76+
"@payloadcms/storage-uploadthing": "patch/@payloadcms/storage-uploadthing.patch"
77+
}
78+
}
79+
```
80+
81+
...as well as a file in your project's `patches` directory that represents the patch itself, in git diff format. This file type is a little difficult to read. You can re-open your patch file in your code editor to see the changes. Your changes will also be reflected in your `pnpm-lock.yaml` file.
82+
83+
This will cause pnpm to apply the patch every time you install dependencies.
84+
85+
### If you need to edit your patch
86+
87+
This is one part of the workflow that leaves a bit to be desired - if you need to edit your patch later, the best workflow I've found is to remove the patch from your project by running: `pnpm patch-remove <package-name>`, and then start a fresh patch by running: `pnpm patch <package-name>` (and going through these steps again).
88+
89+
You _can_ edit the `.patch` file directly, but it's finicky. In most cases, it's easier to remove and re-create the patch.
90+
91+
92+
### Closing the loop with the package maintainer
93+
94+
Your dependency is now patched, and you can commit it to source control to use. This can help provide evidence to open source maintainers that the fix is needed, and that it works. It's good practice to [open a pull request](https://github.com/payloadcms/payload/pull/14250) to the package maintainer with your fix so that they can review it and merge it into the main branch.
95+
96+
97+
### When it's time to remove the patch
98+
99+
Once there is an upstream fix in place, you can remove the patch from your project by running: `pnpm patch-remove <package-name>`.
100+
101+
This will remove the patch from your `package.json` and the `patches` directory, and you can then commit the changes to source control.
102+
103+
Remember that this doesn't automatically update the dependency in your project to the latest version - you'll still need to use `pnpm install` to do that.
104+
105+
---
106+
107+
## Why this process is useful
108+
109+
- You don't need to wait on library maintainers to merge PRs to get your fix into your project
110+
- Your fix lives in version control, so teammates and CI builds stay in sync
111+
- It's a fast, repeatable way to patch packages without publishing a fork
112+
113+
114+
## Not using pnpm?
115+
If your team is using a package manager other than pnpm, there are other options that are very similar:
116+
117+
- for `yarn` you can use [`yarn patch`](https://yarnpkg.com/cli/patch)
118+
- for `bun` you can use [`bun patch`](https://bun.com/docs/pm/cli/patch)
119+
- for `npm` you can use [`npx patch package`](https://www.npmjs.com/package/patch-package)
120+
- if you're using ruby's `bundler`, you're in the wrong place, and you have my sympathies.
121+
122+
The specifics of each of these options are very similar, but vary slightly - refer to the docs linked above for a walkthrough of each.

src/utils/email/templates/EmailLayout.tsx

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,37 @@ type EmailLayoutProps = {
1818
children: React.ReactNode;
1919
/** Optional first name for greeting. Set to `false` to disable greeting entirely. */
2020
firstName?: string | false;
21+
includeUnsubscribeLink?: boolean;
2122
};
2223

2324
export const EmailLayout = ({
2425
preview,
2526
children,
2627
firstName,
28+
includeUnsubscribeLink = false,
2729
}: EmailLayoutProps) => {
2830
const showGreeting = firstName !== false;
2931
const greeting = `Hey ${typeof firstName === 'string' ? firstName : 'there'}`;
3032

3133
return (
3234
<Html>
33-
<Head />
35+
<Head>
36+
<style>{`
37+
h1, h2, h3, h4, h5, h6 {
38+
font-weight: 900;
39+
color: #D83D84;
40+
margin-top: 24px;
41+
margin-bottom: 16px;
42+
line-height: 1.3;
43+
}
44+
h1 { font-size: 32px; }
45+
h2 { font-size: 28px; }
46+
h3 { font-size: 24px; }
47+
h4 { font-size: 20px; }
48+
h5 { font-size: 18px; }
49+
h6 { font-size: 16px; }
50+
`}</style>
51+
</Head>
3452
<Preview>{preview}</Preview>
3553

3654
<Tailwind>
@@ -67,18 +85,42 @@ export const EmailLayout = ({
6785
</Section>
6886

6987
{/* Footer Text */}
70-
<Text
88+
<Section
7189
style={{
72-
textAlign: 'center',
73-
fontSize: 12,
74-
color: 'rgb(0,0,0, 0.7)',
90+
maxWidth: '500px',
91+
display: 'flex',
92+
flexDirection: 'column',
93+
gap: '10px',
7594
}}
95+
className="mt-2 text-gray-500"
7696
>
77-
© 2024 | 💌 Tiny Improvements ·{' '}
78-
<Link href="https://mikebifulco.com" className="text-pink-600">
79-
mikebifulco.com
80-
</Link>{' '}
81-
</Text>
97+
<Text
98+
style={{
99+
fontSize: 12,
100+
}}
101+
className="my-0"
102+
>
103+
© {new Date().getFullYear()} &bull; 💌 Tiny Improvements &bull;{' '}
104+
<Link
105+
href="https://mikebifulco.com/newsletter"
106+
className="text-pink-600"
107+
>
108+
mikebifulco.com
109+
</Link>{' '}
110+
</Text>
111+
{includeUnsubscribeLink && (
112+
<Text className="my-0 text-xs text-gray-500">
113+
Not getting what you need? No worries, you can{' '}
114+
<Link
115+
href="{{{RESEND_UNSUBSCRIBE_LINK}}}"
116+
className="text-pink-600"
117+
>
118+
unsubscribe
119+
</Link>{' '}
120+
anytime.
121+
</Text>
122+
)}
123+
</Section>
82124
</Container>
83125
</Body>
84126
</Tailwind>

src/utils/email/templates/NewsletterEmail.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ type NewsletterEmailProps = {
2727
* );
2828
* ```
2929
*/
30-
export const NewsletterEmail = ({ content, excerpt }: NewsletterEmailProps) => {
30+
export const NewsletterEmail = ({
31+
content = '',
32+
excerpt = 'Preview text for email clients',
33+
}: NewsletterEmailProps) => {
3134
return (
32-
<EmailLayout preview={excerpt} firstName={false}>
35+
<EmailLayout
36+
preview={excerpt}
37+
firstName={false}
38+
includeUnsubscribeLink={true}
39+
>
3340
<Row>
3441
<Column>
3542
<Markdown>{content}</Markdown>

0 commit comments

Comments
 (0)