@altano/html-cdnify 1.0.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/README.md +398 -0
- package/assets/logo.png +0 -0
- package/dist/CDNTransformer.d.ts +25 -0
- package/dist/CDNTransformer.js +9 -0
- package/dist/HtmlAttributeStreamTransformer.d.ts +36 -0
- package/dist/HtmlAttributeStreamTransformer.js +7 -0
- package/dist/HtmlTransformer.d.ts +18 -0
- package/dist/HtmlTransformer.js +6 -0
- package/dist/cdnify.d.ts +15 -0
- package/dist/cdnify.js +23 -0
- package/dist/chunk-HS44D3X6.js +132 -0
- package/dist/chunk-P5I4BMT3.js +25 -0
- package/dist/chunk-Z7NH4HT6.js +84 -0
- package/dist/chunk-ZHBDQZLT.js +32 -0
- package/dist/urlConverter.d.ts +6 -0
- package/dist/urlConverter.js +6 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/html-cdnify)  
|
|
4
|
+
|
|
5
|
+
Transform the relative URLs in your HTML markup (e.g. scripts, stylesheets, images) to use your CDN URL.
|
|
6
|
+
|
|
7
|
+
- Uses a real HTML parser, [trumpet](https://www.npmjs.com/package/trumpet), not regular expressions.
|
|
8
|
+
- Doesn't require changes to your HTML, is a purely post-process transformation.
|
|
9
|
+
- Supports relative URLs in your HTML.
|
|
10
|
+
- Will perform minimal modifications to your HTML, only modifying the element that is having an attribute cdnified.
|
|
11
|
+
- Like [grunt-cdnify](https://www.npmjs.com/package/grunt-cdnify) but usable with or without grunt.
|
|
12
|
+
- Like [cdnify](https://www.npmjs.com/package/cdnify) but you don't have to mark up your HTML with magic attributes.
|
|
13
|
+
- This library's scope is limited to HTML and is not meant to process CSS.
|
|
14
|
+
|
|
15
|
+
# Simplest usage
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
19
|
+
|
|
20
|
+
cdnify({
|
|
21
|
+
cdnUrl: "//cdn.com",
|
|
22
|
+
buffer: `<img src="/face.png">`,
|
|
23
|
+
})
|
|
24
|
+
.then((buffer) => buffer.toString())
|
|
25
|
+
.then((output) => console.log(output));
|
|
26
|
+
|
|
27
|
+
// Output:
|
|
28
|
+
// <img src="//cdn.com/face.png">
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
# What gets cdnified
|
|
32
|
+
|
|
33
|
+
Any URLs where only the path (and after) is specified which are found in the following locations will be cdnified:
|
|
34
|
+
|
|
35
|
+
<img data-src="____">
|
|
36
|
+
<img src="____">
|
|
37
|
+
<img srcset="____">
|
|
38
|
+
<video poster="____">
|
|
39
|
+
<script src="____">
|
|
40
|
+
<source src="____">
|
|
41
|
+
<link rel="apple-touch-icon" href="____">
|
|
42
|
+
<link rel="icon" href="____">
|
|
43
|
+
<link rel="shortcut icon" href="____">
|
|
44
|
+
<link rel="stylesheet" href="____">
|
|
45
|
+
|
|
46
|
+
## Will be cdnified
|
|
47
|
+
|
|
48
|
+
- `<img src="/logo.png">`
|
|
49
|
+
- `<script src="jquery.js"></script>`
|
|
50
|
+
- `<link rel="stylesheet" href="main.css"></script>`
|
|
51
|
+
|
|
52
|
+
## Won't be cdnified
|
|
53
|
+
|
|
54
|
+
| HTMLElement | Reason not cdnified |
|
|
55
|
+
| ------------------------------------- | ------------------------------------------- |
|
|
56
|
+
| `<img src="http://foo.com/logo.png">` | Absolute URL specified |
|
|
57
|
+
| `<img src="//foo.com/logo.png">` | Scheme-relative/agnostic URL specified |
|
|
58
|
+
| `<img custom="/logo.png">` | "custom" attribute not among those cdnified |
|
|
59
|
+
|
|
60
|
+
Adding the `data-cdn-ignore` attribute to any element will cause the element to be skipped during cdnification, and the `data-cdn-ignore` attribute will be removed to clean up your HTML.
|
|
61
|
+
|
|
62
|
+
# More Examples
|
|
63
|
+
|
|
64
|
+
## Using `bufferPath` to deal with subdirectories
|
|
65
|
+
|
|
66
|
+
You can specify `bufferPath`, the relative path to the buffer being processed. If this isn't specified, all resources with relative paths will be assumed to be at the root of the domain before being cdnified.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
70
|
+
|
|
71
|
+
var input = `
|
|
72
|
+
<img src="figure1.png">
|
|
73
|
+
<img src="articleSubDirectory/figure2.png">
|
|
74
|
+
<img src="/images/logo.png">
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
var outputPromise = cdnify({
|
|
78
|
+
cdnUrl: "//cdn.com",
|
|
79
|
+
bufferPath: "articles/article1/index.html",
|
|
80
|
+
buffer: input,
|
|
81
|
+
}).then((buffer) => buffer.toString());
|
|
82
|
+
|
|
83
|
+
outputPromise.then((output) => console.log(output));
|
|
84
|
+
|
|
85
|
+
// Output:
|
|
86
|
+
// <img src="//cdn.com/articles/article1/figure1.png">
|
|
87
|
+
// <img src="//cdn.com/articles/article1/articleSubDirectory/figure2.png">
|
|
88
|
+
// <img src="//cdn.com/images/logo.png">
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
_Without_ `bufferPath`, the output would be:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
// Output:
|
|
95
|
+
// <img src="//cdn.com/figure1.png">
|
|
96
|
+
// <img src="//cdn.com/articleSubDirectory/figure2.png">
|
|
97
|
+
// <img src="//cdn.com/images/logo.png">
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If your CDN has a path specified, it is assumed that both relative URLs AND root-relative URLs in your HTML are relative to the CDN directory. In other words, the CDN subdirectory will always be in the final URL. For example:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
104
|
+
|
|
105
|
+
var input = `
|
|
106
|
+
<img src="figure1.png">
|
|
107
|
+
<img src="articleSubDirectory/figure2.png">
|
|
108
|
+
<img src="/images/logo.png">
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
var outputPromise = cdnify({
|
|
112
|
+
cdnUrl: "//cdn.com/sub/directory/in/cdn/is/always/present",
|
|
113
|
+
bufferPath: "article/index.html",
|
|
114
|
+
buffer: input,
|
|
115
|
+
}).then((buffer) => buffer.toString());
|
|
116
|
+
|
|
117
|
+
outputPromise.then((output) => console.log(output));
|
|
118
|
+
|
|
119
|
+
// Output:
|
|
120
|
+
// <img src="//cdn.com/sub/directory/in/cdn/is/always/present/article/figure1.png">
|
|
121
|
+
// <img src="//cdn.com/sub/directory/in/cdn/is/always/present/article/articleSubDirectory/figure2.png">
|
|
122
|
+
// <img src="//cdn.com/sub/directory/in/cdn/is/always/present/images/logo.png">
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
It should usually be the expectation that a CDN subdir is always present in the cdnified result. If you need to specify a CDN subdirectory AND you need to sometimes escape URLs in your HTML to the root of the CDN domain, you'll have to write a custom transformFunction.
|
|
126
|
+
|
|
127
|
+
## Using the underlying stream instead of a Promise
|
|
128
|
+
|
|
129
|
+
If you are dealing with large files or network IO and would like to use the underlying NodeJS Transform Stream rather than buffering all of the input and output, feel free:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
var streamifier = require("streamifier");
|
|
133
|
+
var CDNTransformer = require("html-cdnify").CDNTransformer;
|
|
134
|
+
|
|
135
|
+
var transformer = new CDNTransformer({
|
|
136
|
+
cdnUrl: "http://cdn.com",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
var outputStream = streamifier
|
|
140
|
+
.createReadStream(
|
|
141
|
+
`<img src="face1.png">
|
|
142
|
+
<img src="face2.png">
|
|
143
|
+
<img src="face3.png">`,
|
|
144
|
+
)
|
|
145
|
+
.pipe(transformer.stream);
|
|
146
|
+
|
|
147
|
+
outputStream.pipe(process.stdout);
|
|
148
|
+
|
|
149
|
+
// Output:
|
|
150
|
+
// <img src="http://cdn.com/face1.png">
|
|
151
|
+
// <img src="http://cdn.com/face2.png">
|
|
152
|
+
// <img src="http://cdn.com/face3.png">
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Specifying a transform defintion to add `<custom-element src="____">` to the list of attributes to be cdnified
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
159
|
+
|
|
160
|
+
var outputPromise = cdnify({
|
|
161
|
+
cdnUrl: "//cdn.com/cdn/",
|
|
162
|
+
transformDefinitions: [
|
|
163
|
+
{
|
|
164
|
+
selector: "custom-element[src]",
|
|
165
|
+
attribute: "src",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
buffer: `<custom-element src="/face7.png">`,
|
|
169
|
+
}).then((buffer) => buffer.toString());
|
|
170
|
+
|
|
171
|
+
outputPromise.then((output) => console.log(output));
|
|
172
|
+
|
|
173
|
+
// Output:
|
|
174
|
+
// <custom-element src="//cdn.com/cdn/face7.png">
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Specifying a custom transform function
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
181
|
+
|
|
182
|
+
var outputPromise = cdnify({
|
|
183
|
+
cdnUrl: "//cdn.com",
|
|
184
|
+
transformFunction: (cdnUrl, oldUrl, bufferPath) => {
|
|
185
|
+
return cdnUrl + "/subdir" + oldUrl;
|
|
186
|
+
},
|
|
187
|
+
buffer: `<img src="/logo.png">`,
|
|
188
|
+
}).then((buffer) => buffer.toString());
|
|
189
|
+
|
|
190
|
+
outputPromise.then((output) => console.log(output));
|
|
191
|
+
|
|
192
|
+
// Output:
|
|
193
|
+
// <img src="//cdn.com/subdir/logo.png">
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Specifying a custom transform function that delegates to the default
|
|
197
|
+
|
|
198
|
+
If your assets are on different CDNs, you might need to write something like:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
202
|
+
var CDNTransformer = require("html-cdnify").CDNTransformer;
|
|
203
|
+
|
|
204
|
+
var outputPromise = cdnify({
|
|
205
|
+
cdnUrl: "//cdn.com",
|
|
206
|
+
transformFunction: (cdnUrl, oldUrl, bufferPath) => {
|
|
207
|
+
if (oldUrl.endsWith(".png")) {
|
|
208
|
+
var customCdnBaseUrl = "//imagecdn.com";
|
|
209
|
+
} else {
|
|
210
|
+
var customCdnBaseUrl = "//assetcdn.com";
|
|
211
|
+
}
|
|
212
|
+
return CDNTransformer.defaultTransformFunction(customCdnBaseUrl, oldUrl, bufferPath);
|
|
213
|
+
},
|
|
214
|
+
buffer: `<img src="logo.png"><script src="main.js"></script>`,
|
|
215
|
+
}).then((buffer) => buffer.toString());
|
|
216
|
+
|
|
217
|
+
outputPromise.then((output) => console.log(output));
|
|
218
|
+
|
|
219
|
+
// Output:
|
|
220
|
+
// <img src="//imagecdn.com/logo.png"><script src="//assetcdn.com/main.js"></script>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Specifying a custom attributeParser
|
|
224
|
+
|
|
225
|
+
A custom attributeParser is like a custom transformFunction, but is scoped to just one selector rather than being applied to every attribute transformation. For example, to uppercase only PNG image names before cdnifying:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
229
|
+
|
|
230
|
+
var outputPromise = cdnify({
|
|
231
|
+
cdnUrl: "//cdn.com",
|
|
232
|
+
transformDefinitions: [
|
|
233
|
+
{
|
|
234
|
+
selector: `img[src$="png"]`,
|
|
235
|
+
attribute: "src",
|
|
236
|
+
attributeParser: (oldAttribute, transformFunction) => transformFunction(oldAttribute.toUpperCase()),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
buffer: `<img src="/logo.gif"><img src="/logo.png">`,
|
|
240
|
+
}).then((buffer) => buffer.toString());
|
|
241
|
+
|
|
242
|
+
outputPromise.then((output) => console.log(output));
|
|
243
|
+
|
|
244
|
+
// Output:
|
|
245
|
+
// <img src="//cdn.com/logo.gif"><img src="//cdn.com/LOGO.PNG">
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Overriding an existing transform
|
|
249
|
+
|
|
250
|
+
The default transformDefinitions array is:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
[
|
|
254
|
+
{
|
|
255
|
+
selector: `video[poster]:not([data-cdn-ignore])`,
|
|
256
|
+
attribute: "poster",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
selector: `img[data-src]:not([data-cdn-ignore])`,
|
|
260
|
+
attribute: "data-src",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
selector: `script[src]:not([data-cdn-ignore])`,
|
|
264
|
+
attribute: "src",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
selector: `source[src]:not([data-cdn-ignore])`,
|
|
268
|
+
attribute: "src",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
selector: `link[rel="apple-touch-icon"]:not([data-cdn-ignore])`,
|
|
272
|
+
attribute: "href",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
selector: `link[rel="icon"]:not([data-cdn-ignore])`,
|
|
276
|
+
attribute: "href",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
selector: `link[rel="shortcut icon"]:not([data-cdn-ignore])`,
|
|
280
|
+
attribute: "href",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
selector: `link[rel="stylesheet"]:not([data-cdn-ignore])`,
|
|
284
|
+
attribute: "href",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
selector: `img[src]:not([data-cdn-ignore])`,
|
|
288
|
+
attribute: "src",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
selector: `img[srcset]:not([data-cdn-ignore])`,
|
|
292
|
+
attribute: "srcset",
|
|
293
|
+
attributeParser: (attr, transformFunction) => {
|
|
294
|
+
return attr
|
|
295
|
+
.split(",")
|
|
296
|
+
.map((imgInfo) => imgInfo.replace(/([^ ]+)/, transformFunction))
|
|
297
|
+
.join(",");
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
If you'd like to ovverride any of these, specify the same selector and attribute but a different attributeParser. For example:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
307
|
+
|
|
308
|
+
var noopFn = (oldAttribute) => oldAttribute;
|
|
309
|
+
|
|
310
|
+
var outputPromise = cdnify({
|
|
311
|
+
cdnUrl: "//cdn.com",
|
|
312
|
+
transformDefinitions: [
|
|
313
|
+
{
|
|
314
|
+
selector: "img[src]:not([data-cdn-ignore])",
|
|
315
|
+
attribute: "src",
|
|
316
|
+
attributeParser: noopFn, // pass-through attribute without modification
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
buffer: `<img src="logo.gif">`,
|
|
320
|
+
}).then((buffer) => buffer.toString());
|
|
321
|
+
|
|
322
|
+
outputPromise.then((output) => console.log(output));
|
|
323
|
+
|
|
324
|
+
// Output:
|
|
325
|
+
// <img src="logo.gif">
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
# API
|
|
329
|
+
|
|
330
|
+
## Promise-based API (recommended for simplicity)
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
var cdnify = require("html-cdnify").cdnify;
|
|
334
|
+
|
|
335
|
+
cdnify({ ... Options ...}) => Promise<Buffer>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Options
|
|
339
|
+
|
|
340
|
+
Same options as stream-based API (see below), plus:
|
|
341
|
+
|
|
342
|
+
| Property | Type | Optional | Description |
|
|
343
|
+
| -------- | ---------------- | ---------------------------------------- | ----------- |
|
|
344
|
+
| buffer | Buffer or string | The content that is going to be cdnified |
|
|
345
|
+
|
|
346
|
+
## Stream-based API
|
|
347
|
+
|
|
348
|
+
use the stream-based API when you are dealing with large files or network IO and would like to use the underlying NodeJS Transform Stream rather than buffering all of the input and output. The Promisebased API is simpler but the input/output must be completely buffered, which is obviously fine most of the time.
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
var CDNTransformer = require("html-cdnify").CDNTransformer;
|
|
352
|
+
|
|
353
|
+
var transformer = new CDNTransformer({ ... Options ... }) => CDNTransformer
|
|
354
|
+
transformer.stream => NodeJS TransformStream
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Options
|
|
358
|
+
|
|
359
|
+
| Property | Type | Optional | Description |
|
|
360
|
+
| -------------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
361
|
+
| cdnUrl | string | | The absolute (or scheme-relative/agnostic) URL to your CDN. |
|
|
362
|
+
| bufferPath | string | Y | The relative path to the buffer being processed. If this isn't specified, all resources with relative paths will be assumed to be at the root of the domain before being cdnified. |
|
|
363
|
+
| transformDefinitions | TransformDefinition | Y | See below... |
|
|
364
|
+
| transformFunction | CDNTransformFunction | Y | See below... |
|
|
365
|
+
|
|
366
|
+
### TransformDefinition
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
interface TransformDefinition {
|
|
370
|
+
selector: string;
|
|
371
|
+
attribute: string;
|
|
372
|
+
attributeParser?: (oldAttribute: string, transformFunction: TransformFunction) => string;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
The default attributeParser when one isn't specified is:
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
HtmlAttributeStreamTransformerOptions.attributeParsers = {
|
|
380
|
+
default: (attr, transformFunction) => transformFunction(attr)
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### CDNTransformFunction
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
(cdnUrl: string, oldUrl: string, bufferPath: string) => string;
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### TransformFunction
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
(oldAttribute: string) => string;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
Shield (in logo) by [Flaticon](http://www.flaticon.com) from [Freepik](http://www.freepik.com)
|
package/assets/logo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TransformDefinition, HtmlAttributeStreamTransformer } from './HtmlAttributeStreamTransformer.js';
|
|
2
|
+
import './HtmlTransformer.js';
|
|
3
|
+
import 'trumpet';
|
|
4
|
+
|
|
5
|
+
type CDNTransformFunction = (cdnUrl: string, oldUrl: string, bufferPath: string) => string;
|
|
6
|
+
interface CDNTransformerOptions {
|
|
7
|
+
cdnUrl: string;
|
|
8
|
+
bufferPath?: string;
|
|
9
|
+
transformDefinitions?: TransformDefinition[];
|
|
10
|
+
transformFunction?: CDNTransformFunction;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a node-trumpet Transform stream, w/ HTML as input and
|
|
14
|
+
* HTML as output. The output will have all non-absolute-URL
|
|
15
|
+
* attributes that identify CDN-able resources converted to
|
|
16
|
+
* use the given CDN URL.
|
|
17
|
+
*/
|
|
18
|
+
declare class CDNTransformer extends HtmlAttributeStreamTransformer {
|
|
19
|
+
#private;
|
|
20
|
+
cdnOptions: Required<CDNTransformerOptions>;
|
|
21
|
+
constructor(cdnOptions: CDNTransformerOptions);
|
|
22
|
+
static defaultTransformFunction: (newUrlBase: string, oldUrl: string, pathOldUrlIsRelativeTo?: string | undefined) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { CDNTransformFunction, CDNTransformer, CDNTransformerOptions };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import HtmlTransformer from './HtmlTransformer.js';
|
|
2
|
+
import 'trumpet';
|
|
3
|
+
|
|
4
|
+
interface HtmlAttributeStreamTransformerOptions {
|
|
5
|
+
transformDefinitions: TransformDefinition[];
|
|
6
|
+
transformFunction: TransformFunction;
|
|
7
|
+
attributeToMarkElementToBeIgnored?: string;
|
|
8
|
+
}
|
|
9
|
+
interface TransformDefinition {
|
|
10
|
+
selector: string;
|
|
11
|
+
attribute: string;
|
|
12
|
+
attributeParser?: (oldAttribute: string, transformFunction: TransformFunction) => string;
|
|
13
|
+
}
|
|
14
|
+
type TransformFunction = (oldAttribute: string) => string;
|
|
15
|
+
interface AttributeParser {
|
|
16
|
+
(oldAttribute: string, transformFunction: TransformFunction): string;
|
|
17
|
+
}
|
|
18
|
+
interface AttributeParsers {
|
|
19
|
+
[index: string]: AttributeParser;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wraps a node-trumpet Transform stream, w/ HTML as input and
|
|
23
|
+
* HTML as output. The output will have all non-absolute-URL
|
|
24
|
+
* attributes that identify CDN-able resources converted to
|
|
25
|
+
* use the given CDN URL.
|
|
26
|
+
*/
|
|
27
|
+
declare class HtmlAttributeStreamTransformer extends HtmlTransformer {
|
|
28
|
+
options: HtmlAttributeStreamTransformerOptions;
|
|
29
|
+
condensedTransformOptions: TransformDefinition[];
|
|
30
|
+
constructor(options: HtmlAttributeStreamTransformerOptions);
|
|
31
|
+
static attributeParsers: AttributeParsers;
|
|
32
|
+
protected applyTransforms(): void;
|
|
33
|
+
private processTransforms;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { AttributeParser, AttributeParsers, HtmlAttributeStreamTransformer, HtmlAttributeStreamTransformerOptions, TransformDefinition, TransformFunction };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Trumpet } from 'trumpet';
|
|
2
|
+
|
|
3
|
+
interface TrumpetTransformStream extends Trumpet {
|
|
4
|
+
}
|
|
5
|
+
declare class HtmlTransformer {
|
|
6
|
+
private transformStream;
|
|
7
|
+
constructor();
|
|
8
|
+
/**
|
|
9
|
+
* Get the underlying transform stream
|
|
10
|
+
*/
|
|
11
|
+
get stream(): Trumpet;
|
|
12
|
+
/**
|
|
13
|
+
* Get a promise to the complete output Buffer (alternative API to dealing with stream)
|
|
14
|
+
*/
|
|
15
|
+
get outputBufferPromise(): Promise<Buffer>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { TrumpetTransformStream, HtmlTransformer as default };
|
package/dist/cdnify.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CDNTransformerOptions } from './CDNTransformer.js';
|
|
2
|
+
export { CDNTransformFunction, CDNTransformer } from './CDNTransformer.js';
|
|
3
|
+
import './HtmlAttributeStreamTransformer.js';
|
|
4
|
+
import './HtmlTransformer.js';
|
|
5
|
+
import 'trumpet';
|
|
6
|
+
|
|
7
|
+
interface CdnifyOptions extends CDNTransformerOptions {
|
|
8
|
+
buffer: string | Buffer;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Alternative wrapper for simple Promise-based use of library (as opposed to transform stream)
|
|
12
|
+
*/
|
|
13
|
+
declare function cdnify(options: CdnifyOptions): Promise<Buffer>;
|
|
14
|
+
|
|
15
|
+
export { CDNTransformerOptions, CdnifyOptions, cdnify, cdnify as default };
|
package/dist/cdnify.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CDNTransformer
|
|
3
|
+
} from "./chunk-HS44D3X6.js";
|
|
4
|
+
import "./chunk-Z7NH4HT6.js";
|
|
5
|
+
import "./chunk-P5I4BMT3.js";
|
|
6
|
+
import "./chunk-ZHBDQZLT.js";
|
|
7
|
+
|
|
8
|
+
// src/cdnify.ts
|
|
9
|
+
import streamifier from "streamifier";
|
|
10
|
+
function cdnify(options) {
|
|
11
|
+
if (typeof options.buffer !== "string") {
|
|
12
|
+
options.buffer = options.buffer.toString();
|
|
13
|
+
}
|
|
14
|
+
const cdnTransformer = new CDNTransformer(options);
|
|
15
|
+
streamifier.createReadStream(options.buffer).pipe(cdnTransformer.stream);
|
|
16
|
+
return cdnTransformer.outputBufferPromise;
|
|
17
|
+
}
|
|
18
|
+
var cdnify_default = cdnify;
|
|
19
|
+
export {
|
|
20
|
+
CDNTransformer,
|
|
21
|
+
cdnify,
|
|
22
|
+
cdnify_default as default
|
|
23
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HtmlAttributeStreamTransformer
|
|
3
|
+
} from "./chunk-Z7NH4HT6.js";
|
|
4
|
+
import {
|
|
5
|
+
urlConverter_default
|
|
6
|
+
} from "./chunk-ZHBDQZLT.js";
|
|
7
|
+
|
|
8
|
+
// src/CDNTransformer.ts
|
|
9
|
+
import { unionBy, merge, mergeWith } from "lodash";
|
|
10
|
+
var OptionsMerger = class _OptionsMerger {
|
|
11
|
+
static isTransformDefinition(a) {
|
|
12
|
+
return a != null && typeof a === "object" && "selector" in a && "attribute" in a && typeof a.selector === "string" && typeof a.selector === "string" && typeof a.attribute === "string";
|
|
13
|
+
}
|
|
14
|
+
static isTransformDefinitions(a) {
|
|
15
|
+
return Array.isArray(a) && a.length > 0 && _OptionsMerger.isTransformDefinition(a[0]);
|
|
16
|
+
}
|
|
17
|
+
static mergeCustomizer = (objValue, srcValue) => {
|
|
18
|
+
if (_OptionsMerger.isTransformDefinitions(objValue) || _OptionsMerger.isTransformDefinitions(srcValue)) {
|
|
19
|
+
const result = unionBy(
|
|
20
|
+
srcValue,
|
|
21
|
+
objValue,
|
|
22
|
+
// reverse order to allow srcValue to take precedence
|
|
23
|
+
(value) => `${value.selector} / ${value.attribute}`
|
|
24
|
+
);
|
|
25
|
+
return result;
|
|
26
|
+
} else {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
static merge(target, source1, source2, source3) {
|
|
31
|
+
return mergeWith(
|
|
32
|
+
target,
|
|
33
|
+
source1,
|
|
34
|
+
source2,
|
|
35
|
+
source3,
|
|
36
|
+
_OptionsMerger.mergeCustomizer
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var CDNTransformer = class _CDNTransformer extends HtmlAttributeStreamTransformer {
|
|
41
|
+
cdnOptions;
|
|
42
|
+
constructor(cdnOptions) {
|
|
43
|
+
super(
|
|
44
|
+
OptionsMerger.merge(
|
|
45
|
+
{},
|
|
46
|
+
_CDNTransformer.#defaultHtmlAttributeStreamTransformerOptions,
|
|
47
|
+
{
|
|
48
|
+
transformDefinitions: [],
|
|
49
|
+
transformFunction: (attribute) => this.#cdnifyAttribute(attribute)
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
transformDefinitions: cdnOptions.transformDefinitions
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
this.cdnOptions = merge(
|
|
57
|
+
{
|
|
58
|
+
transformDefinitions: []
|
|
59
|
+
},
|
|
60
|
+
_CDNTransformer.#defaultCdnOptions,
|
|
61
|
+
cdnOptions
|
|
62
|
+
);
|
|
63
|
+
if (typeof this.cdnOptions.cdnUrl !== "string") {
|
|
64
|
+
throw new Error(`Invalid cdnUrl "${this.cdnOptions.cdnUrl}" specified.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
#cdnifyAttribute(oldUrl) {
|
|
68
|
+
return this.cdnOptions.transformFunction(
|
|
69
|
+
this.cdnOptions.cdnUrl,
|
|
70
|
+
oldUrl,
|
|
71
|
+
this.cdnOptions.bufferPath
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
static #defaultCdnOptions = {
|
|
75
|
+
bufferPath: ".",
|
|
76
|
+
transformFunction: urlConverter_default
|
|
77
|
+
};
|
|
78
|
+
static defaultTransformFunction = _CDNTransformer.#defaultCdnOptions.transformFunction;
|
|
79
|
+
static #defaultHtmlAttributeStreamTransformerOptions = {
|
|
80
|
+
attributeToMarkElementToBeIgnored: "data-cdn-ignore",
|
|
81
|
+
// inspired by https://github.com/callumlocke/grunt-cdnify/blob/master/tasks/cdnify.js#L31
|
|
82
|
+
transformDefinitions: [
|
|
83
|
+
{
|
|
84
|
+
selector: `video[poster]:not([data-cdn-ignore])`,
|
|
85
|
+
attribute: "poster"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
selector: `img[data-src]:not([data-cdn-ignore])`,
|
|
89
|
+
attribute: "data-src"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
selector: `script[src]:not([data-cdn-ignore])`,
|
|
93
|
+
attribute: "src"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
selector: `source[src]:not([data-cdn-ignore])`,
|
|
97
|
+
attribute: "src"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
selector: `link[rel="apple-touch-icon"]:not([data-cdn-ignore])`,
|
|
101
|
+
attribute: "href"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
selector: `link[rel="icon"]:not([data-cdn-ignore])`,
|
|
105
|
+
attribute: "href"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
selector: `link[rel="shortcut icon"]:not([data-cdn-ignore])`,
|
|
109
|
+
attribute: "href"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
selector: `link[rel="stylesheet"]:not([data-cdn-ignore])`,
|
|
113
|
+
attribute: "href"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
selector: `img[src]:not([data-cdn-ignore])`,
|
|
117
|
+
attribute: "src"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
selector: `img[srcset]:not([data-cdn-ignore])`,
|
|
121
|
+
attribute: "srcset",
|
|
122
|
+
attributeParser(attr, transformFunction) {
|
|
123
|
+
return attr.split(",").map((imgInfo) => imgInfo.replace(/([^ ]+)/, transformFunction)).join(",");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
CDNTransformer
|
|
132
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/HtmlTransformer.ts
|
|
2
|
+
import trumpet from "trumpet";
|
|
3
|
+
import streamToPromise from "stream-to-promise";
|
|
4
|
+
var HtmlTransformer = class {
|
|
5
|
+
transformStream;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.transformStream = trumpet();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get the underlying transform stream
|
|
11
|
+
*/
|
|
12
|
+
get stream() {
|
|
13
|
+
return this.transformStream;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get a promise to the complete output Buffer (alternative API to dealing with stream)
|
|
17
|
+
*/
|
|
18
|
+
get outputBufferPromise() {
|
|
19
|
+
return streamToPromise(this.stream);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
HtmlTransformer
|
|
25
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HtmlTransformer
|
|
3
|
+
} from "./chunk-P5I4BMT3.js";
|
|
4
|
+
|
|
5
|
+
// src/HtmlAttributeStreamTransformer.ts
|
|
6
|
+
import { groupBy } from "lodash";
|
|
7
|
+
var HtmlAttributeStreamTransformer = class _HtmlAttributeStreamTransformer extends HtmlTransformer {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super();
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.processTransforms();
|
|
12
|
+
this.applyTransforms();
|
|
13
|
+
}
|
|
14
|
+
condensedTransformOptions = [];
|
|
15
|
+
static attributeParsers = {
|
|
16
|
+
default: (attr, transformFunction) => transformFunction(attr)
|
|
17
|
+
};
|
|
18
|
+
applyTransforms() {
|
|
19
|
+
this.condensedTransformOptions.forEach((settings) => {
|
|
20
|
+
const selector = settings.selector;
|
|
21
|
+
const attribute = settings.attribute;
|
|
22
|
+
const parser = settings.attributeParser;
|
|
23
|
+
this.stream.selectAll(selector, (elem) => {
|
|
24
|
+
const oldAttributeValue = elem.getAttribute(attribute);
|
|
25
|
+
if (typeof oldAttributeValue === "string") {
|
|
26
|
+
const newAttributeValue = parser && parser(oldAttributeValue, this.options.transformFunction);
|
|
27
|
+
if (newAttributeValue && newAttributeValue !== oldAttributeValue) {
|
|
28
|
+
elem.setAttribute(attribute, newAttributeValue);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const { attributeToMarkElementToBeIgnored } = this.options;
|
|
34
|
+
if (attributeToMarkElementToBeIgnored) {
|
|
35
|
+
this.stream.selectAll(
|
|
36
|
+
`[${attributeToMarkElementToBeIgnored}]`,
|
|
37
|
+
(elem) => {
|
|
38
|
+
elem.removeAttribute(attributeToMarkElementToBeIgnored);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
processTransforms() {
|
|
44
|
+
this.options.transformDefinitions.forEach((option) => {
|
|
45
|
+
if (!option.attributeParser) {
|
|
46
|
+
option.attributeParser = _HtmlAttributeStreamTransformer.attributeParsers["default"];
|
|
47
|
+
}
|
|
48
|
+
if (typeof option.selector !== "string") {
|
|
49
|
+
throw new Error(`Invalid selector "${option.selector}" specified.`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof option.attribute !== "string") {
|
|
52
|
+
throw new Error(`Invalid attribute "${option.attribute}" specified.`);
|
|
53
|
+
}
|
|
54
|
+
if (!(option.attributeParser instanceof Function)) {
|
|
55
|
+
throw new Error("Invalid attributeParser specified.");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const groupedTransformOptions = groupBy(
|
|
59
|
+
this.options.transformDefinitions,
|
|
60
|
+
(obj) => {
|
|
61
|
+
return [obj.attribute, obj.attributeParser];
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
this.condensedTransformOptions = Object.keys(groupedTransformOptions).map(
|
|
65
|
+
(key) => {
|
|
66
|
+
const likeTransformOptions = groupedTransformOptions[key];
|
|
67
|
+
if (likeTransformOptions == null || likeTransformOptions[0] == null) {
|
|
68
|
+
throw new Error(`No likeTransformOptions`);
|
|
69
|
+
}
|
|
70
|
+
const attribute = likeTransformOptions[0].attribute;
|
|
71
|
+
const attributeParser = likeTransformOptions[0].attributeParser;
|
|
72
|
+
return {
|
|
73
|
+
attribute,
|
|
74
|
+
attributeParser,
|
|
75
|
+
selector: likeTransformOptions.map((likeTransformOption) => likeTransformOption.selector).join(", ")
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
HtmlAttributeStreamTransformer
|
|
84
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/urlConverter.ts
|
|
2
|
+
import url from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function isLocalPath(filePath) {
|
|
5
|
+
return typeof filePath === "string" && filePath.length > 0 && filePath.indexOf("//") === -1 && filePath.indexOf("data:") !== 0;
|
|
6
|
+
}
|
|
7
|
+
var urlConverter_default = (newUrlBase, oldUrl, pathOldUrlIsRelativeTo) => {
|
|
8
|
+
if (!isLocalPath(oldUrl)) {
|
|
9
|
+
return oldUrl;
|
|
10
|
+
} else {
|
|
11
|
+
oldUrl = oldUrl.trim();
|
|
12
|
+
let relativePath = ".";
|
|
13
|
+
if (pathOldUrlIsRelativeTo) {
|
|
14
|
+
const isDirAlready = pathOldUrlIsRelativeTo.endsWith("/");
|
|
15
|
+
const fileDirName = isDirAlready ? pathOldUrlIsRelativeTo : path.dirname(pathOldUrlIsRelativeTo) + "/";
|
|
16
|
+
relativePath = url.resolve(fileDirName, oldUrl);
|
|
17
|
+
} else {
|
|
18
|
+
relativePath = oldUrl;
|
|
19
|
+
}
|
|
20
|
+
if (relativePath.startsWith("/")) {
|
|
21
|
+
relativePath = relativePath.slice(1);
|
|
22
|
+
}
|
|
23
|
+
if (!newUrlBase.endsWith("/")) {
|
|
24
|
+
newUrlBase += "/";
|
|
25
|
+
}
|
|
26
|
+
return url.resolve(newUrlBase, relativePath);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
urlConverter_default
|
|
32
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@altano/html-cdnify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"description": "Transform the relative URLs in your HTML markup (e.g. scripts, stylesheets, images) to use your CDN URL.",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cdn",
|
|
11
|
+
"html",
|
|
12
|
+
"cdnify",
|
|
13
|
+
"cdnification"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"assets/logo.png",
|
|
17
|
+
"dist/**/*.js",
|
|
18
|
+
"dist/**/*.d.ts",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/altano/npm-packages",
|
|
24
|
+
"directory": "packages/html-cdnify"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/altano/npm-packages/tree/main/packages/html-cdnify",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Alan Norbauer",
|
|
29
|
+
"email": "altano@gmail.com",
|
|
30
|
+
"url": "https://alan.norbauer.com"
|
|
31
|
+
},
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"glob": "^10.3.3",
|
|
35
|
+
"lodash": "^4.17.21",
|
|
36
|
+
"stream-to-promise": "^3.0.0",
|
|
37
|
+
"streamifier": "^0.1.1",
|
|
38
|
+
"trumpet": "^1.7.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/glob": "^8.1.0",
|
|
42
|
+
"@types/lodash": "^4.14.197",
|
|
43
|
+
"@types/node": "^20.5.3",
|
|
44
|
+
"@types/stream-to-promise": "^2.2.1",
|
|
45
|
+
"@types/streamifier": "^0.1.0",
|
|
46
|
+
"eslint": "^8.47.0",
|
|
47
|
+
"prettier": "^3.0.2",
|
|
48
|
+
"tsup": "^7.2.0",
|
|
49
|
+
"typescript": "^5.1.6",
|
|
50
|
+
"vitest": "^0.34.2",
|
|
51
|
+
"eslint-config-altano": "0.0.0",
|
|
52
|
+
"@altano/tsconfig": "0.0.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=5.10.1"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"build:types": "tsc --emitDeclarationOnly --declaration",
|
|
63
|
+
"dev": "tsup --watch",
|
|
64
|
+
"format": "prettier --write src",
|
|
65
|
+
"lint": "eslint src/**/*.ts*",
|
|
66
|
+
"lint:timing": "TIMING=1 eslint src/**/*.ts*",
|
|
67
|
+
"lint:fix": "TIMING=1 eslint src/**/*.ts* --fix",
|
|
68
|
+
"clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
|
|
69
|
+
"test": "vitest --run"
|
|
70
|
+
}
|
|
71
|
+
}
|