edgepress 2026.10.12 → 2026.10.13

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.
Files changed (57) hide show
  1. package/README.md +12 -2
  2. package/config.yml +171 -171
  3. package/content/assets/edgepress/brand/sources/export-manifest.json +181 -181
  4. package/content/assets/edgepress-markdown-guide.svg +21 -21
  5. package/content/guides/markdown-media.md +38 -0
  6. package/content/guides/product-demos.md +30 -30
  7. package/content/pages/deployment/en.json +1 -1
  8. package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +1 -1
  9. package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +1 -1
  10. package/languages/base/en.json +98 -98
  11. package/languages/packs/zh-CN.json +98 -98
  12. package/package.json +90 -90
  13. package/src/assets.js +187 -186
  14. package/src/browser-audit.js +158 -158
  15. package/src/build.js +293 -291
  16. package/src/cli.js +364 -364
  17. package/src/content.js +233 -233
  18. package/src/data-saver.js +3 -3
  19. package/src/generators.js +414 -420
  20. package/src/i18n.js +86 -86
  21. package/src/markdown.js +195 -160
  22. package/src/page-check.js +1 -1
  23. package/src/plugin-api.js +77 -77
  24. package/src/report-visuals.js +642 -642
  25. package/src/revision-cards.js +30 -30
  26. package/src/rss.js +100 -0
  27. package/src/theme.js +4 -4
  28. package/src/video-metadata.js +36 -0
  29. package/static/edgepress/fonts/faces.css +2217 -2217
  30. package/static/edgepress/media-viewer.js +67 -10
  31. package/static/edgepress/project-demo.js +83 -83
  32. package/static/edgepress/search.js +84 -84
  33. package/tests/assets-runtime.test.mjs +26 -26
  34. package/tests/audit.test.mjs +142 -142
  35. package/tests/brand-regression.test.mjs +110 -110
  36. package/tests/document-package.test.mjs +2 -2
  37. package/tests/embed-syntax.test.mjs +68 -68
  38. package/tests/image-viewer.test.mjs +36 -36
  39. package/tests/markdown-media.test.mjs +119 -0
  40. package/tests/oembed-layout.test.mjs +6 -1
  41. package/tests/project-demo.test.mjs +73 -73
  42. package/tests/release-version.test.mjs +28 -0
  43. package/tests/rss.test.mjs +54 -0
  44. package/tests/video-metadata.test.mjs +28 -0
  45. package/tests/writing.test.mjs +83 -83
  46. package/themes/atelier/assets/style.css +340 -340
  47. package/themes/default/assets/style.css +371 -371
  48. package/themes/default/layouts/partials/head.html +1 -1
  49. package/themes/folio/assets/style.css +515 -515
  50. package/themes/folio/theme.json +5 -5
  51. package/themes/signal/assets/style.css +332 -332
  52. package/tools/README.md +10 -10
  53. package/tools/assets/generate-brand-assets.py +160 -160
  54. package/tools/assets/video-preview.mjs +15 -0
  55. package/tools/recordings/demo-workbench.mjs +53 -53
  56. package/tools/recordings/desktop-recording.mjs +70 -70
  57. package/tools/recordings/vm-desktop.mjs +59 -59
