Skip to content

Commit a6991bb

Browse files
authored
feat(handleError): add the ability to handle errors (#15)
* feat(handleError): add the ability to handle errors * test: add another test case
1 parent cc95c85 commit a6991bb

File tree

3 files changed

+123
-6
lines changed

3 files changed

+123
-6
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ async function go() {
108108
// <p>This is a CodeSandbox:</p>
109109
// <iframe src="https://codesandbox.io/embed/css-variables-vs-themeprovider-df90h" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>
110110
}
111+
112+
go()
111113
```
112114

113115
### Options
@@ -169,7 +171,56 @@ return `<div><blockquote>...</blockquote><a href="...">...</a></div>`
169171
Some services have endpoints that you can use to get the embed HTML ([like
170172
twitter for example][twitter-oembed-docs]).
171173

172-
#### `cache: Map<string, string | null>`
174+
#### `handleError?: (errorInfo: ErrorInfo) => GottenHTML | Promise<GottenHTML>
175+
176+
```ts
177+
type ErrorInfo = {
178+
error: Error
179+
url: string
180+
transformer: Transformer<unknown>
181+
config: TransformerConfig
182+
}
183+
```
184+
185+
If remark-embedder encounters an error with any transformer, then compilation
186+
will fail. I've found this to be problematic when using
187+
[`@remark-embedder/transformer-oembed`][@remark-embedder/transformer-oembed] for
188+
tweets and the tweet author deletes their tweet. It'll prevent you from building
189+
and that's annoying.
190+
191+
So `handleError` allows you to handle any error that's thrown by a transformer.
192+
This way you can gracefully fallback to something rather than crashing
193+
everything. Even if you provide a `handleError`, we'll log to the console so you
194+
can fix the problem in the future.
195+
196+
Here's a quick example of an error handler that would handle deleted tweets:
197+
198+
```typescript
199+
function handleError({error, url, transformer}) {
200+
if (
201+
transformer.name !== '@remark-embedder/transformer-oembed' ||
202+
!url.includes('twitter.com')
203+
) {
204+
// we're only handling errors from this specific transformer and the twitter URL
205+
// so we'll rethrow errors from any other transformer/url
206+
throw error
207+
}
208+
return `<p>Unable to embed <a href="${url}">this tweet</a>.</p>`
209+
}
210+
211+
const result = await remark()
212+
.use(remarkEmbedder, {
213+
transformers: [oembedTransformer],
214+
handleError,
215+
})
216+
.use(html)
217+
.process(exampleMarkdown)
218+
```
219+
220+
You'll get an error logged, but it won't break your build. It also won't be
221+
cached (if you're using the `cache` option).
222+
223+
#### `cache?: Map<string, string | null>`
173224

174225
**You'll mostly likely want to use
175226
[`@remark-embedder/cache`][@remark-embedder/cache]**

src/__tests__/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ const unquoteSerializer = {
1212

1313
expect.addSnapshotSerializer(unquoteSerializer)
1414

15+
const consoleError = jest.spyOn(console, 'error')
16+
afterEach(() => {
17+
expect(consoleError).not.toHaveBeenCalled()
18+
consoleError.mockReset()
19+
})
20+
1521
const getTransformer = <ConfigType>(
1622
overrides: Partial<Transformer<ConfigType>> = {},
1723
): Transformer<ConfigType> => ({
@@ -201,6 +207,48 @@ https://some-site.com/do-not-transform
201207
`)
202208
})
203209

210+
test('can handle errors', async () => {
211+
consoleError.mockImplementationOnce(() => {})
212+
const transformer = getTransformer({
213+
getHTML: () => Promise.reject(new Error('OH_NO_AN_ERROR_HAPPENED')),
214+
})
215+
const handleError = jest.fn(({error}) => `<div>${error.message}</div>`)
216+
const result = await remark()
217+
.use(remarkEmbedder, {transformers: [transformer], handleError})
218+
.use(remarkHTML)
219+
.process(`[https://some-site.com](https://some-site.com)`)
220+
221+
expect(result.toString()).toMatchInlineSnapshot(
222+
`<div>OH_NO_AN_ERROR_HAPPENED</div>`,
223+
)
224+
225+
expect(consoleError).toHaveBeenCalledTimes(1)
226+
expect(consoleError).toHaveBeenCalledWith(expect.any(String))
227+
expect(consoleError.mock.calls[0][0]).toMatchInlineSnapshot(`
228+
The following error occurred while processing \`https://some-site.com/\` with the remark-embedder transformer \`TEST\`:
229+
230+
OH_NO_AN_ERROR_HAPPENED
231+
`)
232+
consoleError.mockClear()
233+
})
234+
235+
test('handleError can return null', async () => {
236+
consoleError.mockImplementationOnce(() => {})
237+
const transformer = getTransformer({
238+
getHTML: () => Promise.reject(new Error('OH_NO_AN_ERROR_HAPPENED')),
239+
})
240+
const handleError = jest.fn(() => null)
241+
const result = await remark()
242+
.use(remarkEmbedder, {transformers: [transformer], handleError})
243+
.use(remarkHTML)
244+
.process(`[https://some-site.com](https://some-site.com)`)
245+
246+
expect(result.toString()).toMatchInlineSnapshot(
247+
`<p><a href="https://some-site.com">https://some-site.com</a></p>`,
248+
)
249+
consoleError.mockClear()
250+
})
251+
204252
// no idea why TS and remark aren't getting along here,
205253
// but I don't have time to look into it right now...
206254
/*

src/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type RemarkEmbedderOptions = {
2626
[key: string]: unknown
2727
}
2828
transformers: Array<Transformer<any> | [Transformer<any>, TransformerConfig]>
29+
handleError?: (errorInfo: {
30+
error: Error
31+
url: string
32+
transformer: Transformer<unknown>
33+
config: TransformerConfig
34+
}) => GottenHTML | Promise<GottenHTML>
2935
}
3036

3137
// results in an AST node of type "root" with a single "children" node of type "element"
@@ -46,6 +52,7 @@ const getUrlString = (url: string): string | null => {
4652
const remarkEmbedder: Plugin<[RemarkEmbedderOptions]> = ({
4753
cache,
4854
transformers,
55+
handleError,
4956
}) => {
5057
// convert the array of transformers to one with both the transformer and the config tuple
5158
const transformersAndConfig: Array<{
@@ -103,14 +110,26 @@ const remarkEmbedder: Plugin<[RemarkEmbedderOptions]> = ({
103110

104111
const promises = nodesToTransform.map(
105112
async ({parentNode, url, transformer, config}) => {
113+
const errorMessageBanner = `The following error occurred while processing \`${url}\` with the remark-embedder transformer \`${transformer.name}\`:`
106114
try {
107115
const cacheKey = `remark-embedder:${transformer.name}:${url}`
108116
let html: GottenHTML | undefined = await cache?.get(cacheKey)
109117

110118
if (!html) {
111-
html = await transformer.getHTML(url, config)
112-
html = html?.trim() ?? null
113-
await cache?.set(cacheKey, html)
119+
try {
120+
html = await transformer.getHTML(url, config)
121+
html = html?.trim() ?? null
122+
await cache?.set(cacheKey, html)
123+
} catch (e: unknown) {
124+
if (handleError) {
125+
const error = e as Error
126+
console.error(`${errorMessageBanner}\n\n${error.message}`)
127+
html = await handleError({error, url, transformer, config})
128+
html = html?.trim() ?? null
129+
} else {
130+
throw e
131+
}
132+
}
114133
}
115134

116135
// if nothing's returned from getHTML, then no modifications are needed
@@ -128,9 +147,8 @@ const remarkEmbedder: Plugin<[RemarkEmbedderOptions]> = ({
128147
hProperties: htmlElement.properties,
129148
}
130149
} catch (e: unknown) {
131-
// https://github.com/microsoft/TypeScript/issues/20024#issuecomment-344511199
132150
const error = e as Error
133-
error.message = `The following error occurred while processing \`${url}\` with the remark-embedder transformer \`${transformer.name}\`:\n\n${error.message}`
151+
error.message = `${errorMessageBanner}\n\n${error.message}`
134152

135153
throw error
136154
}

0 commit comments

Comments
 (0)