edgepress 2026.9.3

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 (80) hide show
  1. package/README.md +70 -0
  2. package/config.yml +146 -0
  3. package/content/pages/home/en.md +84 -0
  4. package/content/pages/home/zh-cn.md +84 -0
  5. package/content/pages/plugin-development/en.md +108 -0
  6. package/content/pages/plugin-development/zh-cn.md +108 -0
  7. package/content/pages/privacy-policy/en.md +73 -0
  8. package/content/pages/privacy-policy/zh-cn.md +73 -0
  9. package/content/pages/project-introduction/en.md +88 -0
  10. package/content/pages/project-introduction/zh-cn.md +88 -0
  11. package/content/pages/quick-start/en.md +147 -0
  12. package/content/pages/quick-start/zh-cn.md +147 -0
  13. package/content/pages/theme-development/en.md +106 -0
  14. package/content/pages/theme-development/zh-cn.md +106 -0
  15. package/content/posts/2026-09-23-edgepress-on-the-edge/en.md +169 -0
  16. package/content/posts/2026-09-23-edgepress-on-the-edge/zh-cn.md +169 -0
  17. package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +30 -0
  18. package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +30 -0
  19. package/content/posts/2026-09-25-edgepress-release-2026-9-2/en.md +28 -0
  20. package/content/posts/2026-09-25-edgepress-release-2026-9-2/zh-cn.md +28 -0
  21. package/content/posts/2026-09-25-edgepress-release-2026-9-3/en.md +39 -0
  22. package/content/posts/2026-09-25-edgepress-release-2026-9-3/zh-cn.md +39 -0
  23. package/edgepress.config.mjs +18 -0
  24. package/languages/base/en.json +55 -0
  25. package/languages/packs/zh-CN.json +55 -0
  26. package/package.json +59 -0
  27. package/plugins/consent/index.js +40 -0
  28. package/project-compatibility.json +48 -0
  29. package/src/assets.js +181 -0
  30. package/src/build.js +277 -0
  31. package/src/cli.js +344 -0
  32. package/src/concurrency.js +26 -0
  33. package/src/config.js +396 -0
  34. package/src/content.js +160 -0
  35. package/src/generators.js +263 -0
  36. package/src/i18n.js +64 -0
  37. package/src/maintenance.js +122 -0
  38. package/src/markdown.js +65 -0
  39. package/src/page-blocks.js +350 -0
  40. package/src/page-check.js +416 -0
  41. package/src/plugin-api.js +75 -0
  42. package/src/preview.js +167 -0
  43. package/src/report-visuals.js +480 -0
  44. package/src/theme.js +161 -0
  45. package/src/worker.js +139 -0
  46. package/static/.gitkeep +0 -0
  47. package/static/edgepress/language-select.js +5 -0
  48. package/static/edgepress/plugins/advertising/google-adsense.js +26 -0
  49. package/static/edgepress/plugins/captcha/cloudflare-turnstile.js +28 -0
  50. package/static/edgepress/plugins/captcha/google-recaptcha.js +27 -0
  51. package/static/edgepress/plugins/captcha/hcaptcha.js +27 -0
  52. package/static/edgepress/plugins/consent/manager.js +180 -0
  53. package/static/edgepress/plugins/statistics/baidu-tongji.js +8 -0
  54. package/static/edgepress/plugins/statistics/cloudflare-web-analytics.js +9 -0
  55. package/static/edgepress/plugins/statistics/google-analytics.js +13 -0
  56. package/static/edgepress/plugins/tracking/google-tag-manager.js +10 -0
  57. package/static/edgepress/plugins/tracking/meta-pixel.js +18 -0
  58. package/static/edgepress/search.js +84 -0
  59. package/static/edgepress-markdown-guide.svg +21 -0
  60. package/templates/gitignore +6 -0
  61. package/templates/site-readme.md +20 -0
  62. package/themes/atelier/assets/style.css +120 -0
  63. package/themes/atelier/layouts/layout.html +17 -0
  64. package/themes/atelier/layouts/partials/footer.html +4 -0
  65. package/themes/atelier/layouts/partials/head.html +17 -0
  66. package/themes/atelier/layouts/partials/header.html +5 -0
  67. package/themes/atelier/theme.json +6 -0
  68. package/themes/default/assets/style.css +157 -0
  69. package/themes/default/layouts/layout.html +12 -0
  70. package/themes/default/layouts/partials/footer.html +4 -0
  71. package/themes/default/layouts/partials/head.html +19 -0
  72. package/themes/default/layouts/partials/header.html +5 -0
  73. package/themes/default/theme.json +6 -0
  74. package/themes/signal/assets/style.css +113 -0
  75. package/themes/signal/layouts/layout.html +12 -0
  76. package/themes/signal/layouts/partials/footer.html +4 -0
  77. package/themes/signal/layouts/partials/head.html +17 -0
  78. package/themes/signal/layouts/partials/header.html +5 -0
  79. package/themes/signal/theme.json +6 -0
  80. package/wrangler.jsonc +13 -0
