@postalsys/email-text-tools 2.4.26 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +12 -0
- package/dist/cjs/index.d.ts +20 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/lib/commonjs.cjs +17 -0
- package/dist/cjs/lib/commonjs.d.cts +4 -0
- package/dist/cjs/lib/format-date.d.ts +15 -0
- package/dist/cjs/lib/format-date.js +60 -0
- package/dist/cjs/lib/html-guard.d.ts +3 -0
- package/dist/cjs/lib/html-guard.js +119 -0
- package/dist/cjs/lib/html-to-text.d.ts +1 -0
- package/dist/cjs/lib/html-to-text.js +49 -0
- package/dist/cjs/lib/inline-html.d.ts +19 -0
- package/dist/cjs/lib/inline-html.js +150 -0
- package/dist/cjs/lib/inline-style.d.ts +2 -0
- package/dist/cjs/lib/inline-style.js +42 -0
- package/dist/cjs/lib/inline-text.d.ts +3 -0
- package/dist/cjs/lib/inline-text.js +111 -0
- package/dist/cjs/lib/juice-worker.d.ts +1 -0
- package/dist/cjs/lib/juice-worker.js +19 -0
- package/dist/cjs/lib/mime-html.d.ts +34 -0
- package/dist/cjs/lib/mime-html.js +496 -0
- package/dist/cjs/lib/text-to-html.d.ts +1 -0
- package/dist/cjs/lib/text-to-html.js +182 -0
- package/dist/cjs/lib/worker-pool.d.ts +61 -0
- package/dist/cjs/lib/worker-pool.js +229 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/lib/commonjs.cjs +17 -0
- package/dist/esm/lib/commonjs.d.cts +4 -0
- package/dist/esm/lib/format-date.d.ts +15 -0
- package/{lib → dist/esm/lib}/format-date.js +8 -16
- package/dist/esm/lib/html-guard.d.ts +3 -0
- package/{lib → dist/esm/lib}/html-guard.js +8 -24
- package/dist/esm/lib/html-to-text.d.ts +1 -0
- package/{lib → dist/esm/lib}/html-to-text.js +2 -14
- package/dist/esm/lib/inline-html.d.ts +19 -0
- package/{lib → dist/esm/lib}/inline-html.js +17 -55
- package/dist/esm/lib/inline-style.d.ts +2 -0
- package/dist/esm/lib/inline-style.js +39 -0
- package/dist/esm/lib/inline-text.d.ts +3 -0
- package/{lib → dist/esm/lib}/inline-text.js +8 -31
- package/dist/esm/lib/juice-worker.d.ts +1 -0
- package/{lib → dist/esm/lib}/juice-worker.js +7 -8
- package/dist/esm/lib/mime-html.d.ts +34 -0
- package/{lib → dist/esm/lib}/mime-html.js +80 -161
- package/dist/esm/lib/text-to-html.d.ts +1 -0
- package/{lib → dist/esm/lib}/text-to-html.js +35 -63
- package/dist/esm/lib/worker-pool.d.ts +61 -0
- package/{lib → dist/esm/lib}/worker-pool.js +18 -64
- package/dist/esm/package.json +3 -0
- package/package.json +67 -10
- package/.github/VULNERABILITY_REPORT.yml +0 -75
- package/.github/workflows/release.yaml +0 -38
- package/.github/workflows/test.yaml +0 -35
- package/.ncurc.js +0 -28
- package/.prettierignore +0 -14
- package/.prettierrc.js +0 -10
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -105
- package/benchmark/benchmark.js +0 -348
- package/benchmark/concurrency-benchmark.js +0 -176
- package/benchmark/quick-benchmark.js +0 -200
- package/benchmark/quick-concurrency.js +0 -64
- package/benchmark/real-world-benchmark.js +0 -257
- package/docs/PERFORMANCE.md +0 -262
- package/eslint.config.js +0 -71
- package/examples/failing.html +0 -617
- package/examples/fixtures/message-1.json +0 -74
- package/examples/inline-html.js +0 -24
- package/examples/inline-text.js +0 -24
- package/examples/mime-html.js +0 -284
- package/index.js +0 -22
- package/release-please-config.json +0 -10
- package/test/comprehensive.test.js +0 -358
- package/test/containment.test.js +0 -128
- package/test/edge-cases.test.js +0 -62
- package/test/hardening.test.js +0 -504
- package/test/html-to-text.test.js +0 -253
- package/test/inline-html.test.js +0 -230
- package/test/inline-text.test.js +0 -204
- package/test/mime-html-extra.test.js +0 -39
- package/test/mime-html.test.js +0 -302
- package/test/text-to-html-extra.test.js +0 -42
- package/test/worker-pool-extra.test.js +0 -132
- package/test/worker-pool-regression.test.js +0 -128
- package/test/worker-pool.test.js +0 -282
package/docs/PERFORMANCE.md
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
# Performance Guide for email-text-tools
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
The `mime-html` module now provides both synchronous and asynchronous versions with configurable options for handling CSS processing. The async version includes worker thread support with pooling for high-throughput scenarios.
|
|
6
|
-
|
|
7
|
-
## Performance Characteristics
|
|
8
|
-
|
|
9
|
-
### Benchmark Results
|
|
10
|
-
|
|
11
|
-
Based on our benchmarks with various HTML complexities:
|
|
12
|
-
|
|
13
|
-
| Method | Average Time | Use Case |
|
|
14
|
-
| ---------------------- | ------------ | ---------------------------------------------------- |
|
|
15
|
-
| **Sync** | 5-10ms | Low-latency, simple HTML, single requests |
|
|
16
|
-
| **Async with Pool** | 5-10ms | High-throughput, concurrent processing |
|
|
17
|
-
| **Async without Pool** | 100-110ms | Isolated processing, memory-constrained environments |
|
|
18
|
-
|
|
19
|
-
### Key Findings
|
|
20
|
-
|
|
21
|
-
1. **Worker Pool Advantage**: The worker pool is **13.6x faster** than creating new workers for each request
|
|
22
|
-
2. **Sync vs Async**: For simple HTML, sync and async with pool have similar performance (~5-10ms)
|
|
23
|
-
3. **Startup Overhead**: Single worker creation adds ~100ms overhead per request
|
|
24
|
-
4. **Problematic CSS**: Both versions pre-filter known problematic selectors for optimal performance
|
|
25
|
-
5. **Timeout Protection**: The async version's timeout is rarely triggered thanks to pre-filtering, serving only as a safety net for unknown edge cases
|
|
26
|
-
|
|
27
|
-
## API Usage
|
|
28
|
-
|
|
29
|
-
### Synchronous Version (Default)
|
|
30
|
-
|
|
31
|
-
```javascript
|
|
32
|
-
const mimeHtml = require('@postalsys/email-text-tools/lib/mime-html');
|
|
33
|
-
|
|
34
|
-
// Simple usage - backward compatible
|
|
35
|
-
const result = mimeHtml({ html: htmlContent });
|
|
36
|
-
|
|
37
|
-
// Or explicitly use sync
|
|
38
|
-
const result = mimeHtml.sync({ html: htmlContent });
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Asynchronous Version with Options
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
const mimeHtml = require('@postalsys/email-text-tools/lib/mime-html');
|
|
45
|
-
|
|
46
|
-
// With default worker pool
|
|
47
|
-
const result = await mimeHtml.async(
|
|
48
|
-
{ html: htmlContent },
|
|
49
|
-
{
|
|
50
|
-
timeout: 5000, // Timeout in ms (default: 5000)
|
|
51
|
-
useWorkerPool: true, // Use worker pool (default: true)
|
|
52
|
-
minWorkers: 2, // Minimum pool workers (default: 2)
|
|
53
|
-
maxWorkers: 4, // Maximum pool workers (default: 4)
|
|
54
|
-
fallbackOnError: true, // Continue on error (default: true)
|
|
55
|
-
logErrors: true // Log errors to console (default: true)
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
// Without worker pool (creates new worker each time)
|
|
60
|
-
const result = await mimeHtml.async({ html: htmlContent }, { useWorkerPool: false });
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Configuration Options
|
|
64
|
-
|
|
65
|
-
### Async Options
|
|
66
|
-
|
|
67
|
-
| Option | Type | Default | Description |
|
|
68
|
-
| ----------------- | ------- | ------- | ------------------------------------------------- |
|
|
69
|
-
| `timeout` | number | 5000 | Maximum time (ms) for CSS processing |
|
|
70
|
-
| `useWorkerPool` | boolean | true | Use worker pool for better performance |
|
|
71
|
-
| `minWorkers` | number | 2 | Minimum workers in pool |
|
|
72
|
-
| `maxWorkers` | number | 4 | Maximum workers in pool |
|
|
73
|
-
| `fallbackOnError` | boolean | true | Return unstyled HTML on error instead of throwing |
|
|
74
|
-
| `logErrors` | boolean | true | Log processing errors to console |
|
|
75
|
-
| `idleTimeout` | number | 30000 | Time (ms) before idle workers are terminated |
|
|
76
|
-
|
|
77
|
-
## Worker Pool Management
|
|
78
|
-
|
|
79
|
-
### Monitoring Pool Status
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
// Get current pool statistics
|
|
83
|
-
const stats = mimeHtml.getWorkerPoolStats();
|
|
84
|
-
console.log(stats);
|
|
85
|
-
// {
|
|
86
|
-
// totalWorkers: 2,
|
|
87
|
-
// busyWorkers: 0,
|
|
88
|
-
// freeWorkers: 2,
|
|
89
|
-
// queuedTasks: 0,
|
|
90
|
-
// minWorkers: 2,
|
|
91
|
-
// maxWorkers: 4
|
|
92
|
-
// }
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Manual Pool Cleanup
|
|
96
|
-
|
|
97
|
-
```javascript
|
|
98
|
-
// Close worker pool when done (automatic on process exit)
|
|
99
|
-
await mimeHtml.closeWorkerPool();
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Choosing the Right Method
|
|
103
|
-
|
|
104
|
-
### Use Sync When:
|
|
105
|
-
|
|
106
|
-
- Processing single emails
|
|
107
|
-
- Low-latency is critical (<10ms)
|
|
108
|
-
- Simple HTML without complex CSS
|
|
109
|
-
- Running in resource-constrained environments
|
|
110
|
-
|
|
111
|
-
### Use Async with Pool When:
|
|
112
|
-
|
|
113
|
-
- Processing many emails concurrently
|
|
114
|
-
- Running a high-throughput service
|
|
115
|
-
- Need timeout protection for untrusted content
|
|
116
|
-
- Can accept slight latency for robustness
|
|
117
|
-
|
|
118
|
-
### Use Async without Pool When:
|
|
119
|
-
|
|
120
|
-
- Processing untrusted content occasionally
|
|
121
|
-
- Memory constraints prevent worker pooling
|
|
122
|
-
- Isolation between requests is critical
|
|
123
|
-
|
|
124
|
-
## Performance Optimization Tips
|
|
125
|
-
|
|
126
|
-
1. **Reuse the Worker Pool**: The global worker pool is shared across all async calls. Don't close it between requests.
|
|
127
|
-
|
|
128
|
-
2. **Tune Pool Size**: Adjust `minWorkers` and `maxWorkers` based on your workload:
|
|
129
|
-
|
|
130
|
-
```javascript
|
|
131
|
-
// For high-throughput services
|
|
132
|
-
{ minWorkers: 4, maxWorkers: 8 }
|
|
133
|
-
|
|
134
|
-
// For occasional processing
|
|
135
|
-
{ minWorkers: 1, maxWorkers: 2 }
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
3. **Handle Timeouts Gracefully**: Set appropriate timeouts based on HTML complexity:
|
|
139
|
-
|
|
140
|
-
```javascript
|
|
141
|
-
// For simple HTML
|
|
142
|
-
{
|
|
143
|
-
timeout: 2000;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// For complex HTML with many styles
|
|
147
|
-
{
|
|
148
|
-
timeout: 10000;
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
4. **Monitor Pool Health**: In production, monitor pool statistics to detect issues:
|
|
153
|
-
```javascript
|
|
154
|
-
setInterval(() => {
|
|
155
|
-
const stats = mimeHtml.getWorkerPoolStats();
|
|
156
|
-
if (stats && stats.queuedTasks > 10) {
|
|
157
|
-
console.warn('High queue depth:', stats.queuedTasks);
|
|
158
|
-
}
|
|
159
|
-
}, 5000);
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Handling Problematic CSS
|
|
163
|
-
|
|
164
|
-
The library handles modern CSS selectors that cause the juice library to hang:
|
|
165
|
-
|
|
166
|
-
### Problematic Selectors
|
|
167
|
-
|
|
168
|
-
- `:is()` pseudo-class
|
|
169
|
-
- `:where()` pseudo-class
|
|
170
|
-
- `:has()` pseudo-class
|
|
171
|
-
- Complex `:not()` selectors
|
|
172
|
-
|
|
173
|
-
### How It's Handled
|
|
174
|
-
|
|
175
|
-
**Both Versions**: Pre-process HTML to remove known problematic selectors before juice processing. This prevents most hangs from occurring in the first place.
|
|
176
|
-
|
|
177
|
-
**Key Difference**:
|
|
178
|
-
|
|
179
|
-
- **Sync Version**: Pre-filters selectors, then runs juice synchronously. Can still hang if an unknown problematic pattern is encountered.
|
|
180
|
-
- **Async Version**: Pre-filters selectors (same as sync), then runs juice in a worker thread with timeout protection. Provides complete safety against any CSS that might cause hangs, even unknown patterns.
|
|
181
|
-
|
|
182
|
-
The timeout in the async version is a safety net for edge cases, not the primary defense. Both versions handle the known problematic selectors efficiently without timing out.
|
|
183
|
-
|
|
184
|
-
### Example
|
|
185
|
-
|
|
186
|
-
```javascript
|
|
187
|
-
const problematicHtml = `
|
|
188
|
-
<style>
|
|
189
|
-
.item:is(.active, .selected) { background: yellow; }
|
|
190
|
-
.parent:has(> .child) { border: 1px solid red; }
|
|
191
|
-
</style>
|
|
192
|
-
<div class="item active">Content</div>
|
|
193
|
-
`;
|
|
194
|
-
|
|
195
|
-
// Both handle it safely
|
|
196
|
-
const syncResult = mimeHtml.sync({ html: problematicHtml });
|
|
197
|
-
const asyncResult = await mimeHtml.async({ html: problematicHtml });
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## Memory Considerations
|
|
201
|
-
|
|
202
|
-
- Each worker uses ~10-20MB of memory
|
|
203
|
-
- Worker pool maintains minimum workers even when idle
|
|
204
|
-
- Adjust pool size based on available memory:
|
|
205
|
-
- Low memory: `{ minWorkers: 1, maxWorkers: 2 }`
|
|
206
|
-
- Standard: `{ minWorkers: 2, maxWorkers: 4 }`
|
|
207
|
-
- High memory: `{ minWorkers: 4, maxWorkers: 8 }`
|
|
208
|
-
|
|
209
|
-
## Error Handling
|
|
210
|
-
|
|
211
|
-
```javascript
|
|
212
|
-
try {
|
|
213
|
-
// With fallback (default) - returns unstyled HTML on error
|
|
214
|
-
const result = await mimeHtml.async({ html: htmlContent }, { fallbackOnError: true });
|
|
215
|
-
} catch (err) {
|
|
216
|
-
// Only throws if fallbackOnError: false
|
|
217
|
-
console.error('Processing failed:', err);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check for processing issues
|
|
221
|
-
const result = await mimeHtml.async(
|
|
222
|
-
{ html: htmlContent },
|
|
223
|
-
{
|
|
224
|
-
logErrors: false, // Suppress console warnings
|
|
225
|
-
fallbackOnError: true
|
|
226
|
-
}
|
|
227
|
-
);
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Migration Guide
|
|
231
|
-
|
|
232
|
-
### From v2.0.x to v2.1.x
|
|
233
|
-
|
|
234
|
-
The default behavior remains unchanged - existing code will continue to work:
|
|
235
|
-
|
|
236
|
-
```javascript
|
|
237
|
-
// This still works exactly as before
|
|
238
|
-
const result = mimeHtml({ html: htmlContent });
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
To leverage new features:
|
|
242
|
-
|
|
243
|
-
```javascript
|
|
244
|
-
// Before - only sync available
|
|
245
|
-
const result = mimeHtml({ html: htmlContent });
|
|
246
|
-
|
|
247
|
-
// After - use async for better protection
|
|
248
|
-
const result = await mimeHtml.async({ html: htmlContent }, { timeout: 5000 });
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### For High-Volume Processing
|
|
252
|
-
|
|
253
|
-
```javascript
|
|
254
|
-
// Before - sequential processing
|
|
255
|
-
for (const email of emails) {
|
|
256
|
-
const result = mimeHtml({ html: email.html });
|
|
257
|
-
// Process result
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// After - concurrent with worker pool
|
|
261
|
-
const results = await Promise.all(emails.map(email => mimeHtml.async({ html: email.html }, { useWorkerPool: true })));
|
|
262
|
-
```
|
package/eslint.config.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Shared lint baseline for the Postal Systems / Nodemailer packages: eslint:recommended
|
|
4
|
-
// plus the rules of the retired eslint-config-nodemailer, with formatting left to Prettier.
|
|
5
|
-
// Keep this file identical across repos except for the "Repo-specific" block at the end.
|
|
6
|
-
|
|
7
|
-
const js = require('@eslint/js');
|
|
8
|
-
const prettier = require('eslint-config-prettier');
|
|
9
|
-
const globals = require('globals');
|
|
10
|
-
|
|
11
|
-
module.exports = [
|
|
12
|
-
{
|
|
13
|
-
ignores: ['node_modules/**', 'coverage/**', 'dist/**']
|
|
14
|
-
},
|
|
15
|
-
js.configs.recommended,
|
|
16
|
-
{
|
|
17
|
-
files: ['**/*.js', '**/*.cjs'],
|
|
18
|
-
languageOptions: {
|
|
19
|
-
ecmaVersion: 2022,
|
|
20
|
-
sourceType: 'commonjs',
|
|
21
|
-
globals: { ...globals.node }
|
|
22
|
-
},
|
|
23
|
-
rules: {
|
|
24
|
-
'no-await-in-loop': 'error',
|
|
25
|
-
'no-div-regex': 'error',
|
|
26
|
-
strict: ['error', 'global'],
|
|
27
|
-
eqeqeq: 'error',
|
|
28
|
-
'dot-notation': 'error',
|
|
29
|
-
curly: 'error',
|
|
30
|
-
'no-unused-expressions': ['error', { allowShortCircuit: true }],
|
|
31
|
-
'no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
|
|
32
|
-
'handle-callback-err': 'error',
|
|
33
|
-
'no-new': 'error',
|
|
34
|
-
'new-cap': 'error',
|
|
35
|
-
'no-eval': 'error',
|
|
36
|
-
'no-invalid-this': 'error',
|
|
37
|
-
radix: ['error', 'always'],
|
|
38
|
-
'no-use-before-define': ['error', 'nofunc'],
|
|
39
|
-
'callback-return': ['error', ['callback', 'cb', 'done']],
|
|
40
|
-
'no-redeclare': 'error',
|
|
41
|
-
'block-scoped-var': 'error',
|
|
42
|
-
'no-sequences': 'error',
|
|
43
|
-
'no-throw-literal': 'error',
|
|
44
|
-
'no-useless-call': 'error',
|
|
45
|
-
'no-useless-concat': 'error',
|
|
46
|
-
'no-void': 'error',
|
|
47
|
-
yoda: 'error',
|
|
48
|
-
'no-var': 'error',
|
|
49
|
-
'no-bitwise': 'error',
|
|
50
|
-
'no-lonely-if': 'error',
|
|
51
|
-
'arrow-body-style': ['error', 'as-needed'],
|
|
52
|
-
'prefer-arrow-callback': 'error',
|
|
53
|
-
'object-shorthand': 'error',
|
|
54
|
-
'prefer-spread': 'error',
|
|
55
|
-
'no-prototype-builtins': 'off'
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
files: ['test/**'],
|
|
60
|
-
languageOptions: {
|
|
61
|
-
globals: { ...globals.node, ...globals.mocha }
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
// Repo-specific: overrides go here, each with a one-line reason
|
|
65
|
-
{
|
|
66
|
-
// Benchmarks time each iteration sequentially on purpose (9 violations, all in benchmark/)
|
|
67
|
-
files: ['benchmark/**'],
|
|
68
|
-
rules: { 'no-await-in-loop': 'off' }
|
|
69
|
-
},
|
|
70
|
-
prettier
|
|
71
|
-
];
|