html-to-gutenberg 4.2.8 → 4.2.10
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/.env.example +20 -0
- package/.eslintrc.json +35 -0
- package/.github/workflows/build.yml +26 -0
- package/.github/workflows/coverage.yml +26 -0
- package/.github/workflows/sync-npm.yml +154 -0
- package/.nyc_output/1f0406b8-bb70-495d-8f8a-521fdd81b500.json +1 -0
- package/.nyc_output/6390956f-4f8a-4adb-9256-4a1c7e34a52d.json +1 -0
- package/.nyc_output/processinfo/1f0406b8-bb70-495d-8f8a-521fdd81b500.json +1 -0
- package/.nyc_output/processinfo/6390956f-4f8a-4adb-9256-4a1c7e34a52d.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/@types.d.ts +3 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +198 -0
- package/coverage-demo.test.ts +8 -0
- package/dist/coverage-demo.test.js +10 -0
- package/dist/coverage-demo.test.js.map +1 -0
- package/dist/globals.js +24 -0
- package/dist/globals.js.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.js +166 -0
- package/dist/index.test.js.map +1 -0
- package/dist/package.json +130 -0
- package/dist/snapapi-screenshot.test.js +44 -0
- package/dist/snapapi-screenshot.test.js.map +1 -0
- package/dist/src/coverage-demo.js +7 -0
- package/dist/src/coverage-demo.js.map +1 -0
- package/dist/src/utils-extra.test.js +137 -0
- package/dist/src/utils-extra.test.js.map +1 -0
- package/dist/src/utils.test.js +65 -0
- package/dist/src/utils.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils.js +61 -0
- package/dist/utils.js.map +1 -0
- package/fetch-page-assets.test.ts +448 -0
- package/index.d.ts +173 -0
- package/index.js +628 -249
- package/index.test.ts +774 -0
- package/index.ts +155 -1530
- package/package.json +87 -15
- package/r2.js +163 -0
- package/readme.md +126 -72
- package/scripts/patch-fetch-page-assets.mjs +13 -0
- package/scripts/sync-from-npm.mjs +115 -0
- package/snapapi-screenshot.test.ts +46 -0
- package/src/coverage-demo.ts +3 -0
- package/src/utils-extra.test.ts +108 -0
- package/src/utils.test.ts +36 -0
- package/temp-block-test.js +19 -0
- package/tsconfig.json +25 -4
- package/utils.ts +56 -0
- package/vendor/fetch-page-assets/LICENSE.MD +21 -0
- package/vendor/fetch-page-assets/README.md +117 -0
- package/vendor/fetch-page-assets/index.js +362 -0
- package/vendor/fetch-page-assets/package.json +48 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import block from './index.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
describe('block screenshot integration', () => {
|
|
10
|
+
const testHtml = '<html><body><h1>SnapAPI Test</h1></body></html>';
|
|
11
|
+
const testOptions = {
|
|
12
|
+
name: 'SnapAPITestBlock',
|
|
13
|
+
prefix: 'wp',
|
|
14
|
+
category: 'common',
|
|
15
|
+
basePath: process.cwd(),
|
|
16
|
+
shouldSaveFiles: true,
|
|
17
|
+
generateIconPreview: true,
|
|
18
|
+
jsFiles: [],
|
|
19
|
+
cssFiles: [],
|
|
20
|
+
source: 'https://example.com',
|
|
21
|
+
};
|
|
22
|
+
const previewPath = path.join(
|
|
23
|
+
testOptions.basePath,
|
|
24
|
+
'snapapitestblock',
|
|
25
|
+
'preview.jpeg'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
after(function () {
|
|
29
|
+
if (fs.existsSync(previewPath)) {
|
|
30
|
+
fs.unlinkSync(previewPath);
|
|
31
|
+
// Use fs.rmSync for recursive directory removal in Node >=14
|
|
32
|
+
const dir = path.dirname(previewPath);
|
|
33
|
+
if (fs.existsSync(dir)) {
|
|
34
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should generate a preview.jpeg using SnapAPI', async function (this: Mocha.Context) {
|
|
40
|
+
this.timeout(15000); // Allow time for API call
|
|
41
|
+
await block(testHtml, testOptions);
|
|
42
|
+
expect(fs.existsSync(previewPath)).to.equal(true);
|
|
43
|
+
const stats = fs.statSync(previewPath);
|
|
44
|
+
expect(stats.size).to.be.greaterThan(1000); // Should not be empty
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import * as utils from '../utils';
|
|
3
|
+
|
|
4
|
+
describe('hasTailwindCdnSource edge cases', () => {
|
|
5
|
+
it('returns false for empty array', () => {
|
|
6
|
+
expect(utils.hasTailwindCdnSource([])).to.equal(false);
|
|
7
|
+
});
|
|
8
|
+
it('returns true for multiple tailwind URLs', () => {
|
|
9
|
+
expect(utils.hasTailwindCdnSource([
|
|
10
|
+
'https://cdn.tailwindcss.com',
|
|
11
|
+
'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.0.0'
|
|
12
|
+
])).to.equal(true);
|
|
13
|
+
});
|
|
14
|
+
it('returns false for malformed URLs', () => {
|
|
15
|
+
expect(utils.hasTailwindCdnSource(['not-a-url'])).to.equal(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('replaceSourceUrlVars edge cases', () => {
|
|
20
|
+
it('replaces matching pattern', () => {
|
|
21
|
+
const str = "var.url+'http://site.com/path'";
|
|
22
|
+
const source = 'http://site.com';
|
|
23
|
+
const expected = "${vars.url}/path";
|
|
24
|
+
expect(utils.replaceSourceUrlVars(str, source)).to.contain(expected);
|
|
25
|
+
});
|
|
26
|
+
it('returns input for empty string', () => {
|
|
27
|
+
expect(utils.replaceSourceUrlVars('', 'http://site.com')).to.equal('');
|
|
28
|
+
});
|
|
29
|
+
it('returns input for null source', () => {
|
|
30
|
+
expect(utils.replaceSourceUrlVars("var.url+'http://site.com/path'", null)).to.equal("var.url+'http://site.com/path'");
|
|
31
|
+
});
|
|
32
|
+
it('replaces multiple matches', () => {
|
|
33
|
+
const str = "var.url+'http://site.com/a' and var.url+'http://site.com/b'";
|
|
34
|
+
const source = 'http://site.com';
|
|
35
|
+
const result = utils.replaceSourceUrlVars(str, source);
|
|
36
|
+
expect(result.match(/\${vars\.url}/g)?.length).to.be.greaterThanOrEqual(2);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('sanitizeAndReplaceLeadingNumbers edge cases', () => {
|
|
41
|
+
it('returns input for no numbers', () => {
|
|
42
|
+
expect(utils.sanitizeAndReplaceLeadingNumbers('test')).to.equal('test');
|
|
43
|
+
});
|
|
44
|
+
it('handles multiple leading numbers', () => {
|
|
45
|
+
expect(utils.sanitizeAndReplaceLeadingNumbers('123abc')).to.match(/one1two2three3abc|123abc/);
|
|
46
|
+
});
|
|
47
|
+
it('handles only numbers', () => {
|
|
48
|
+
expect(utils.sanitizeAndReplaceLeadingNumbers('123')).to.match(/one1two2three3|123/);
|
|
49
|
+
});
|
|
50
|
+
it('handles special characters', () => {
|
|
51
|
+
expect(utils.sanitizeAndReplaceLeadingNumbers('1_test')).to.match(/one1test|1test/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('replaceUnderscoresSpacesAndUppercaseLetters edge cases', () => {
|
|
56
|
+
it('handles only underscores', () => {
|
|
57
|
+
expect(utils.replaceUnderscoresSpacesAndUppercaseLetters('___')).to.equal('---');
|
|
58
|
+
});
|
|
59
|
+
it('handles only spaces', () => {
|
|
60
|
+
expect(utils.replaceUnderscoresSpacesAndUppercaseLetters(' ')).to.equal('---');
|
|
61
|
+
});
|
|
62
|
+
it('handles mixed cases', () => {
|
|
63
|
+
expect(utils.replaceUnderscoresSpacesAndUppercaseLetters('Test_Name Here')).to.equal('test-name-here');
|
|
64
|
+
});
|
|
65
|
+
it('handles empty string', () => {
|
|
66
|
+
expect(utils.replaceUnderscoresSpacesAndUppercaseLetters('')).to.equal('');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('convertDashesSpacesAndUppercaseToUnderscoresAndLowercase edge cases', () => {
|
|
71
|
+
it('handles only dashes', () => {
|
|
72
|
+
expect(utils.convertDashesSpacesAndUppercaseToUnderscoresAndLowercase('---')).to.equal('___');
|
|
73
|
+
});
|
|
74
|
+
it('handles only spaces', () => {
|
|
75
|
+
expect(utils.convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(' ')).to.equal('___');
|
|
76
|
+
});
|
|
77
|
+
it('handles mixed cases', () => {
|
|
78
|
+
expect(utils.convertDashesSpacesAndUppercaseToUnderscoresAndLowercase('Test-Name Here')).to.equal('test_name_here');
|
|
79
|
+
});
|
|
80
|
+
it('handles empty string', () => {
|
|
81
|
+
expect(utils.convertDashesSpacesAndUppercaseToUnderscoresAndLowercase('')).to.equal('');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('hasAbsoluteKeyword edge cases', () => {
|
|
86
|
+
it('detects uppercase', () => {
|
|
87
|
+
expect(utils.hasAbsoluteKeyword('ABSOLUTE')).to.equal(true);
|
|
88
|
+
});
|
|
89
|
+
it('returns false for empty string', () => {
|
|
90
|
+
expect(utils.hasAbsoluteKeyword('')).to.equal(false);
|
|
91
|
+
});
|
|
92
|
+
it('returns false for empty string', () => {
|
|
93
|
+
expect(utils.hasAbsoluteKeyword('')).to.equal(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('generateRandomVariableName edge cases', () => {
|
|
98
|
+
it('returns prefix only for length 0', () => {
|
|
99
|
+
expect(utils.generateRandomVariableName('prefix', 0)).to.equal('prefix');
|
|
100
|
+
});
|
|
101
|
+
it('returns only random part for empty prefix', () => {
|
|
102
|
+
expect(utils.generateRandomVariableName('', 3)).to.match(/^[a-z]{3}$/);
|
|
103
|
+
});
|
|
104
|
+
it('returns correct length', () => {
|
|
105
|
+
const result = utils.generateRandomVariableName('pre', 5);
|
|
106
|
+
expect(result.length).to.equal(8);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import * as utils from '../utils';
|
|
3
|
+
|
|
4
|
+
describe('utils.ts functions', () => {
|
|
5
|
+
it('hasTailwindCdnSource returns true for Tailwind CDN', () => {
|
|
6
|
+
expect(utils.hasTailwindCdnSource(['https://cdn.tailwindcss.com'])).to.equal(true);
|
|
7
|
+
expect(utils.hasTailwindCdnSource(['https://example.com'])).to.equal(false);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('replaceSourceUrlVars returns input if no match', () => {
|
|
11
|
+
const str = "var.url+'http://site.com/path'";
|
|
12
|
+
const expected = "${vars.url}/path";
|
|
13
|
+
expect(utils.replaceSourceUrlVars(str, 'http://site.com')).to.equal(expected);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('sanitizeAndReplaceLeadingNumbers replaces leading numbers', () => {
|
|
17
|
+
expect(utils.sanitizeAndReplaceLeadingNumbers('1test')).to.match(/one1test|1test/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('replaceUnderscoresSpacesAndUppercaseLetters replaces underscores and spaces', () => {
|
|
21
|
+
expect(utils.replaceUnderscoresSpacesAndUppercaseLetters('Test_Name Here')).to.equal('test-name-here');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('convertDashesSpacesAndUppercaseToUnderscoresAndLowercase converts correctly', () => {
|
|
25
|
+
expect(utils.convertDashesSpacesAndUppercaseToUnderscoresAndLowercase('Test-Name Here')).to.equal('test_name_here');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('hasAbsoluteKeyword detects absolute', () => {
|
|
29
|
+
expect(utils.hasAbsoluteKeyword('absolute')).to.equal(true);
|
|
30
|
+
expect(utils.hasAbsoluteKeyword('relative')).to.equal(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('generateRandomVariableName returns string with prefix', () => {
|
|
34
|
+
expect(utils.generateRandomVariableName('prefix')).to.match(/^prefix/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import block from './index.js';
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
const htmlContent = fs.readFileSync('index.html', 'utf8');
|
|
6
|
+
const options = {
|
|
7
|
+
name: 'TempBlockTest',
|
|
8
|
+
prefix: 'wp',
|
|
9
|
+
category: 'common',
|
|
10
|
+
basePath: process.cwd(),
|
|
11
|
+
shouldSaveFiles: true,
|
|
12
|
+
generateIconPreview: true,
|
|
13
|
+
jsFiles: [],
|
|
14
|
+
cssFiles: [],
|
|
15
|
+
source: 'https://diogoangelim.github.io/html-to-gutenberg/',
|
|
16
|
+
};
|
|
17
|
+
const result = await block(htmlContent, options);
|
|
18
|
+
console.log('Block generation result:', Object.keys(result));
|
|
19
|
+
})();
|
package/tsconfig.json
CHANGED
|
@@ -3,11 +3,32 @@
|
|
|
3
3
|
"sourceMap": true,
|
|
4
4
|
"outDir": "dist",
|
|
5
5
|
"target": "esnext",
|
|
6
|
-
"module": "
|
|
6
|
+
"module": "nodenext",
|
|
7
|
+
"moduleResolution": "nodenext",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
7
10
|
"strict": true,
|
|
8
11
|
"lib": [
|
|
9
12
|
"esnext"
|
|
10
13
|
],
|
|
11
|
-
"esModuleInterop": true
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"@types.d.ts",
|
|
18
|
+
"globals.ts",
|
|
19
|
+
"utils.ts",
|
|
20
|
+
"src/**/*.ts"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"coverage",
|
|
26
|
+
"vendor",
|
|
27
|
+
"**/*.test.ts",
|
|
28
|
+
"index.ts",
|
|
29
|
+
"fetch-page-assets.test.ts",
|
|
30
|
+
"index.test.ts",
|
|
31
|
+
"coverage-demo.test.ts",
|
|
32
|
+
"snapapi-screenshot.test.ts"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/utils.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Utility functions for html-to-gutenberg
|
|
2
|
+
export function hasTailwindCdnSource(jsFiles: string[]): boolean {
|
|
3
|
+
const tailwindCdnRegex = /https:\/\/(cdn\.tailwindcss\.com(\?[^"'\s]*)?|cdn\.jsdelivr\.net\/npm\/@tailwindcss\/browser@4(\.\d+){0,2})/;
|
|
4
|
+
return jsFiles.some((url: string) => tailwindCdnRegex.test(url));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function replaceSourceUrlVars(str: string, source: any): string {
|
|
8
|
+
if (!source) return str;
|
|
9
|
+
const escapedSource = String(source).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
const pattern = new RegExp(`var\\.url\\+'${escapedSource}([^']*)'`, 'g');
|
|
11
|
+
if (!pattern.test(str)) return str;
|
|
12
|
+
return str.replace(pattern, (_match: string, path: string) => `\${vars.url}${path}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sanitizeAndReplaceLeadingNumbers(str: string): string {
|
|
16
|
+
const numberWords = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
|
|
17
|
+
let firstNumberReplaced = false;
|
|
18
|
+
return str
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[\s\-_]/g, '')
|
|
21
|
+
.replace(/\d/g, (digit: string) => {
|
|
22
|
+
if (!firstNumberReplaced) {
|
|
23
|
+
firstNumberReplaced = true;
|
|
24
|
+
return numberWords[parseInt(digit)] + digit;
|
|
25
|
+
}
|
|
26
|
+
return digit;
|
|
27
|
+
})
|
|
28
|
+
.replace(/^[^a-z]+/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function replaceUnderscoresSpacesAndUppercaseLetters(name: string = ''): string {
|
|
32
|
+
return name.replace(new RegExp(/\W|_/, 'g'), '-').toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(string: string): string {
|
|
36
|
+
if (string) {
|
|
37
|
+
return `${string.replaceAll('-', '_').replaceAll(' ', '_').toLowerCase()}`;
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasAbsoluteKeyword(str: string): boolean {
|
|
43
|
+
if (typeof str !== 'string') return false;
|
|
44
|
+
return str.toLowerCase().includes('absolute');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function generateRandomVariableName(prefix: string = 'content', length: number = 3): string {
|
|
48
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz';
|
|
49
|
+
let suffix = '';
|
|
50
|
+
for (let i = 0; i < length; i++) {
|
|
51
|
+
suffix += chars.charAt(
|
|
52
|
+
Math.floor(Math.random() * chars.length)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return `${prefix}${suffix}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Diogo Angelim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# fetch-page-assets
|
|
2
|
+
|
|
3
|
+
Download page assets, rewrite the HTML to point at the fetched assets, and optionally upload those downloads directly to Cloudflare R2 instead of writing them to disk.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install fetch-page-assets
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Environment
|
|
12
|
+
|
|
13
|
+
Keep real secrets in `.env` and never commit them.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp .env.example .env
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Required for R2 uploads:
|
|
20
|
+
|
|
21
|
+
- `CLOUDFLARE_R2_ACCOUNT_ID`
|
|
22
|
+
- `CLOUDFLARE_R2_BUCKET`
|
|
23
|
+
- `CLOUDFLARE_R2_ACCESS_KEY_ID`
|
|
24
|
+
- `CLOUDFLARE_R2_SECRET_ACCESS_KEY`
|
|
25
|
+
- `CLOUDFLARE_R2_PUBLIC_BASE_URL`
|
|
26
|
+
|
|
27
|
+
Optional:
|
|
28
|
+
|
|
29
|
+
- `CLOUDFLARE_API_TOKEN`
|
|
30
|
+
- `CLOUDFLARE_R2_ENDPOINT`
|
|
31
|
+
|
|
32
|
+
## Getting and rotating Cloudflare credentials
|
|
33
|
+
|
|
34
|
+
1. Open the Cloudflare dashboard.
|
|
35
|
+
2. Create or rotate the R2 access keys for the bucket you want to use.
|
|
36
|
+
3. Update `.env` with the new key values.
|
|
37
|
+
4. If you use a Cloudflare API token for verification or other account workflows, rotate it in the API Tokens section and update `.env`.
|
|
38
|
+
5. Restart the service after updating `.env`.
|
|
39
|
+
6. Revoke the old token or key after the new one is confirmed working.
|
|
40
|
+
|
|
41
|
+
To verify a token without exposing it in source code:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
curl "https://api.cloudflare.com/client/v4/user/tokens/verify" \
|
|
45
|
+
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Legacy local mode
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import extractAssets from 'fetch-page-assets';
|
|
54
|
+
|
|
55
|
+
const html = await extractAssets('<img src="/logo.png" />', {
|
|
56
|
+
source: 'https://example.com',
|
|
57
|
+
basePath: process.cwd(),
|
|
58
|
+
saveFile: true
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### R2 upload mode
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import extractAssets from 'fetch-page-assets';
|
|
66
|
+
|
|
67
|
+
const result = await extractAssets('<img src="/logo.png" />', {
|
|
68
|
+
source: 'https://example.com',
|
|
69
|
+
uploadToR2: true,
|
|
70
|
+
returnDetails: true,
|
|
71
|
+
jobId: 'conv_123',
|
|
72
|
+
r2Prefix: 'generated/conv_123/assets'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log(result);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Example response:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"html": "<img src=\"https://storage.example.com/generated/conv_123/assets/logo.png\">",
|
|
83
|
+
"assets": [
|
|
84
|
+
{
|
|
85
|
+
"id": "file_1",
|
|
86
|
+
"name": "logo.png",
|
|
87
|
+
"type": "image/png",
|
|
88
|
+
"size": 48211,
|
|
89
|
+
"path": "/generated/conv_123/assets/logo.png",
|
|
90
|
+
"url": "https://storage.example.com/generated/conv_123/assets/logo.png",
|
|
91
|
+
"kind": "asset"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Options
|
|
98
|
+
|
|
99
|
+
| Option | Description | Type | Default |
|
|
100
|
+
| --- | --- | --- | --- |
|
|
101
|
+
| `source` | Base URL used to resolve relative asset paths. | `string` | `''` |
|
|
102
|
+
| `basePath` | Local base path used in legacy mode. | `string` | current directory |
|
|
103
|
+
| `saveFile` | Writes downloaded assets to disk in legacy mode. | `boolean` | `true` |
|
|
104
|
+
| `uploadToR2` | Uploads resolved assets to Cloudflare R2. | `boolean` | `false` |
|
|
105
|
+
| `returnDetails` | Returns `{ html, assets }` metadata instead of only the rewritten HTML string. | `boolean` | `false` |
|
|
106
|
+
| `jobId` | Stable conversion identifier used to build remote paths. | `string` | `conv_local` |
|
|
107
|
+
| `r2Prefix` | Remote storage prefix for uploaded assets. | `string` | derived from `jobId` |
|
|
108
|
+
| `concurrency` | Maximum number of simultaneous downloads. | `number` | `8` |
|
|
109
|
+
| `maxRetryAttempts` | Maximum attempts per asset. | `number` | `3` |
|
|
110
|
+
| `retryDelay` | Delay between attempts in milliseconds. | `number` | `1000` |
|
|
111
|
+
| `verbose` | Enables console logging. | `boolean` | `true` |
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- `returnDetails: true` is the recommended mode when using R2 because it gives you the uploaded asset metadata.
|
|
116
|
+
- Keep all Cloudflare credentials in `.env`.
|
|
117
|
+
- Do not hardcode tokens or access keys in code, documentation, tests, or shell history.
|