Skip to content

Commit bbc554a

Browse files
authored
Merge pull request #203 from NaverPayDev/feature/202
[safe-html-react-parser] Replace isomorphic-dompurify with custom implementation supporting flexible DOM libraries
2 parents 5026fb9 + 3a166d5 commit bbc554a

7 files changed

Lines changed: 587 additions & 78 deletions

File tree

.changeset/fifty-cars-heal.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@naverpay/safe-html-react-parser": minor
3+
---
4+
5+
[safe-html-react-parser] Replace isomorphic-dompurify with custom implementation supporting flexible DOM libraries
6+
7+
PR: [[safe-html-react-parser] Replace isomorphic-dompurify with custom implementation supporting flexible DOM libraries](https://github.com/NaverPayDev/pie/pull/203)

packages/safe-html-react-parser/README.md

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
# safe-html-react-parser
22

3-
A secure wrapper for **html-react-parser** with **isomorphic-dompurify** that automatically sanitizes HTML before parsing.
3+
A secure wrapper for **html-react-parser** with **DOMPurify** that automatically sanitizes HTML before parsing.
44

55
## What it does
66

77
- 🛡️ **Security**: Automatically sanitizes malicious HTML using DOMPurify
88
- ⚛️ **React**: Seamlessly integrates with html-react-parser
99
- 🌐 **Universal**: Works in both browser and Node.js (SSR) environments
1010
- 🏷️ **Custom Tags**: Handles project-specific tags like `<custom>` safely
11-
12-
## Requirements
13-
14-
- Node.js >=20.19.5: isomorphic-dompurify@^2.30.1
11+
- 🚀 **Flexible**: Choose your DOM implementation (jsdom, happy-dom, or linkedom)
12+
-**Optimized**: Built-in caching and memory management
1513

1614
## Installation
1715

1816
```bash
1917
npm install @naverpay/safe-html-react-parser
2018
```
2119

20+
### Choosing a DOM Implementation (Server-Side Only)
21+
22+
For server-side rendering, you need to install one of the following DOM implementations:
23+
24+
```bash
25+
# Option 1: jsdom (most complete, heavier)
26+
npm install jsdom
27+
28+
# Option 2: happy-dom (faster, lighter, recommended)
29+
npm install happy-dom
30+
31+
# Option 3: linkedom (fastest, lightest)
32+
npm install linkedom
33+
```
34+
35+
The library will automatically detect and use the first available implementation in this order: jsdom → happy-dom → linkedom.
36+
2237
## Basic Usage
2338

2439
```tsx
@@ -119,6 +134,63 @@ const result = safeParse(html, {
119134
})
120135
```
121136

137+
### Configuring DOM Implementation (Server-Side)
138+
139+
You have two ways to configure the DOM implementation:
140+
141+
#### Method 1: Per-call configuration (Recommended)
142+
143+
Pass `domPurifyOptions` directly to `safeParse()`:
144+
145+
```tsx
146+
import { safeParse } from '@naverpay/safe-html-react-parser'
147+
import { Window } from 'happy-dom'
148+
149+
const result = safeParse(htmlString, {
150+
domPurifyOptions: {
151+
domWindowFactory: () => new Window(),
152+
enableCache: true,
153+
maxCacheSize: 100
154+
}
155+
})
156+
```
157+
158+
#### Method 2: Global configuration
159+
160+
Configure once at app initialization:
161+
162+
```tsx
163+
import { configureDOMPurify } from '@naverpay/safe-html-react-parser'
164+
165+
// Using jsdom
166+
import { JSDOM } from 'jsdom'
167+
configureDOMPurify({
168+
domWindowFactory: () => new JSDOM('<!DOCTYPE html>'),
169+
enableCache: true,
170+
maxCacheSize: 100,
171+
recreateInterval: 1000 // Recreate DOM instance every 1000 sanitizations
172+
})
173+
174+
// Using happy-dom (recommended for better performance)
175+
import { Window } from 'happy-dom'
176+
configureDOMPurify({
177+
domWindowFactory: () => new Window(),
178+
enableCache: true,
179+
recreateInterval: 500
180+
})
181+
182+
// Using linkedom (fastest, minimal footprint)
183+
import { parseHTML } from 'linkedom'
184+
configureDOMPurify({
185+
domWindowFactory: () => parseHTML('<!DOCTYPE html>'),
186+
enableCache: true
187+
})
188+
```
189+
190+
> [!NOTE]
191+
>
192+
> If you don't configure anything, the library will automatically try jsdom → happy-dom → linkedom in that order.
193+
122194
## Default Allowed Tags
123195

124196
By default, the following HTML tags are allowed:
@@ -132,6 +204,37 @@ ALLOWED_TAGS: [
132204
]
133205
```
134206

207+
## Performance Optimization
208+
209+
### Caching
210+
211+
By default, caching is enabled to improve performance:
212+
213+
```tsx
214+
configureDOMPurify({
215+
enableCache: true, // Default: true
216+
maxCacheSize: 100, // Default: 100
217+
})
218+
```
219+
220+
### Memory Management
221+
222+
The DOM instance is automatically recreated periodically to prevent memory leaks:
223+
224+
```tsx
225+
configureDOMPurify({
226+
recreateInterval: 1000 // Default: 1000 sanitization calls
227+
})
228+
```
229+
230+
### DOM Implementation Comparison
231+
232+
| Implementation | Speed | Memory | Completeness | Recommended For |
233+
|----------------|-------|--------|--------------|-----------------|
234+
| **jsdom** | Slower | Higher | Most complete | Maximum compatibility |
235+
| **happy-dom** | Fast | Medium | Good | **Balanced (Recommended)** |
236+
| **linkedom** | Fastest | Lowest | Basic | Performance-critical apps |
237+
135238
## Security Notes
136239

137240
- All HTML is sanitized by DOMPurify before parsing
@@ -142,7 +245,8 @@ ALLOWED_TAGS: [
142245
## Built with
143246

144247
- [html-react-parser@^5.2.7](https://github.com/remarkablemark/html-react-parser) - HTML string to React element parser
145-
- [isomorphic-dompurify@^2.30.1](https://github.com/kkomelin/isomorphic-dompurify) - Universal XSS sanitizer
248+
- [dompurify@^3.3.0](https://github.com/cure53/DOMPurify) - XSS sanitizer
249+
- Optional: [jsdom](https://github.com/jsdom/jsdom), [happy-dom](https://github.com/capricorn86/happy-dom), or [linkedom](https://github.com/WebReflection/linkedom)
146250

147251
## License
148252

packages/safe-html-react-parser/package.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,39 @@
1919
],
2020
"author": "@NaverPayDev/frontend",
2121
"dependencies": {
22-
"html-react-parser": "^5.2.7",
23-
"isomorphic-dompurify": "^2.30.1"
22+
"dompurify": "^3.3.0",
23+
"html-react-parser": "^5.2.7"
2424
},
2525
"devDependencies": {
26+
"@types/jsdom": "^27.0.0",
2627
"@types/react": "0.14 || 15 || 16 || 17 || 18 || 19",
28+
"happy-dom": "^17.4.4",
29+
"jsdom": "^27.2.0",
30+
"linkedom": "^0.18.12",
2731
"react": "0.14 || 15 || 16 || 17 || 18 || 19"
2832
},
2933
"peerDependencies": {
3034
"@types/react": "0.14 || 15 || 16 || 17 || 18 || 19",
35+
"happy-dom": "^17.4.4",
36+
"jsdom": "^27.2.0",
37+
"linkedom": "^0.18.12",
3138
"react": "0.14 || 15 || 16 || 17 || 18 || 19"
3239
},
40+
"peerDependenciesMeta": {
41+
"jsdom": {
42+
"optional": true
43+
},
44+
"happy-dom": {
45+
"optional": true
46+
},
47+
"linkedom": {
48+
"optional": true
49+
}
50+
},
3351
"scripts": {
3452
"clean": "rm -rf dist",
35-
"build": "npm run clean && vite build"
53+
"build": "npm run clean && vite build",
54+
"test:memory": "vitest run --watch=false"
3655
},
3756
"main": "./dist/cjs/index.js",
3857
"module": "./dist/esm/index.mjs",

packages/safe-html-react-parser/src/index.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
* Utilizes html-react-parser with DOMPurify for safe HTML parsing
44
*/
55
import * as htmlReactParser from 'html-react-parser'
6-
import DOMPurify from 'isomorphic-dompurify'
6+
7+
import {sanitizeHtml, type SanitizerOptions as DOMPurifyOptionsType, type SanitizeConfig} from './utils/dompurify'
78

89
import type {DOMNode, HTMLReactParserOptions} from 'html-react-parser'
910

10-
// html-react-parser가 esm에서 cjs 모듈을 re-export 하는 문제 처리
11+
// Re-export configuration function
12+
export {configureDOMPurify} from './utils/dompurify'
13+
export type {DOMWindow, DOMWindowFactory, SanitizerOptions as DOMPurifyOptions} from './utils/dompurify'
14+
15+
// Solving the issue of html-react-parser re-exporting cjs modules in esm
1116
// In CJS: htmlReactParser.default.default is the actual function
1217
// In ESM: htmlReactParser.default is the function
1318
const parse = ((htmlReactParser as any).default?.default ||
@@ -16,16 +21,30 @@ const parse = ((htmlReactParser as any).default?.default ||
1621

1722
export interface SafeParseOptions extends HTMLReactParserOptions {
1823
/**
19-
* DOMPurify Options
24+
* DOMPurify sanitization configuration
2025
*/
21-
sanitizeConfig?: DOMPurify.Config
26+
sanitizeConfig?: SanitizeConfig
2227
/**
2328
* Custom tag preservation option (temporary conversion before and after DOMPurify processing)
2429
*/
2530
preserveCustomTags?: string[]
31+
/**
32+
* Server-side DOMPurify options (DOM implementation, caching, etc.)
33+
* Only used on server-side. Ignored on client-side.
34+
*
35+
* @example
36+
* import { Window } from 'happy-dom'
37+
* safeParse(html, {
38+
* domPurifyOptions: {
39+
* domWindowFactory: () => new Window(),
40+
* enableCache: true
41+
* }
42+
* })
43+
*/
44+
domPurifyOptions?: DOMPurifyOptionsType
2645
}
2746

28-
export const DEFAULT_SANITIZE_CONFIG: DOMPurify.Config = {
47+
export const DEFAULT_SANITIZE_CONFIG: SanitizeConfig = {
2948
ALLOWED_TAGS: [
3049
'p',
3150
'br',
@@ -61,7 +80,7 @@ export const DEFAULT_SANITIZE_CONFIG: DOMPurify.Config = {
6180
* @returns Parsed React elements
6281
*/
6382
export function safeParse(htmlString: string, options: SafeParseOptions = {}) {
64-
const {sanitizeConfig = DEFAULT_SANITIZE_CONFIG, preserveCustomTags, ...parserOptions} = options
83+
const {sanitizeConfig = DEFAULT_SANITIZE_CONFIG, preserveCustomTags, domPurifyOptions, ...parserOptions} = options
6584

6685
// Temporarily convert custom tags to safe tags to preserve them during DOMPurify processing
6786
const processedHtml =
@@ -73,7 +92,11 @@ export function safeParse(htmlString: string, options: SafeParseOptions = {}) {
7392
htmlString,
7493
) || htmlString
7594

76-
const sanitizedHtml = DOMPurify.sanitize(processedHtml, sanitizeConfig)
95+
const sanitizedHtml = sanitizeHtml(processedHtml, sanitizeConfig, domPurifyOptions)
96+
97+
if (!sanitizedHtml) {
98+
return null
99+
}
77100

78101
return parse(sanitizedHtml, {
79102
...parserOptions,

0 commit comments

Comments
 (0)