@universalcms/connect 0.1.0 → 0.13.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 +694 -37
- package/dist/angular-runtime.d.mts +4 -0
- package/dist/angular-runtime.mjs +7 -0
- package/dist/angular-setup.mjs +124 -0
- package/dist/astro-runtime.d.mts +6 -0
- package/dist/astro-runtime.mjs +255 -0
- package/dist/astro-setup.mjs +215 -0
- package/dist/astro-source.mjs +451 -0
- package/dist/astro-website.d.mts +5 -0
- package/dist/astro-website.mjs +42 -0
- package/dist/cli.mjs +418 -15
- package/dist/connector-runtime.mjs +130 -0
- package/dist/content-next-setup.mjs +200 -0
- package/dist/content-registry-validation.mjs +40 -0
- package/dist/content-registry.mjs +44 -0
- package/dist/content-setup.mjs +13 -0
- package/dist/content-storefront-setup.mjs +155 -0
- package/dist/content-vite-setup.mjs +107 -0
- package/dist/core.mjs +32 -51
- package/dist/framework-ownership.mjs +68 -0
- package/dist/framework.mjs +54 -0
- package/dist/guided.mjs +335 -0
- package/dist/native-adapters.mjs +31 -0
- package/dist/native-chrome-model.d.mts +3 -0
- package/dist/native-chrome-model.mjs +14 -0
- package/dist/native-chrome-review.mjs +49 -0
- package/dist/native-chrome-source.mjs +69 -0
- package/dist/native-route-review.mjs +152 -0
- package/dist/native-routes.mjs +27 -0
- package/dist/native-website-model.d.mts +18 -0
- package/dist/native-website-model.mjs +67 -0
- package/dist/native-website-server.d.mts +1 -0
- package/dist/native-website-server.mjs +35222 -0
- package/dist/native-website-server.mjs.LEGAL.txt +56 -0
- package/dist/native-website.d.mts +3 -0
- package/dist/native-website.mjs +120 -0
- package/dist/native-website.mjs.LEGAL.txt +56 -0
- package/dist/next-publication.d.mts +12 -0
- package/dist/next-publication.mjs +107 -0
- package/dist/next-settings-layout.mjs +233 -0
- package/dist/next-source.d.mts +8 -0
- package/dist/next-source.mjs +93 -0
- package/dist/page-discovery.mjs +236 -0
- package/dist/page-mapping-review.mjs +160 -0
- package/dist/preflight-recovery.mjs +122 -0
- package/dist/preflight.mjs +138 -0
- package/dist/project-files.mjs +56 -0
- package/dist/public-bindings.d.mts +3 -0
- package/dist/public-bindings.mjs +28 -0
- package/dist/publication-browser.d.mts +5 -0
- package/dist/publication-browser.mjs +258 -0
- package/dist/publication-check.mjs +141 -0
- package/dist/publication-document.d.mts +2 -0
- package/dist/publication-document.mjs +59 -0
- package/dist/publication-next-setup.mjs +134 -0
- package/dist/publication-setup.mjs +138 -0
- package/dist/publication-verification.mjs +85 -0
- package/dist/publication.css +6 -0
- package/dist/publication.d.mts +31 -0
- package/dist/publication.mjs +320 -0
- package/dist/react-publication.d.mts +22 -0
- package/dist/react-publication.mjs +301 -0
- package/dist/react-website.d.mts +15 -0
- package/dist/react-website.mjs +735 -0
- package/dist/reviewed-commerce-ownership.mjs +26 -0
- package/dist/reviewed-commerce-settings-setup.mjs +243 -0
- package/dist/reviewed-native-ownership.mjs +118 -0
- package/dist/reviewed-storefront-source.mjs +227 -0
- package/dist/runtime-dependencies.mjs +59 -0
- package/dist/setup-environment.mjs +70 -0
- package/dist/source-bindings.mjs +77 -0
- package/dist/source-check.mjs +149 -0
- package/dist/source-collect.mjs +80 -0
- package/dist/source-config.mjs +20 -0
- package/dist/source-extract.mjs +514 -0
- package/dist/source-finalize.mjs +52 -0
- package/dist/source-routes.mjs +65 -0
- package/dist/source-setup.mjs +490 -0
- package/dist/staged-setup.mjs +287 -0
- package/dist/static-runtime.mjs +67 -0
- package/dist/static-setup.mjs +136 -0
- package/dist/svelte-runtime.d.mts +27 -0
- package/dist/svelte-runtime.mjs +40 -0
- package/dist/svelte-setup.mjs +135 -0
- package/dist/svelte-template.mjs +48 -0
- package/dist/templates.mjs +167 -3
- package/dist/vendor/astro-compiler/LICENSE +53 -0
- package/dist/vendor/astro-compiler/astro.wasm +0 -0
- package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
- package/dist/vendor/astro-compiler/node/index.js +1 -0
- package/dist/vendor/project-analysis.mjs +56 -4
- package/dist/vendor/react-LICENSE.txt +21 -0
- package/dist/vendor/react-document.mjs +124 -0
- package/dist/vendor/react-dom-LICENSE.txt +21 -0
- package/dist/vendor/react-form.mjs +63 -0
- package/dist/vendor/react-safety.mjs +47 -0
- package/dist/vendor/source-manifest-provider.mjs +104 -0
- package/dist/vendor/typescript-LICENSE.txt +55 -0
- package/dist/vendor/typescript.cjs +200276 -0
- package/dist/vite-publication-server.d.mts +3 -0
- package/dist/vite-publication-server.mjs +82 -0
- package/dist/vite-publication.d.mts +15 -0
- package/dist/vite-publication.mjs +241 -0
- package/dist/vite-source-discovery.mjs +315 -0
- package/dist/vite-source.d.mts +30 -0
- package/dist/vite-source.mjs +141 -0
- package/dist/vue-runtime.d.mts +10 -0
- package/dist/vue-runtime.mjs +50 -0
- package/dist/vue-server.d.mts +2 -0
- package/dist/vue-server.mjs +58 -0
- package/dist/vue-setup.mjs +127 -0
- package/dist/vue-source.mjs +168 -0
- package/dist/website-check.mjs +41 -0
- package/dist/website-http.mjs +149 -0
- package/dist/website-model.d.mts +20 -0
- package/dist/website-model.mjs +155 -0
- package/dist/website-next.d.mts +2 -0
- package/dist/website-next.mjs +29 -0
- package/dist/website-node.d.mts +3 -0
- package/dist/website-node.mjs +50 -0
- package/dist/website-runtime.d.mts +13 -0
- package/dist/website-runtime.mjs +396 -0
- package/dist/website-schema.mjs +22175 -0
- package/dist/website-setup.mjs +256 -0
- package/dist/website.css +49 -0
- package/dist/wizard.mjs +237 -38
- package/package.json +43 -7
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/** Public browser presentation helpers. No key, API origin or private settings. */
|
|
2
|
+
export const WEBSITE_SECTIONS = Object.freeze(['branding', 'social', 'seo', 'languages', 'analytics', 'navigation', 'experience', 'scripts', 'redirects', 'authors', 'templates', 'taxonomies']);
|
|
3
|
+
export const record = value => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
4
|
+
export const text = (value, limit = 2000) => typeof value === 'string' ? value.slice(0, limit) : '';
|
|
5
|
+
const states = new Set(['ready', 'missing', 'denied', 'unavailable']);
|
|
6
|
+
export function emptyWebsiteSettings() {
|
|
7
|
+
return { version: 1, state: 'unavailable', sections: Object.fromEntries(WEBSITE_SECTIONS.map(name => [name, { state: 'unavailable', version: 0 }])) };
|
|
8
|
+
}
|
|
9
|
+
export function parseWebsiteSettings(value) {
|
|
10
|
+
const input = record(value);
|
|
11
|
+
if (input.version !== 1 || !['ready', 'unavailable'].includes(input.state) || Object.keys(record(input.sections)).some(name => !WEBSITE_SECTIONS.includes(name))) throw Error('Invalid website settings');
|
|
12
|
+
const sections = {};
|
|
13
|
+
for (const name of WEBSITE_SECTIONS) {
|
|
14
|
+
const section = record(record(input.sections)[name]);
|
|
15
|
+
if (!states.has(section.state) || !Number.isSafeInteger(section.version) || section.version < 0 || (section.state === 'ready' && (!section.data || Array.isArray(section.data) || typeof section.data !== 'object'))) throw Error('Invalid website settings');
|
|
16
|
+
// Never retain data after an access denial or failed read.
|
|
17
|
+
sections[name] = { state: section.state, version: section.version, ...(section.state === 'ready' && input.state === 'ready' ? { data: section.data } : {}) };
|
|
18
|
+
}
|
|
19
|
+
return { version: 1, state: input.state, sections };
|
|
20
|
+
}
|
|
21
|
+
export function sectionData(settings, name) {
|
|
22
|
+
const section = record(record(record(settings).sections)[name]);
|
|
23
|
+
return record(settings).state === 'ready' && section.state === 'ready' ? record(section.data) : null;
|
|
24
|
+
}
|
|
25
|
+
export function publicHref(value, { mail = false, relative = true } = {}) {
|
|
26
|
+
if (typeof value !== 'string' || !value || value.length > 2048 || /[\s\\\u0000-\u001f\u007f]|%(?:0[0-9a-f]|1[0-9a-f]|7f|5c)/i.test(value)) return null;
|
|
27
|
+
if (mail && /^mailto:/i.test(value)) {
|
|
28
|
+
const address = value.slice(7);
|
|
29
|
+
if (address.length > 254 || /[?#%,;]/.test(address)) return null;
|
|
30
|
+
const [local, domain, extra] = address.split('@');
|
|
31
|
+
return !extra && local?.length <= 64 && /^[A-Za-z0-9!$&'*+/=^_`{|}~-]+(?:\.[A-Za-z0-9!$&'*+/=^_`{|}~-]+)*$/.test(local) && /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/.test(domain ?? '') ? `mailto:${address}` : null;
|
|
32
|
+
}
|
|
33
|
+
if (relative && (/^\/(?!\/)/.test(value) || /^#[a-zA-Z0-9_-]+$/.test(value))) {
|
|
34
|
+
if (/(?:^|\/)(?:\.|\.\.)(?:\/|$)/.test(value) || /%2[ef]/i.test(value)) return null;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(value);
|
|
39
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password ? url.href : null;
|
|
40
|
+
} catch { return null; }
|
|
41
|
+
}
|
|
42
|
+
function link(value) {
|
|
43
|
+
const row = record(value), url = publicHref(row.url), label = text(row.label, 80);
|
|
44
|
+
return url && label ? { label, url } : null;
|
|
45
|
+
}
|
|
46
|
+
export function navigationLinks(settings, location, originalLinks = [], excludeUrls = []) {
|
|
47
|
+
const original = (Array.isArray(originalLinks) ? originalLinks : []).slice(0, 200).map(link).filter(Boolean);
|
|
48
|
+
const nativeUrls = new Set([...original.map(item => item.url), ...(Array.isArray(excludeUrls) ? excludeUrls.slice(0, 200).map(value => publicHref(value)).filter(Boolean) : [])]);
|
|
49
|
+
const data = sectionData(settings, 'navigation'), saved = data && Array.isArray(data[location]) ? data[location].slice(0, 40) : [];
|
|
50
|
+
const extra = saved.flatMap(value => {
|
|
51
|
+
const row = record(value), label = text(row.label, 80);
|
|
52
|
+
if (!label) return [];
|
|
53
|
+
const children = location === 'header' && Array.isArray(row.children) ? row.children.slice(0, 20).map(link).filter(item => item && !nativeUrls.has(item.url)) : [];
|
|
54
|
+
const direct = link(row);
|
|
55
|
+
if (Array.isArray(row.children) && row.children.length) return children.length ? [{ label, ...(direct && !nativeUrls.has(direct.url) ? { url: direct.url } : {}), children }] : [];
|
|
56
|
+
return direct && !nativeUrls.has(direct.url) ? [direct] : [];
|
|
57
|
+
});
|
|
58
|
+
return [...original, ...extra];
|
|
59
|
+
}
|
|
60
|
+
function locale(value) { try { return typeof value === 'string' && value.length <= 35 ? Intl.getCanonicalLocales(value)[0] : null; } catch { return null; } }
|
|
61
|
+
export function languageLinks(settings, entry) {
|
|
62
|
+
const languages = sectionData(settings, 'languages');
|
|
63
|
+
if (!languages || !Array.isArray(languages.enabled)) return [];
|
|
64
|
+
const enabled = new Set(languages.enabled.map(locale).filter(Boolean)), current = record(entry), rows = [current, ...(Array.isArray(current.translations) ? current.translations.slice(0, 100) : [])], seen = new Set();
|
|
65
|
+
return rows.flatMap(value => {
|
|
66
|
+
const row = record(value), language = locale(row.locale), path = publicHref(row.path);
|
|
67
|
+
if (!language || !enabled.has(language) || !path?.startsWith('/') || seen.has(language)) return [];
|
|
68
|
+
seen.add(language); return [{ locale: language, path, title: text(row.title || record(row.document).title, 200), current: language === locale(current.locale) }];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function experienceCopy(settings, language) {
|
|
72
|
+
const base = sectionData(settings, 'experience');
|
|
73
|
+
if (!base) return null;
|
|
74
|
+
const canonical = locale(language), translation = Array.isArray(base.translations) ? base.translations.find(value => locale(record(value).locale) === canonical) : null;
|
|
75
|
+
return { ...base, ...record(translation) };
|
|
76
|
+
}
|
|
77
|
+
export function consentPolicy(settings) {
|
|
78
|
+
const experience = sectionData(settings, 'experience');
|
|
79
|
+
if (!experience || experience.cookieConsentEnabled !== true) return null;
|
|
80
|
+
return { version: text(experience.consentVersion, 80) || '1', maxAgeDays: Number.isInteger(experience.consentMaxAgeDays) && experience.consentMaxAgeDays >= 1 && experience.consentMaxAgeDays <= 365 ? experience.consentMaxAgeDays : 180 };
|
|
81
|
+
}
|
|
82
|
+
export function parseConsent(value, policy, now = Date.now()) {
|
|
83
|
+
if (!policy || typeof value !== 'string' || value.length > 1024) return null;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = record(JSON.parse(value));
|
|
86
|
+
if (parsed.version !== policy.version || typeof parsed.analytics !== 'boolean' || typeof parsed.marketing !== 'boolean' || !Number.isSafeInteger(parsed.savedAt) || !Number.isSafeInteger(parsed.expiresAt) || parsed.savedAt > now || parsed.expiresAt <= now || parsed.expiresAt <= parsed.savedAt || parsed.expiresAt - parsed.savedAt > policy.maxAgeDays * 86400000 || now - parsed.savedAt >= policy.maxAgeDays * 86400000) return null;
|
|
87
|
+
return { version: parsed.version, analytics: parsed.analytics, marketing: parsed.marketing, savedAt: parsed.savedAt, expiresAt: parsed.expiresAt };
|
|
88
|
+
} catch { return null; }
|
|
89
|
+
}
|
|
90
|
+
export function createConsent(policy, choices, now = Date.now()) {
|
|
91
|
+
if (!policy) return null;
|
|
92
|
+
return { version: policy.version, analytics: choices.analytics === true, marketing: choices.marketing === true, savedAt: now, expiresAt: now + policy.maxAgeDays * 86400000 };
|
|
93
|
+
}
|
|
94
|
+
/** Configuration only: never accepts inline text, event handlers or arbitrary attributes. */
|
|
95
|
+
export function scriptDescriptors(settings, decision, privacySignal = false, now = Date.now()) {
|
|
96
|
+
const policy = consentPolicy(settings), consent = parseConsent(JSON.stringify(decision), policy, now), result = [];
|
|
97
|
+
const scripts = sectionData(settings, 'scripts');
|
|
98
|
+
for (const value of Array.isArray(scripts?.entries) ? scripts.entries.slice(0, 20) : []) {
|
|
99
|
+
const row = record(value), src = publicHref(row.src, { relative: false });
|
|
100
|
+
if (row.enabled !== true || !src || !['head', 'body'].includes(row.placement) || !['essential', 'analytics', 'marketing'].includes(row.category)) continue;
|
|
101
|
+
if (row.category !== 'essential' && (privacySignal || consent?.[row.category] !== true)) continue;
|
|
102
|
+
result.push({ src, placement: row.placement, category: row.category, provider: 'external', attributes: {} });
|
|
103
|
+
}
|
|
104
|
+
const analytics = sectionData(settings, 'analytics');
|
|
105
|
+
const experienceState = settings?.sections?.experience?.state;
|
|
106
|
+
const policyKnown = experienceState === 'ready' || experienceState === 'missing';
|
|
107
|
+
if (analytics && !privacySignal && (consent?.analytics === true || analytics.requireConsent === false && policyKnown && !policy)) {
|
|
108
|
+
const google = text(analytics.googleMeasurementId, 50), clarity = text(analytics.clarityProjectId, 50), domain = text(analytics.plausibleDomain, 253), matomo = publicHref(analytics.matomoUrl, { relative: false }), site = text(analytics.matomoSiteId, 20);
|
|
109
|
+
if (/^G-[A-Z0-9]+$/.test(google)) result.push({ src: `https://www.googletagmanager.com/gtag/js?id=${google}`, placement: 'head', category: 'analytics', provider: 'google', id: google, attributes: {} });
|
|
110
|
+
if (/^[a-zA-Z0-9]+$/.test(clarity)) result.push({ src: `https://www.clarity.ms/tag/${clarity}`, placement: 'head', category: 'analytics', provider: 'clarity', id: clarity, attributes: {} });
|
|
111
|
+
if (/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/.test(domain)) result.push({ src: 'https://plausible.io/js/script.js', placement: 'head', category: 'analytics', provider: 'plausible', attributes: { 'data-domain': domain } });
|
|
112
|
+
if (matomo && /^\d+$/.test(site) && !new URL(matomo).search && !new URL(matomo).hash) { const base = matomo.replace(/\/?$/, '/'); result.push({ src: `${base}matomo.js`, placement: 'head', category: 'analytics', provider: 'matomo', id: site, tracker: `${base}matomo.php`, attributes: {} }); }
|
|
113
|
+
}
|
|
114
|
+
const seen = new Set(); return result.filter(item => { if (seen.has(item.src)) return false; seen.add(item.src); return true; });
|
|
115
|
+
}
|
|
116
|
+
export function pagePresentation(settings, entry) {
|
|
117
|
+
const row = record(entry), templates = sectionData(settings, 'templates'), template = record(templates?.[row.kind === 'post' ? 'post' : 'page']);
|
|
118
|
+
const seo = record(row.seo), image = record(seo.featuredImage), authors = sectionData(settings, 'authors'), taxonomies = sectionData(settings, 'taxonomies');
|
|
119
|
+
const author = authors && Array.isArray(authors.entries) ? authors.entries.find(value => record(value).id === (record(row.author).id || seo.authorId)) : null;
|
|
120
|
+
const terms = name => taxonomies && Array.isArray(taxonomies[name]) && Array.isArray(row[name]) ? row[name].slice(0, 100).flatMap(value => { const term = record(value), saved = taxonomies[name].find(item => record(item).id === term.id), path = publicHref(term.path); return saved && path?.startsWith('/') ? [{ id: text(term.id, 80), name: text(record(saved).name, 80), path }] : []; }) : [];
|
|
121
|
+
return { layout: ['standard', 'narrow', 'wide'].includes(template.layout) ? template.layout : 'standard', showFeaturedImage: template.showFeaturedImage === true, featuredImage: { url: publicHref(image.url, { relative: false }), alt: text(image.alt, 300) }, showTableOfContents: template.showTableOfContents === true, showBreadcrumbs: template.showBreadcrumbs === true, showShare: template.showShare === true, showRelated: template.showRelated === true, showDate: template.showDate === true, author: template.showAuthor === true && author ? { id: text(author.id, 80), name: text(author.name, 100), bio: text(author.bio, 1000), jobTitle: text(author.jobTitle, 100), avatarUrl: publicHref(author.avatarUrl, { relative: false }), url: publicHref(author.url) } : null, categories: terms('categories'), tags: terms('tags') };
|
|
122
|
+
}
|
|
123
|
+
export function websiteJsonLd(settings, entry, { title, description, canonical, image } = {}) {
|
|
124
|
+
if (!sectionData(settings, 'seo')) return null;
|
|
125
|
+
const row = record(entry), schema = record(record(row.seo).schema), templates = sectionData(settings, 'templates');
|
|
126
|
+
const kind = schema.type && schema.type !== 'none' ? schema.type : record(templates?.[row.kind]).schemaType;
|
|
127
|
+
if (kind === 'custom') {
|
|
128
|
+
const value = record(schema.json), serialized = JSON.stringify(value);
|
|
129
|
+
return Object.keys(value).length && serialized.length <= 20000 && !/<\/?script/i.test(serialized) ? value : null;
|
|
130
|
+
}
|
|
131
|
+
if (!['WebPage', 'Article', 'BlogPosting', 'NewsArticle', 'FAQPage', 'HowTo', 'Product', 'Organization'].includes(kind)) return null;
|
|
132
|
+
const data = { '@context': 'https://schema.org', '@type': kind, name: text(title, 500), description: text(description, 2000), ...(canonical && publicHref(canonical, { relative: false }) ? { url: canonical } : {}), ...(image && publicHref(image, { relative: false }) ? { image } : {}) };
|
|
133
|
+
const authors = sectionData(settings, 'authors'), author = Array.isArray(authors?.entries) ? authors.entries.find(item => record(item).id === (record(row.author).id || record(row.seo).authorId)) : null;
|
|
134
|
+
if (author) data.author = { '@type': 'Person', name: text(author.name, 100), ...(publicHref(author.url) ? { url: author.url } : {}) };
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
/** Stream bound and abort-aware so a stalled body cannot keep old settings active. */
|
|
138
|
+
export async function readWebsiteJson(response, signal, limit = 2 * 1024 * 1024) {
|
|
139
|
+
if (response.status !== 200 || response.redirected || !/^application\/json(?:;|$)/i.test(response.headers.get('content-type') ?? '') || Number(response.headers.get('content-length')) > limit || !response.body) throw Error('Website response unavailable');
|
|
140
|
+
const reader = response.body.getReader(), chunks = []; let size = 0;
|
|
141
|
+
const abort = () => { void reader.cancel().catch(() => {}); };
|
|
142
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
143
|
+
try {
|
|
144
|
+
while (true) {
|
|
145
|
+
if (signal?.aborted) throw Error('Website response canceled');
|
|
146
|
+
const { done, value } = await reader.read();
|
|
147
|
+
if (signal?.aborted) throw Error('Website response canceled');
|
|
148
|
+
if (done) break;
|
|
149
|
+
size += value.byteLength; if (size > limit) throw Error('Website response too large'); chunks.push(value);
|
|
150
|
+
}
|
|
151
|
+
const bytes = new Uint8Array(size); let offset = 0;
|
|
152
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
153
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
154
|
+
} finally { signal?.removeEventListener('abort', abort); await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
155
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { WebsiteSection } from './website-runtime.mjs';
|
|
2
|
+
export function createNextWebsiteProxy(options: {configuration:unknown;sourceConfiguration?:unknown;contentBindings?:unknown;env?:Record<string,string|undefined>;fetcher?:typeof fetch;nativePathPrefixes?:string[];installedSections?:readonly WebsiteSection[]}): (request:Request)=>Promise<{response?:Response;requestHeaders:Headers;headers:Readonly<Record<string,string>>}>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createWebsiteRequestHandler } from './website-http.mjs';
|
|
2
|
+
import { createNextPublicationProxy, nextPublicationPath } from './next-publication.mjs';
|
|
3
|
+
/** Compose settings/new public routes ahead of the original publication guard. */
|
|
4
|
+
export function createNextWebsiteProxy(options) {
|
|
5
|
+
const website=createWebsiteRequestHandler(options),publication=createNextPublicationProxy(options);
|
|
6
|
+
return async request=>{
|
|
7
|
+
let target=request,layoutPath=null;
|
|
8
|
+
try {
|
|
9
|
+
const url=new URL(request.url),pathname=nextPublicationPath(url.pathname,options.configuration.routes.some(route=>route.path==='/index'));
|
|
10
|
+
layoutPath=encodeURIComponent(pathname);
|
|
11
|
+
if(pathname!==url.pathname){url.pathname=pathname;target=new Request(url,request);}
|
|
12
|
+
}catch{/* The original guard retains invalid-path rejection. */}
|
|
13
|
+
const result=await website(target);
|
|
14
|
+
if(result instanceof Response){
|
|
15
|
+
// Next's proxy transport requires an absolute Location, including when
|
|
16
|
+
// the saved CMS redirect deliberately stays within this website.
|
|
17
|
+
if([301,302,307,308].includes(result.status)&&result.headers.get('location')?.startsWith('/'))result.headers.set('Location',new URL(result.headers.get('location'),options.configuration.websiteOrigin).href);
|
|
18
|
+
return {response:result,requestHeaders:new Headers(),headers:{}};
|
|
19
|
+
}
|
|
20
|
+
if(result?.pass){const cleaned=new Headers(request.headers);for(const name of [...cleaned.keys()])if(/^(x-universalcms-|x-cms-publication|x-middleware-subrequest$)/i.test(name))cleaned.delete(name);if(layoutPath!==null)cleaned.set('x-universalcms-layout-path',layoutPath);return {requestHeaders:cleaned,headers:{'Cache-Control':'private, no-store, max-age=0','CDN-Cache-Control':'no-store','Vercel-CDN-Cache-Control':'no-store'}};}
|
|
21
|
+
const checked=await publication(request);
|
|
22
|
+
// The root layout cannot obtain its pathname from route params. Supply it
|
|
23
|
+
// only after the proxy has removed all incoming integration headers and
|
|
24
|
+
// retained the actual normalized request path. This is display context;
|
|
25
|
+
// runtime.page still enforces current publication and native ownership.
|
|
26
|
+
if(!checked.response&&layoutPath!==null)checked.requestHeaders.set('x-universalcms-layout-path',layoutPath);
|
|
27
|
+
return checked;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function universalCmsWebsite(options:{configuration:unknown;sourceConfiguration?:unknown;contentBindings?:unknown}):unknown;
|
|
2
|
+
export function createWebsiteServer(options:{root:string;configuration:unknown;sourceConfiguration?:unknown;contentBindings?:unknown;env?:Record<string,string|undefined>;fetcher?:typeof fetch}): import('node:http').Server;
|
|
3
|
+
export function readWebsiteHtml(root:string,built?:boolean):Promise<string>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { createWebsiteRequestHandler } from './website-http.mjs';
|
|
6
|
+
import { readServerEnvironment } from './vite-publication.mjs';
|
|
7
|
+
import { createVitePublicationServer } from './vite-publication-server.mjs';
|
|
8
|
+
const stamp=stat=>[stat.dev,stat.ino,stat.size,stat.mtimeMs,stat.ctimeMs,stat.nlink].join(':');
|
|
9
|
+
export async function readWebsiteHtml(root,built=false) {
|
|
10
|
+
const actual=await realpath(root);let filename=actual;
|
|
11
|
+
for(const part of built?['dist','index.html']:['index.html']) {
|
|
12
|
+
filename=path.join(filename,part);const stat=await lstat(filename);
|
|
13
|
+
if(stat.isSymbolicLink()||await realpath(filename)!==filename||(part==='index.html'? !stat.isFile()||stat.nlink!==1:!stat.isDirectory())) throw Error('Invalid website HTML');
|
|
14
|
+
}
|
|
15
|
+
const file=await open(filename,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
16
|
+
try {
|
|
17
|
+
const before=await file.stat();
|
|
18
|
+
if(!before.isFile()||before.nlink!==1||before.size>2*1024*1024)throw Error('Invalid website HTML');
|
|
19
|
+
// Allocate only the measured, bounded size. An append during the read must
|
|
20
|
+
// fail the final stamp check rather than grow an unbounded readFile buffer.
|
|
21
|
+
const body=Buffer.alloc(before.size);let size=0;
|
|
22
|
+
while(size<body.length){const read=await file.read(body,size,body.length-size,size);if(!read.bytesRead)break;size+=read.bytesRead;}
|
|
23
|
+
if(size!==before.size||stamp(before)!==stamp(await file.stat())||stamp(before)!==stamp(await lstat(filename)))throw Error('Website HTML changed');
|
|
24
|
+
return new TextDecoder('utf-8',{fatal:true}).decode(body);
|
|
25
|
+
}finally{await file.close();}
|
|
26
|
+
}
|
|
27
|
+
export function createWebsiteNodeHandler({root,configuration,sourceConfiguration,contentBindings,env,fetcher,renderHtml}) {
|
|
28
|
+
const handle=createWebsiteRequestHandler({configuration,sourceConfiguration,contentBindings,env:env??(()=>readServerEnvironment(root)),fetcher,renderHtml});
|
|
29
|
+
return async(request,response)=>{
|
|
30
|
+
if(!request.url?.startsWith('/')||request.url.startsWith('//')||/[\\\s#]|%(?:2e|2f|5c|25|3f|23)/i.test(request.url.split('?')[0])||request.headers.host!==new URL(configuration.websiteOrigin).host) {
|
|
31
|
+
response.writeHead(400,{'Cache-Control':'no-store'});response.end();return true;
|
|
32
|
+
}
|
|
33
|
+
const result=await handle(new Request(new URL(request.url,configuration.websiteOrigin),{method:request.method,headers:new Headers(Object.entries(request.headers).flatMap(([name,value])=>value===undefined?[]:[[name,Array.isArray(value)?value.join(', '):value]]))}));
|
|
34
|
+
if(!result||result.pass)return false;
|
|
35
|
+
response.writeHead(result.status,Object.fromEntries(result.headers));response.end(Buffer.from(await result.arrayBuffer()));return true;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function universalCmsWebsite({configuration,sourceConfiguration,contentBindings}) {
|
|
39
|
+
let root;
|
|
40
|
+
function install(server,built) {
|
|
41
|
+
const handler=createWebsiteNodeHandler({root,configuration,sourceConfiguration,contentBindings,renderHtml:async pathname=>{const html=await readWebsiteHtml(root,built);return built?html:server.transformIndexHtml(pathname,html);}});
|
|
42
|
+
server.middlewares.use((request,response,next)=>{void handler(request,response).then(handled=>{if(!handled)next();},()=>{if(!response.writableEnded){response.writeHead(503,{'Cache-Control':'no-store'});response.end('Website unavailable');}});});
|
|
43
|
+
}
|
|
44
|
+
return {name:'universalcms-website-settings',enforce:'pre',configResolved(config){root=config.root;},configureServer(server){install(server,false);},configurePreviewServer(server){install(server,true);}};
|
|
45
|
+
}
|
|
46
|
+
export function createWebsiteServer(options) {
|
|
47
|
+
const original=createVitePublicationServer(options);
|
|
48
|
+
const handler=createWebsiteNodeHandler({...options,renderHtml:()=>readWebsiteHtml(options.root,true)});
|
|
49
|
+
return createServer({requestTimeout:15000,headersTimeout:10000,maxHeaderSize:8192},(request,response)=>{void handler(request,response).then(handled=>{if(!handled)original.emit('request',request,response);},()=>{if(!response.writableEnded){response.writeHead(503,{'Cache-Control':'no-store'});response.end('Website unavailable');}});});
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PublicationConfiguration, PublishedDocumentEntry } from './publication.mjs';
|
|
2
|
+
export type WebsiteSection = 'branding' | 'social' | 'seo' | 'languages' | 'analytics' | 'navigation' | 'experience' | 'scripts' | 'redirects' | 'authors' | 'templates' | 'taxonomies';
|
|
3
|
+
export const websiteSections: readonly WebsiteSection[];
|
|
4
|
+
export type WebsiteSectionState = { state: 'ready' | 'missing' | 'denied' | 'unavailable'; version: number; data?: Record<string, unknown> };
|
|
5
|
+
export type WebsiteSnapshot = { version: 1; state: 'ready' | 'unavailable'; sections: Record<WebsiteSection, WebsiteSectionState> };
|
|
6
|
+
export type WebsiteEntry = PublishedDocumentEntry & { path: string; canonicalUrl?: string; seo?: Record<string, unknown>; author?: Record<string, unknown> | null; categories?: Array<{ id: string; name: string; slug: string; path: string }>; tags?: Array<{ id: string; name: string; slug: string; path: string }>; translations?: Array<{ id: string; locale: string; path: string; title: string }> };
|
|
7
|
+
export type WebsiteSummary = { id: string; kind: string; slug: string; locale: string; path: string; title: string; excerpt: string; publishedAt: string };
|
|
8
|
+
export type WebsiteResult = { state: 'published'; status: 200; path: string; entry: WebsiteEntry; revision: string; related: WebsiteSummary[] } | { state: 'archive'; status: 200; kind: 'category' | 'tag'; path: string; title: string; description: string; entries: WebsiteEntry[] } | { state: 'redirect'; status: 301 | 302 | 307 | 308; path: string; location: string } | { state: 'gone'; status: 410; path: string } | { state: 'missing' | 'unmanaged' | 'unavailable'; status: 200 | 404 | 503 };
|
|
9
|
+
export type WebsiteRuntimeOptions = { configuration: PublicationConfiguration; env?: Record<string, string | undefined> | (() => Record<string, string | undefined> | Promise<Record<string, string | undefined>>); fetcher?: typeof fetch; nativePaths?: string[]; nativePatterns?: string[]; nativePathPrefixes?: string[]; installedSections?: readonly WebsiteSection[]; timeoutMs?: number; maxResponseBytes?: number; maxConcurrent?: number };
|
|
10
|
+
export type WebsiteSeoResponse = { state: 'missing' | 'denied' | 'unavailable' };
|
|
11
|
+
export function createWebsiteRuntime(options: WebsiteRuntimeOptions): { settings(): Promise<WebsiteSnapshot>; resolve(pathname: string): Promise<WebsiteResult>; page(pathname: string): Promise<WebsiteResult>; sitemap(): Promise<WebsiteSeoResponse | { state: 'ready'; enabled: boolean; canonicalBase: string; entries: Array<{ url: string; path: string; lastModified: string; locale: string }> }>; robots(): Promise<WebsiteSeoResponse | { state: 'ready'; body: string }> };
|
|
12
|
+
export type WebsiteMetadata = { title: string; description: string; canonical?: string; robots: { index: boolean; follow: boolean }; openGraph?: { title: string; description: string; type: string; url?: string; image?: string; imageAlt?: string }; twitter?: { card: string; site?: string }; alternates?: Array<{ locale: string; url: string }>; jsonLd?: Record<string, unknown> };
|
|
13
|
+
export function websiteMetadata(entry: PublishedDocumentEntry | WebsiteEntry | null, snapshot: WebsiteSnapshot, pathname: string, websiteOrigin?: string): WebsiteMetadata;
|