@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 ADDED
@@ -0,0 +1,398 @@
1
+ ![logo](./assets/logo.png)
2
+
3
+ [![npm](https://badgen.net/npm/v/html-cdnify)](https://www.npmjs.com/package/html-cdnify) ![Typed with TypeScript](https://badgen.net/npm/types/html-cdnify) ![ESM only](https://badgen.net/badge/module/esm%20only?icon=js)
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)
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,9 @@
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
+ export {
8
+ CDNTransformer
9
+ };
@@ -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,7 @@
1
+ import {
2
+ HtmlAttributeStreamTransformer
3
+ } from "./chunk-Z7NH4HT6.js";
4
+ import "./chunk-P5I4BMT3.js";
5
+ export {
6
+ HtmlAttributeStreamTransformer
7
+ };
@@ -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 };
@@ -0,0 +1,6 @@
1
+ import {
2
+ HtmlTransformer
3
+ } from "./chunk-P5I4BMT3.js";
4
+ export {
5
+ HtmlTransformer as default
6
+ };
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Resolve oldUrl against pathOldUrlIsRelativeTo, and then prepend newUrlBase
3
+ */
4
+ declare const _default: (newUrlBase: string, oldUrl: string, pathOldUrlIsRelativeTo?: string) => string;
5
+
6
+ export { _default as default };
@@ -0,0 +1,6 @@
1
+ import {
2
+ urlConverter_default
3
+ } from "./chunk-ZHBDQZLT.js";
4
+ export {
5
+ urlConverter_default as default
6
+ };
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
+ }