@@ -0,0 +1,55 @@
1
+ {
2
+ "home": "Home",
3
+ "archives": "Archives",
4
+ "noPosts": "No posts yet",
5
+ "newer": "Newer posts",
6
+ "older": "Older posts",
7
+ "tags": "Tags",
8
+ "tag": "Tag",
9
+ "notFound": "Page not found",
10
+ "notFoundText": "The page you requested does not exist.",
11
+ "forbidden": "Access denied",
12
+ "forbiddenText": "You do not have permission to access this page.",
13
+ "returnHome": "Return home",
14
+ "allPosts": "All posts",
15
+ "searchPosts": "Search articles",
16
+ "searchIntro": "Search the Markdown articles published on this site. Search runs locally in your browser.",
17
+ "searchLabel": "Search articles",
18
+ "searchButton": "Search",
19
+ "searchHint": "Enter one or more words from an article title or text.",
20
+ "searchPrompt": "Enter a search term to find articles.",
21
+ "searchNeedsJavaScript": "Article search needs JavaScript enabled. The search index stays on this site.",
22
+ "searchNoResults": "No matching articles.",
23
+ "searchResults": "Matching articles",
24
+ "searchResultCount": "Matching articles: {count}.",
25
+ "markdownTaskComplete": "Task complete",
26
+ "markdownTaskIncomplete": "Task incomplete",
27
+ "feed": "Atom feed",
28
+ "languageName": "English",
29
+ "mainNavigationLabel": "Main navigation",
30
+ "footerNavigationLabel": "Footer navigation",
31
+ "languageLabel": "Language",
32
+ "skipToContent": "Skip to content",
33
+ "pagination": "Pagination",
34
+ "pages": "Project pages",
35
+ "latestPosts": "Latest posts",
36
+ "privacySettings": "Privacy settings",
37
+ "privacyNotice": "Optional services",
38
+ "privacyIntro": "Choose which optional services may load. Nothing is enabled until you save or accept your choices.",
39
+ "acceptOptional": "Accept all",
40
+ "rejectOptional": "Reject optional",
41
+ "savePreferences": "Save choices",
42
+ "optionalServices": "Optional services",
43
+ "noIntegrations": "No optional services are configured.",
44
+ "privacyPolicy": "Privacy policy",
45
+ "privacyController": "Site operator",
46
+ "servicePurpose": "Purpose",
47
+ "serviceRetention": "Retention",
48
+ "pluginTracking": "Tracking",
49
+ "pluginStatistics": "Statistics",
50
+ "pluginAdvertising": "Advertising",
51
+ "pluginCaptcha": "Human verification",
52
+ "consentReload": "Your choice was saved. Reloading to apply the updated settings.",
53
+ "agentSummary": "Project guides and reference pages for the EdgePress website builder.",
54
+ "closePrivacy": "Close settings"
55
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "home": "首页",
3
+ "archives": "归档",
4
+ "noPosts": "还没有文章",
5
+ "newer": "较新文章",
6
+ "older": "较早文章",
7
+ "tags": "标签",
8
+ "tag": "标签",
9
+ "notFound": "页面不存在",
10
+ "notFoundText": "你访问的页面不存在。",
11
+ "forbidden": "无权访问",
12
+ "forbiddenText": "你没有权限访问此页面。",
13
+ "returnHome": "返回首页",
14
+ "allPosts": "所有文章",
15
+ "searchPosts": "搜索文章",
16
+ "searchIntro": "搜索此网站发布的 Markdown 文章。搜索在你的浏览器本地运行。",
17
+ "searchLabel": "搜索文章",
18
+ "searchButton": "搜索",
19
+ "searchHint": "输入文章标题或正文中的一个或多个词。",
20
+ "searchPrompt": "输入搜索词以查找文章。",
21
+ "searchNeedsJavaScript": "文章搜索需要启用 JavaScript。搜索索引保存在此网站。",
22
+ "searchNoResults": "没有找到匹配的文章。",
23
+ "searchResults": "匹配的文章",
24
+ "searchResultCount": "匹配文章数:{count}。",
25
+ "markdownTaskComplete": "任务已完成",
26
+ "markdownTaskIncomplete": "任务未完成",
27
+ "feed": "Atom 订阅",
28
+ "languageName": "简体中文",
29
+ "mainNavigationLabel": "主导航",
30
+ "footerNavigationLabel": "页脚导航",
31
+ "languageLabel": "语言",
32
+ "skipToContent": "跳转到正文",
33
+ "pagination": "分页导航",
34
+ "pages": "项目文档",
35
+ "latestPosts": "最新文章",
36
+ "privacySettings": "隐私设置",
37
+ "privacyNotice": "可选服务",
38
+ "privacyIntro": "请选择允许加载的可选服务。保存或接受之前,不会启用任何服务。",
39
+ "acceptOptional": "全部接受",
40
+ "rejectOptional": "拒绝可选服务",
41
+ "savePreferences": "保存选择",
42
+ "optionalServices": "可选服务",
43
+ "noIntegrations": "尚未配置可选服务。",
44
+ "privacyPolicy": "隐私政策",
45
+ "privacyController": "网站运营者",
46
+ "servicePurpose": "用途",
47
+ "serviceRetention": "保留期限",
48
+ "pluginTracking": "跟踪",
49
+ "pluginStatistics": "统计",
50
+ "pluginAdvertising": "广告",
51
+ "pluginCaptcha": "人机验证",
52
+ "consentReload": "选择已保存,正在重新载入以应用设置。",
53
+ "agentSummary": "EdgePress 网站构建器的项目指南与参考文档。",
54
+ "closePrivacy": "关闭设置"
55
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "edgepress",
3
+ "version": "2026.9.3",
4
+ "description": "A static website builder for Cloudflare Workers with editable pages, Markdown posts, local search, themes, and consent-gated integrations.",
5
+ "type": "module",
6
+ "bin": {
7
+ "edgepress": "src/cli.js"
8
+ },
9
+ "files": [
10
+ "content",
11
+ "languages",
12
+ "plugins",
13
+ "src",
14
+ "static",
15
+ "templates",
16
+ "themes",
17
+ "config.yml",
18
+ "edgepress.config.mjs",
19
+ "project-compatibility.json",
20
+ "wrangler.jsonc",
21
+ "README.md"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/jsw-teams/edgepress.git"
26
+ },
27
+ "homepage": "https://github.com/jsw-teams/edgepress#readme",
28
+ "keywords": [
29
+ "static-site-generator",
30
+ "cloudflare-workers",
31
+ "markdown",
32
+ "website-builder",
33
+ "accessibility"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22.12.0"
37
+ },
38
+ "scripts": {
39
+ "new": "node src/cli.js new",
40
+ "build": "node src/cli.js build",
41
+ "generate": "node src/cli.js generate",
42
+ "dev": "node src/cli.js server",
43
+ "preview": "node src/cli.js server",
44
+ "deploy": "node src/cli.js deploy",
45
+ "clean": "node src/cli.js clean",
46
+ "check": "node src/cli.js check",
47
+ "doctor": "node src/cli.js doctor",
48
+ "iterate": "node src/cli.js iterate",
49
+ "security:check": "node src/cli.js security"
50
+ },
51
+ "dependencies": {
52
+ "marked": "^18.0.13",
53
+ "sanitize-html": "^2.17.7",
54
+ "yaml": "^2.9.1"
55
+ },
56
+ "devDependencies": {
57
+ "wrangler": "^4.129.1"
58
+ }
59
+ }
@@ -0,0 +1,40 @@
1
+ function translate(config, locale, key) {
2
+ const base = config.i18n.translationsByLocale?.[config.i18n.defaultLocale];
3
+ const selected = config.i18n.translationsByLocale?.[locale];
4
+ return selected?.[key] ?? base?.[key] ?? key;
5
+ }
6
+
7
+ function localizedUrl(config, locale, path) {
8
+ const prefix = locale === config.i18n.defaultLocale ? '' : '/' + locale;
9
+ const suffix = String(path ?? '').replace(/^\/+/, '');
10
+ return prefix + '/' + suffix;
11
+ }
12
+
13
+ function safeJson(value) {
14
+ return JSON.stringify(value).replace(/</g, '\\u003c').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
15
+ }
16
+
17
+ export default function consentManager(api) {
18
+ api.registerFilter('html:afterLayout', (html, { config, page }) => {
19
+ if (!config.browserPlugins.consent.enabled) return html;
20
+ const locale = page?.locale || config.i18n.defaultLocale;
21
+ const ui = Object.fromEntries([
22
+ 'privacySettings', 'privacyNotice', 'privacyIntro', 'acceptOptional', 'rejectOptional',
23
+ 'savePreferences', 'optionalServices', 'noIntegrations', 'privacyPolicy', 'privacyController',
24
+ 'servicePurpose', 'serviceRetention', 'consentReload', 'closePrivacy'
25
+ ].map((key) => [key, translate(config, locale, key)]));
26
+ const services = ['tracking', 'statistics', 'advertising', 'captcha'].flatMap((group) =>
27
+ config.browserPlugins[group].enabled ? config.browserPlugins[group].services.map((service) => ({ ...service, category: group })) : []
28
+ );
29
+ const privacy = {
30
+ ...config.privacy,
31
+ consent: config.browserPlugins.consent,
32
+ integrations: services
33
+ };
34
+ if (privacy.policyUrl.startsWith('/')) privacy.policyUrl = localizedUrl(config, locale, privacy.policyUrl);
35
+ const payload = safeJson({ privacy, ui, siteLanguage: locale });
36
+ const integration = '<script type="application/json" id="edgepress-privacy-config">' + payload + '</script>' +
37
+ '<script src="/edgepress/plugins/consent/manager.js" defer></script>';
38
+ return html.replace(/<\/body\s*>/i, integration + '</body>');
39
+ });
40
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "frameworkVersion": "2026.9.3",
4
+ "requirements": {
5
+ "nodeMajor": 22,
6
+ "nodeMinimumVersion": "22.12.0",
7
+ "minimumWorkersDate": "2024-09-23",
8
+ "workerRuntime": "ES modules and Web APIs",
9
+ "nodeCompatibilityRequired": false,
10
+ "pluginApiMajor": 1
11
+ },
12
+ "features": [
13
+ { "name": "Static generation and content-hash cache", "stage": "stable", "since": "2026.9.1" },
14
+ { "name": "Cloudflare Static Assets deployment", "stage": "stable", "since": "2026.9.1" },
15
+ { "name": "Row-based editable pages and reusable layout components", "stage": "experimental", "since": "2026.9.1" },
16
+ { "name": "Image/video with text and display typography elements", "stage": "experimental", "since": "2026.9.1" },
17
+ { "name": "i18n locale routes", "stage": "experimental", "since": "2026.9.1" },
18
+ { "name": "Accessibility and agent-friendliness report with screenshots and PDF", "stage": "experimental", "since": "2026.9.1" },
19
+ { "name": "Consent-gated browser plugins", "stage": "experimental", "since": "2026.9.1" },
20
+ { "name": "Service binding and URL/token backend proxy", "stage": "experimental", "since": "2026.9.1" },
21
+ { "name": "Live local preview and theme switching", "stage": "experimental", "since": "2026.9.1" },
22
+ { "name": "Report-only maintenance iteration", "stage": "experimental", "since": "2026.9.1" },
23
+ { "name": "Local browser search across Markdown posts", "stage": "experimental", "since": "2026.9.2" },
24
+ { "name": "Markdown syntax verification and responsive post screenshots", "stage": "experimental", "since": "2026.9.2" },
25
+ { "name": "Compact localized language selector", "stage": "experimental", "since": "2026.9.2" },
26
+ { "name": "npm theme installation and blank theme scaffolds", "stage": "experimental", "since": "2026.9.3" }
27
+ ],
28
+ "releaseHistory": [
29
+ {
30
+ "version": "2026.9.1",
31
+ "date": "2026-09-24",
32
+ "request": 1,
33
+ "notes": "Introduced row-first editable pages, shared theme components, plugin and backend guidance, live preview, and visual audit reports."
34
+ },
35
+ {
36
+ "version": "2026.9.2",
37
+ "date": "2026-09-25",
38
+ "request": 2,
39
+ "notes": "Improved mobile layouts, added local article search and a compact language menu, verified the bilingual Markdown guide, and made the report cover posts with accessible PDF reading structure."
40
+ },
41
+ {
42
+ "version": "2026.9.3",
43
+ "date": "2026-09-25",
44
+ "request": 3,
45
+ "notes": "Published EdgePress for npm installation, added npm theme installation and blank theme packages, documented GitHub and Cloudflare Workers Builds deployment, and added a Cloudflare one-click deployment button."
46
+ }
47
+ ]
48
+ }
package/src/assets.js ADDED
@@ -0,0 +1,181 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { copyFile, readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
3
+ import { dirname, extname, relative, resolve, sep } from 'node:path';
4
+ import * as posix from 'node:path/posix';
5
+ import { mapLimit } from './concurrency.js';
6
+
7
+ async function walk(directory) {
8
+ let entries;
9
+ try { entries = await readdir(directory, { withFileTypes: true }); }
10
+ catch (error) { if (error.code === 'ENOENT') return []; throw error; }
11
+ const files = [];
12
+ for (const entry of entries) {
13
+ const full = resolve(directory, entry.name);
14
+ if (entry.isDirectory()) files.push(...await walk(full));
15
+ else if (entry.isFile()) files.push(full);
16
+ }
17
+ return files;
18
+ }
19
+
20
+ function isCodeAsset(path) {
21
+ const extension = extname(path).toLowerCase();
22
+ return extension === '.css' || extension === '.js';
23
+ }
24
+
25
+ function referencedPaths(path, content, known) {
26
+ if (!isCodeAsset(path)) return [];
27
+ const text = content.toString('utf8');
28
+ const expression = extname(path).toLowerCase() === '.js'
29
+ ? /\b(?:from\s*|import\s*(?:\(\s*)?)['"]([^'"]+)['"]/g
30
+ : /(?:@import\s+(?:url\()?\s*|url\(\s*)['"]?([^'")\s;]+)['"]?\s*\)?/gi;
31
+ const refs = [];
32
+ for (const match of text.matchAll(expression)) {
33
+ const specifier = match[1];
34
+ if (/^(?:[a-z]+:|\/\/|#)/i.test(specifier)) continue;
35
+ const clean = specifier.split(/[?#]/, 1)[0];
36
+ const target = clean.startsWith('/')
37
+ ? clean.slice(1)
38
+ : posix.normalize(posix.join(posix.dirname(path), clean));
39
+ if (known.has(target) && isCodeAsset(target)) refs.push(target);
40
+ }
41
+ return [...new Set(refs)];
42
+ }
43
+
44
+ function rewriteCodeReferences(item, text, outputByOriginal) {
45
+ const extension = extname(item.path).toLowerCase();
46
+ const rewriteSpecifier = (specifier) => {
47
+ if (/^(?:[a-z]+:|\/\/|#)/i.test(specifier)) return null;
48
+ const splitAt = specifier.search(/[?#]/);
49
+ const clean = splitAt < 0 ? specifier : specifier.slice(0, splitAt);
50
+ const tail = splitAt < 0 ? '' : specifier.slice(splitAt);
51
+ const target = clean.startsWith('/')
52
+ ? clean.slice(1)
53
+ : posix.normalize(posix.join(posix.dirname(item.path), clean));
54
+ const outputTarget = outputByOriginal.get(target);
55
+ if (!outputTarget) return null;
56
+ let rewritten;
57
+ if (clean.startsWith('/')) rewritten = '/' + outputTarget;
58
+ else {
59
+ rewritten = posix.relative(posix.dirname(item.outputPath), outputTarget);
60
+ if (!rewritten.startsWith('.')) rewritten = './' + rewritten;
61
+ }
62
+ return rewritten + tail;
63
+ };
64
+
65
+ if (extension === '.js') {
66
+ const expression = /(\b(?:from\s*|import\s*(?:\(\s*)?))(['"])([^'"]+)\2/g;
67
+ return text.replace(expression, (match, prefix, quote, specifier) => {
68
+ const rewritten = rewriteSpecifier(specifier);
69
+ return rewritten === null ? match : prefix + quote + rewritten + quote;
70
+ });
71
+ }
72
+
73
+ const expression = /((?:@import\s+(?:url\()?\s*|url\(\s*))(['"]?)([^)'"\s;]+)(['"]?\s*\)?)/gi;
74
+ return text.replace(expression, (match, prefix, quote, specifier, suffix) => {
75
+ const rewritten = rewriteSpecifier(specifier);
76
+ return rewritten === null ? match : prefix + quote + rewritten + suffix;
77
+ });
78
+ }
79
+
80
+ export async function collectAssets(config) {
81
+ const sources = [];
82
+ for (const source of await walk(config.resolvedPaths.static)) {
83
+ const path = relative(config.resolvedPaths.static, source).split(sep).join('/');
84
+ if (path.split('/').includes('.gitkeep')) continue;
85
+ sources.push({ source, path });
86
+ }
87
+ const themeAssets = resolve(config.resolvedPaths.theme, 'assets');
88
+ for (const source of await walk(themeAssets)) {
89
+ const path = relative(themeAssets, source).split(sep).join('/');
90
+ if (path.split('/').includes('.gitkeep')) continue;
91
+ sources.push({ source, path });
92
+ }
93
+
94
+ const headers = [];
95
+ const assetSources = [];
96
+ for (const item of sources) {
97
+ if (item.path.toLowerCase() === '_headers') headers.push(item);
98
+ else assetSources.push(item);
99
+ }
100
+ const [headerContents, items] = await Promise.all([
101
+ mapLimit(headers, config.concurrency, async (item) => (await readFile(item.source)).toString('utf8')),
102
+ mapLimit(assetSources, config.concurrency, async (item) => ({
103
+ ...item,
104
+ content: isCodeAsset(item.path) ? await readFile(item.source) : null
105
+ }))
106
+ ]);
107
+ const originals = new Set();
108
+ for (const item of items) {
109
+ const normalized = item.path.toLowerCase();
110
+ if (originals.has(normalized)) throw new Error('Static and theme assets have the same output path: ' + item.path);
111
+ originals.add(normalized);
112
+ }
113
+ const byOriginal = new Map(items.map((item) => [item.path, item]));
114
+ const dependencies = new Map();
115
+ for (const item of items) dependencies.set(item.path, referencedPaths(item.path, item.content, byOriginal));
116
+
117
+ const outputByOriginal = new Map();
118
+ for (const item of items) {
119
+ if (!isCodeAsset(item.path)) outputByOriginal.set(item.path, item.path);
120
+ else {
121
+ const reachable = new Set();
122
+ const visit = (path) => {
123
+ for (const dependency of dependencies.get(path) ?? []) {
124
+ if (reachable.has(dependency)) continue;
125
+ reachable.add(dependency);
126
+ visit(dependency);
127
+ }
128
+ };
129
+ visit(item.path);
130
+ const sourceDigests = [...reachable].sort().map((path) =>
131
+ path + ':' + createHash('sha256').update(byOriginal.get(path).content).digest('hex')
132
+ ).join('\n');
133
+ const digest = createHash('sha256')
134
+ .update(item.content)
135
+ .update('\n')
136
+ .update(sourceDigests)
137
+ .digest('hex')
138
+ .slice(0, 16);
139
+ const extension = extname(item.path).toLowerCase();
140
+ outputByOriginal.set(item.path, item.path.slice(0, -extname(item.path).length) + '.' + digest + extension);
141
+ }
142
+ }
143
+
144
+ const assets = items.map((item) => {
145
+ const outputPath = outputByOriginal.get(item.path);
146
+ if (!isCodeAsset(item.path)) return { path: outputPath, source: item.source };
147
+ const content = Buffer.from(rewriteCodeReferences(
148
+ { ...item, outputPath }, item.content.toString('utf8'), outputByOriginal
149
+ ), 'utf8');
150
+ return { path: outputPath, content };
151
+ });
152
+ const urlMap = Object.create(null);
153
+ for (const item of items) urlMap['/' + item.path] = '/' + outputByOriginal.get(item.path);
154
+ config.assetManifest = urlMap;
155
+ return { assets, urlMap, existingHeaders: headerContents.join('\n\n') };
156
+ }
157
+
158
+ export async function writeAssets(bundle, output, concurrency = 8) {
159
+ await mapLimit(bundle.assets, concurrency, async (asset) => {
160
+ const target = resolve(output, ...asset.path.split('/'));
161
+ await mkdir(dirname(target), { recursive: true });
162
+ if (asset.source) await copyFile(asset.source, target);
163
+ else await writeFile(target, asset.content);
164
+ });
165
+ const securityRules = '/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin';
166
+ const immutableRules = bundle.assets
167
+ .filter((asset) => /\.[a-f0-9]{16}\.(?:css|js)$/i.test(asset.path))
168
+ .map((asset) => '/' + asset.path + '\n Cache-Control: public, max-age=31556952, immutable');
169
+ const headers = [bundle.existingHeaders.trim(), securityRules, ...immutableRules].filter(Boolean).join('\n\n') + '\n';
170
+ await writeFile(resolve(output, '_headers'), headers, 'utf8');
171
+ }
172
+
173
+ export function rewriteAssetLinks(html, urlMap) {
174
+ return html.replace(/(\b(?:src|href)\s*=\s*)(["'])([^"']+)\2/gi, (match, prefix, quote, value) => {
175
+ const suffixAt = value.search(/[?#]/);
176
+ const sourcePath = suffixAt < 0 ? value : value.slice(0, suffixAt);
177
+ const suffix = suffixAt < 0 ? '' : value.slice(suffixAt);
178
+ const replacement = urlMap[sourcePath];
179
+ return replacement ? prefix + quote + replacement + suffix + quote : match;
180
+ });
181
+ }