package/src/assets.js CHANGED
@@ -1,130 +1,131 @@
1
- import { createHash } from 'node:crypto';
2
- import { copyFile, readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
3
- import { dirname, extname, relative, resolve, sep } from 'node:path';
4
- import * as posix from 'node:path/posix';
5
- import {fileURLToPath} from 'node:url';
6
- import { mapLimit } from './concurrency.js';
7
- import {extendConsentPolicy,serializeConsentHeaders} from './consent-csp.js';
1
+ import { createHash } from 'node:crypto';
2
+ import { copyFile, readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
3
+ import { dirname, extname, relative, resolve, sep } from 'node:path';
4
+ import * as posix from 'node:path/posix';
5
+ import {fileURLToPath} from 'node:url';
6
+ import { mapLimit } from './concurrency.js';
7
+ import {extendConsentPolicy,serializeConsentHeaders} from './consent-csp.js';
8
8
  import {imageViewerAssets, mediaViewerAssets} from './media-assets.js';
9
9
  import {documentViewerAssets} from '@jsw-teams/document-viewer/assets';
10
10
  import {collectImageDimensions} from './image-dimensions.js';
11
+ import {collectVideoMetadata} from './video-metadata.js';
11
12
  import {dataSaverLayout} from './data-saver.js';
12
13
  import { build, transform } from 'esbuild';
13
-
14
- async function walk(directory) {
15
- let entries;
16
- try { entries = await readdir(directory, { withFileTypes: true }); }
17
- catch (error) { if (error.code === 'ENOENT') return []; throw error; }
18
- const files = [];
19
- for (const entry of entries) {
20
- const full = resolve(directory, entry.name);
21
- if (entry.isDirectory()) files.push(...await walk(full));
22
- else if (entry.isFile()) files.push(full);
23
- }
24
- return files;
25
- }
26
-
14
+
15
+ async function walk(directory) {
16
+ let entries;
17
+ try { entries = await readdir(directory, { withFileTypes: true }); }
18
+ catch (error) { if (error.code === 'ENOENT') return []; throw error; }
19
+ const files = [];
20
+ for (const entry of entries) {
21
+ const full = resolve(directory, entry.name);
22
+ if (entry.isDirectory()) files.push(...await walk(full));
23
+ else if (entry.isFile()) files.push(full);
24
+ }
25
+ return files;
26
+ }
27
+
27
28
  function isCodeAsset(path) {
28
29
  const extension = extname(path).toLowerCase();
29
30
  return extension === '.css' || extension === '.js' || extension === '.mjs';
30
- }
31
-
32
- function referencedPaths(path, content, known) {
33
- if (!isCodeAsset(path)) return [];
34
- const text = content.toString('utf8');
31
+ }
32
+
33
+ function referencedPaths(path, content, known) {
34
+ if (!isCodeAsset(path)) return [];
35
+ const text = content.toString('utf8');
35
36
  const expression = ['.js', '.mjs'].includes(extname(path).toLowerCase())
36
37
  ? /\b(?:from\s*|import\s*(?:\(\s*)?|new\s+URL\s*\(\s*)['"]([^'"]+)['"]/g
37
38
  : /(?:@import\s*(?:url\()?\s*|url\(\s*)['"]?([^'")\s;]+)['"]?\s*\)?/gi;
38
- const refs = [];
39
- for (const match of text.matchAll(expression)) {
40
- const specifier = match[1];
41
- if (/^(?:[a-z]+:|\/\/|#)/i.test(specifier)) continue;
42
- const clean = specifier.split(/[?#]/, 1)[0];
43
- const target = clean.startsWith('/')
44
- ? clean.slice(1)
45
- : posix.normalize(posix.join(posix.dirname(path), clean));
46
- if (known.has(target) && isCodeAsset(target)) refs.push(target);
47
- }
48
- return [...new Set(refs)];
49
- }
50
-
51
- function rewriteCodeReferences(item, text, outputByOriginal) {
52
- const extension = extname(item.path).toLowerCase();
53
- const rewriteSpecifier = (specifier) => {
54
- if (/^(?:[a-z]+:|\/\/|#)/i.test(specifier)) return null;
55
- const splitAt = specifier.search(/[?#]/);
56
- const clean = splitAt < 0 ? specifier : specifier.slice(0, splitAt);
57
- const tail = splitAt < 0 ? '' : specifier.slice(splitAt);
58
- const target = clean.startsWith('/')
59
- ? clean.slice(1)
60
- : posix.normalize(posix.join(posix.dirname(item.path), clean));
61
- const outputTarget = outputByOriginal.get(target);
62
- if (!outputTarget) return null;
63
- let rewritten;
64
- if (clean.startsWith('/')) rewritten = '/' + outputTarget;
65
- else {
66
- rewritten = posix.relative(posix.dirname(item.outputPath), outputTarget);
67
- if (!rewritten.startsWith('.')) rewritten = './' + rewritten;
68
- }
69
- return rewritten + tail;
70
- };
71
-
39
+ const refs = [];
40
+ for (const match of text.matchAll(expression)) {
41
+ const specifier = match[1];
42
+ if (/^(?:[a-z]+:|\/\/|#)/i.test(specifier)) continue;
43
+ const clean = specifier.split(/[?#]/, 1)[0];
44
+ const target = clean.startsWith('/')
45
+ ? clean.slice(1)
46
+ : posix.normalize(posix.join(posix.dirname(path), clean));
47
+ if (known.has(target) && isCodeAsset(target)) refs.push(target);
48
+ }
49
+ return [...new Set(refs)];
50
+ }
51
+
52
+ function rewriteCodeReferences(item, text, outputByOriginal) {
53
+ const extension = extname(item.path).toLowerCase();
54
+ const rewriteSpecifier = (specifier) => {
55
+ if (/^(?:[a-z]+:|\/\/|#)/i.test(specifier)) return null;
56
+ const splitAt = specifier.search(/[?#]/);
57
+ const clean = splitAt < 0 ? specifier : specifier.slice(0, splitAt);
58
+ const tail = splitAt < 0 ? '' : specifier.slice(splitAt);
59
+ const target = clean.startsWith('/')
60
+ ? clean.slice(1)
61
+ : posix.normalize(posix.join(posix.dirname(item.path), clean));
62
+ const outputTarget = outputByOriginal.get(target);
63
+ if (!outputTarget) return null;
64
+ let rewritten;
65
+ if (clean.startsWith('/')) rewritten = '/' + outputTarget;
66
+ else {
67
+ rewritten = posix.relative(posix.dirname(item.outputPath), outputTarget);
68
+ if (!rewritten.startsWith('.')) rewritten = './' + rewritten;
69
+ }
70
+ return rewritten + tail;
71
+ };
72
+
72
73
  if (extension === '.js' || extension === '.mjs') {
73
74
  const expression = /(\b(?:from\s*|import\s*(?:\(\s*)?|new\s+URL\s*\(\s*))(['"])([^'"]+)\2/g;
74
- return text.replace(expression, (match, prefix, quote, specifier) => {
75
- const rewritten = rewriteSpecifier(specifier);
76
- return rewritten === null ? match : prefix + quote + rewritten + quote;
77
- });
78
- }
79
-
75
+ return text.replace(expression, (match, prefix, quote, specifier) => {
76
+ const rewritten = rewriteSpecifier(specifier);
77
+ return rewritten === null ? match : prefix + quote + rewritten + quote;
78
+ });
79
+ }
80
+
80
81
  const expression = /((?:@import\s*(?:url\()?\s*|url\(\s*))(['"]?)([^)'"\s;]+)(['"]?\s*\)?)/gi;
81
- return text.replace(expression, (match, prefix, quote, specifier, suffix) => {
82
- const rewritten = rewriteSpecifier(specifier);
83
- return rewritten === null ? match : prefix + quote + rewritten + suffix;
84
- });
85
- }
86
-
82
+ return text.replace(expression, (match, prefix, quote, specifier, suffix) => {
83
+ const rewritten = rewriteSpecifier(specifier);
84
+ return rewritten === null ? match : prefix + quote + rewritten + suffix;
85
+ });
86
+ }
87
+
87
88
  export async function collectAssets(config, { documentViewer = false, mediaViewer = false } = {}) {
88
- const sources = [];
89
- const directories = [
90
- { root: config.resolvedPaths.static, name: 'static' },
91
- { root: resolve(config.resolvedPaths.content, 'assets'), name: 'content/assets' },
92
- { root: resolve(config.resolvedPaths.theme, 'assets'), name: 'theme assets' }
93
- ];
94
- for (const { root, name } of directories) {
95
- for (const source of await walk(root)) {
96
- const path = relative(root, source).split(sep).join('/');
97
- if (path.split('/').includes('.gitkeep')) continue;
98
- sources.push({ source, path, sourceName: name });
99
- }
100
- }
101
- // Library consumers do not need to copy framework runtime source into their
102
- // repositories. Project-owned overrides keep precedence for existing sites.
103
- const runtimeRoot=fileURLToPath(new URL('../static/edgepress/',import.meta.url));
89
+ const sources = [];
90
+ const directories = [
91
+ { root: config.resolvedPaths.static, name: 'static' },
92
+ { root: resolve(config.resolvedPaths.content, 'assets'), name: 'content/assets' },
93
+ { root: resolve(config.resolvedPaths.theme, 'assets'), name: 'theme assets' }
94
+ ];
95
+ for (const { root, name } of directories) {
96
+ for (const source of await walk(root)) {
97
+ const path = relative(root, source).split(sep).join('/');
98
+ if (path.split('/').includes('.gitkeep')) continue;
99
+ sources.push({ source, path, sourceName: name });
100
+ }
101
+ }
102
+ // Library consumers do not need to copy framework runtime source into their
103
+ // repositories. Project-owned overrides keep precedence for existing sites.
104
+ const runtimeRoot=fileURLToPath(new URL('../static/edgepress/',import.meta.url));
104
105
  const supplied=new Set(sources.map(item=>item.path));
105
106
  if (documentViewer) for (const item of await documentViewerAssets('edgepress/document-viewer')) if (!supplied.has(item.path)) sources.push(item);
106
107
  for(const item of await imageViewerAssets())if(!supplied.has(item.path))sources.push(item);
107
108
  if (mediaViewer) for (const item of await mediaViewerAssets()) if (!supplied.has(item.path)) sources.push(item);
108
- for(const source of await walk(runtimeRoot)) {
109
- const path='edgepress/'+relative(runtimeRoot,source).split(sep).join('/');
110
- if(!supplied.has(path))sources.push({source,path,sourceName:'EdgePress runtime'});
111
- }
112
- // Built-in navigation and page icons live with the package's content assets,
113
- // not its browser scripts. Include only this library, not example-site media.
114
- const iconsRoot=fileURLToPath(new URL('../content/assets/edgepress/icons/',import.meta.url));
115
- for(const source of await walk(iconsRoot)) {
116
- const path='edgepress/icons/'+relative(iconsRoot,source).split(sep).join('/');
117
- if(!supplied.has(path))sources.push({source,path,sourceName:'EdgePress icons'});
118
- }
119
-
120
- const headers = [];
121
- const assetSources = [];
122
- for (const item of sources) {
123
- if (item.path.toLowerCase() === '_headers') headers.push(item);
124
- else assetSources.push(item);
125
- }
126
- const [headerContents, items] = await Promise.all([
127
- mapLimit(headers, config.concurrency, async (item) => (await readFile(item.source)).toString('utf8')),
109
+ for(const source of await walk(runtimeRoot)) {
110
+ const path='edgepress/'+relative(runtimeRoot,source).split(sep).join('/');
111
+ if(!supplied.has(path))sources.push({source,path,sourceName:'EdgePress runtime'});
112
+ }
113
+ // Built-in navigation and page icons live with the package's content assets,
114
+ // not its browser scripts. Include only this library, not example-site media.
115
+ const iconsRoot=fileURLToPath(new URL('../content/assets/edgepress/icons/',import.meta.url));
116
+ for(const source of await walk(iconsRoot)) {
117
+ const path='edgepress/icons/'+relative(iconsRoot,source).split(sep).join('/');
118
+ if(!supplied.has(path))sources.push({source,path,sourceName:'EdgePress icons'});
119
+ }
120
+
121
+ const headers = [];
122
+ const assetSources = [];
123
+ for (const item of sources) {
124
+ if (item.path.toLowerCase() === '_headers') headers.push(item);
125
+ else assetSources.push(item);
126
+ }
127
+ const [headerContents, items] = await Promise.all([
128
+ mapLimit(headers, config.concurrency, async (item) => (await readFile(item.source)).toString('utf8')),
128
129
  mapLimit(assetSources, config.concurrency, async (item) => {
129
130
  if (!isCodeAsset(item.path)) return { ...item, content: null };
130
131
  const source = item.content ?? await readFile(item.source);
@@ -191,68 +192,68 @@ export async function collectAssets(config, { documentViewer = false, mediaViewe
191
192
  const layout = await dataSaverLayout(config, items);
192
193
  if (layout) items.push(layout);
193
194
  const originals = new Map();
194
- for (const item of items) {
195
- const normalized = item.path.toLowerCase();
196
- const previousSource = originals.get(normalized);
197
- if (previousSource) {
198
- throw new Error(previousSource + ' and ' + item.sourceName + ' assets have the same output path: ' + item.path);
199
- }
200
- originals.set(normalized, item.sourceName);
201
- }
202
- const byOriginal = new Map(items.map((item) => [item.path, item]));
203
- const dependencies = new Map();
204
- for (const item of items) dependencies.set(item.path, referencedPaths(item.path, item.content, byOriginal));
205
-
206
- const outputByOriginal = new Map();
207
- for (const item of items) {
208
- if (!isCodeAsset(item.path)) outputByOriginal.set(item.path, item.path);
209
- else {
210
- const reachable = new Set();
211
- const visit = (path) => {
212
- for (const dependency of dependencies.get(path) ?? []) {
213
- if (reachable.has(dependency)) continue;
214
- reachable.add(dependency);
215
- visit(dependency);
216
- }
217
- };
218
- visit(item.path);
219
- const sourceDigests = [...reachable].sort().map((path) =>
220
- path + ':' + createHash('sha256').update(byOriginal.get(path).content).digest('hex')
221
- ).join('\n');
222
- const digest = createHash('sha256')
223
- .update(item.content)
224
- .update('\n')
225
- .update(sourceDigests)
226
- .digest('hex')
227
- .slice(0, 16);
228
- const extension = extname(item.path).toLowerCase();
229
- outputByOriginal.set(item.path, item.path.slice(0, -extname(item.path).length) + '.' + digest + extension);
230
- }
231
- }
232
-
233
- const assets = items.map((item) => {
234
- const outputPath = outputByOriginal.get(item.path);
235
- if (!isCodeAsset(item.path)) return { path: outputPath, source: item.source };
236
- const content = Buffer.from(rewriteCodeReferences(
237
- { ...item, outputPath }, item.content.toString('utf8'), outputByOriginal
238
- ), 'utf8');
239
- return { path: outputPath, content };
240
- });
241
- const urlMap = Object.create(null);
242
- for (const item of items) urlMap['/' + item.path] = '/' + outputByOriginal.get(item.path);
243
- config.assetManifest = urlMap;
244
- return { assets, urlMap, imageDimensions:await collectImageDimensions(assets), existingHeaders: extendConsentPolicy(headerContents.join('\n\n'),config) };
245
- }
246
-
247
- export async function writeAssets(bundle, output, concurrency = 8) {
248
- await mapLimit(bundle.assets, concurrency, async (asset) => {
249
- const target = resolve(output, ...asset.path.split('/'));
250
- await mkdir(dirname(target), { recursive: true });
251
- if (asset.source) await copyFile(asset.source, target);
252
- else await writeFile(target, asset.content);
253
- });
254
- // Cloudflare already revalidates unversioned assets by default. A broad cache
255
- // rule would be concatenated with exact immutable rules instead of overridden.
195
+ for (const item of items) {
196
+ const normalized = item.path.toLowerCase();
197
+ const previousSource = originals.get(normalized);
198
+ if (previousSource) {
199
+ throw new Error(previousSource + ' and ' + item.sourceName + ' assets have the same output path: ' + item.path);
200
+ }
201
+ originals.set(normalized, item.sourceName);
202
+ }
203
+ const byOriginal = new Map(items.map((item) => [item.path, item]));
204
+ const dependencies = new Map();
205
+ for (const item of items) dependencies.set(item.path, referencedPaths(item.path, item.content, byOriginal));
206
+
207
+ const outputByOriginal = new Map();
208
+ for (const item of items) {
209
+ if (!isCodeAsset(item.path)) outputByOriginal.set(item.path, item.path);
210
+ else {
211
+ const reachable = new Set();
212
+ const visit = (path) => {
213
+ for (const dependency of dependencies.get(path) ?? []) {
214
+ if (reachable.has(dependency)) continue;
215
+ reachable.add(dependency);
216
+ visit(dependency);
217
+ }
218
+ };
219
+ visit(item.path);
220
+ const sourceDigests = [...reachable].sort().map((path) =>
221
+ path + ':' + createHash('sha256').update(byOriginal.get(path).content).digest('hex')
222
+ ).join('\n');
223
+ const digest = createHash('sha256')
224
+ .update(item.content)
225
+ .update('\n')
226
+ .update(sourceDigests)
227
+ .digest('hex')
228
+ .slice(0, 16);
229
+ const extension = extname(item.path).toLowerCase();
230
+ outputByOriginal.set(item.path, item.path.slice(0, -extname(item.path).length) + '.' + digest + extension);
231
+ }
232
+ }
233
+
234
+ const assets = items.map((item) => {
235
+ const outputPath = outputByOriginal.get(item.path);
236
+ if (!isCodeAsset(item.path)) return { path: outputPath, source: item.source };
237
+ const content = Buffer.from(rewriteCodeReferences(
238
+ { ...item, outputPath }, item.content.toString('utf8'), outputByOriginal
239
+ ), 'utf8');
240
+ return { path: outputPath, content };
241
+ });
242
+ const urlMap = Object.create(null);
243
+ for (const item of items) urlMap['/' + item.path] = '/' + outputByOriginal.get(item.path);
244
+ config.assetManifest = urlMap;
245
+ return { assets, urlMap, imageDimensions:await collectImageDimensions(assets), videoMetadata:await collectVideoMetadata(assets), existingHeaders: extendConsentPolicy(headerContents.join('\n\n'),config) };
246
+ }
247
+
248
+ export async function writeAssets(bundle, output, concurrency = 8) {
249
+ await mapLimit(bundle.assets, concurrency, async (asset) => {
250
+ const target = resolve(output, ...asset.path.split('/'));
251
+ await mkdir(dirname(target), { recursive: true });
252
+ if (asset.source) await copyFile(asset.source, target);
253
+ else await writeFile(target, asset.content);
254
+ });
255
+ // Cloudflare already revalidates unversioned assets by default. A broad cache
256
+ // rule would be concatenated with exact immutable rules instead of overridden.
256
257
  const securityRules = '/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin';
257
258
  // Unicode fonts contain hundreds of subsets. Static hosts accept 100 header
258
259
  // rules, so group a directory only when every WOFF2 file in it is hashed.
@@ -272,10 +273,10 @@ export async function writeAssets(bundle, output, concurrency = 8) {
272
273
  for (const directory of fontDirectories) immutableRules.push('/' + (directory === '.' ? '' : directory + '/') + '*.woff2\n Cache-Control: public, max-age=31536000, immutable');
273
274
  const documentDirectories = new Set(bundle.assets.map(asset => asset.path.match(/^(.*\/pdf-assets\.[a-f0-9]{16})\//)?.[1]).filter(Boolean));
274
275
  for (const directory of documentDirectories) immutableRules.push('/' + directory + '/*\n Cache-Control: public, max-age=31536000, immutable');
275
- const mediaRules = await Promise.all(bundle.assets.filter(asset=>/\.(mp4|webm)$/i.test(asset.path)).map(async asset=>{
276
- const {size}=await stat(resolve(output,...asset.path.split('/')));
277
- return '/'+asset.path+'\n Content-Length: '+size+'\n Accept-Ranges: bytes';
278
- }));
276
+ const mediaRules = await Promise.all(bundle.assets.filter(asset=>/\.(mp4|webm)$/i.test(asset.path)).map(async asset=>{
277
+ const {size}=await stat(resolve(output,...asset.path.split('/')));
278
+ return '/'+asset.path+'\n Content-Length: '+size+'\n Accept-Ranges: bytes';
279
+ }));
279
280
  if (immutableRules.length + mediaRules.length > 85) {
280
281
  const immutablePath = /\.[a-f0-9]{16}\.(?:css|m?js|json|png|jpe?g|webp|avif|svg|gif|mp4|webm|woff2?)$/i;
281
282
  const patterns = new Map();
@@ -298,15 +299,15 @@ export async function writeAssets(bundle, output, concurrency = 8) {
298
299
  const headers = [bundle.existingHeaders.trim(), securityRules, ...immutableRules, ...mediaRules].filter(Boolean).join('\n\n') + '\n';
299
300
  const count = headers.split(/\r?\n/).filter(line => line && !/^\s|^#/.test(line)).length;
300
301
  if (count > 100) throw new Error('Static header rules exceed 100 after safe cache grouping; reduce custom rules or per-file media headers.');
301
- await writeFile(resolve(output, '_headers'), serializeConsentHeaders(headers), 'utf8');
302
- }
303
-
304
- export function rewriteAssetLinks(html, urlMap) {
305
- return html.replace(/(\b(?:src|href)\s*=\s*)(["'])([^"']+)\2/gi, (match, prefix, quote, value) => {
306
- const suffixAt = value.search(/[?#]/);
307
- const sourcePath = suffixAt < 0 ? value : value.slice(0, suffixAt);
308
- const suffix = suffixAt < 0 ? '' : value.slice(suffixAt);
309
- const replacement = urlMap[sourcePath];
310
- return replacement ? prefix + quote + replacement + suffix + quote : match;
311
- });
312
- }
302
+ await writeFile(resolve(output, '_headers'), serializeConsentHeaders(headers), 'utf8');
303
+ }
304
+
305
+ export function rewriteAssetLinks(html, urlMap) {
306
+ return html.replace(/(\b(?:src|href)\s*=\s*)(["'])([^"']+)\2/gi, (match, prefix, quote, value) => {
307
+ const suffixAt = value.search(/[?#]/);
308
+ const sourcePath = suffixAt < 0 ? value : value.slice(0, suffixAt);
309
+ const suffix = suffixAt < 0 ? '' : value.slice(suffixAt);
310
+ const replacement = urlMap[sourcePath];
311
+ return replacement ? prefix + quote + replacement + suffix + quote : match;
312
+ });
313
+ }