@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,152 @@
|
|
|
1
|
+
/** Explicit website-owned route review. No source execution or private reads. */
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import ts from './vendor/typescript.cjs';
|
|
6
|
+
import { projectRoot, readProjectFile, applyPlan, SetupError } from './core.mjs';
|
|
7
|
+
import { inspectSetupFramework } from './framework.mjs';
|
|
8
|
+
import { inspectSourceRoutes } from './source-routes.mjs';
|
|
9
|
+
import { inspectReviewedNativeOwnership, NATIVE_ROUTES_FILE } from './reviewed-native-ownership.mjs';
|
|
10
|
+
import { REVIEWED_NEXT_NATIVE_PROFILE, isNativeReviewFilePath, validateNativeRoutes } from './native-routes.mjs';
|
|
11
|
+
import { isExcludedPublicationPath } from './publication.mjs';
|
|
12
|
+
import { sanitizeSetupPaths } from './preflight-recovery.mjs';
|
|
13
|
+
|
|
14
|
+
const fail = message => { throw new SetupError(message); };
|
|
15
|
+
const digest = text => createHash('sha256').update(text).digest('hex');
|
|
16
|
+
const json = value => JSON.stringify(value, null, 2) + '\n';
|
|
17
|
+
const extensions = ['ts', 'tsx', 'js', 'jsx'];
|
|
18
|
+
const installedFiles = ['.universalcms/content.bindings.json', '.universalcms/publication.config.json', '.universalcms/website.integration.json', '.universalcms/reviewed.integration.json'];
|
|
19
|
+
const configurationFiles = ['package.json', 'tsconfig.json', 'jsconfig.json', ...['js','mjs','cjs','ts'].map(ext => `next.config.${ext}`)];
|
|
20
|
+
|
|
21
|
+
async function beforeInstallation(root) {
|
|
22
|
+
for (const name of installedFiles) {
|
|
23
|
+
if (await readProjectFile(root, name) !== null) fail('An integration is already installed. Preserve its route ownership and use a reviewed adapter update; review-routes never reassigns installed content.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function inspectNativeRouteCandidates(directory) {
|
|
28
|
+
const root = await projectRoot(directory);
|
|
29
|
+
const framework = await inspectSetupFramework(root);
|
|
30
|
+
if (framework.framework !== 'next') fail('review-routes supports Next.js App Router native routes. Other custom routers need their framework-specific reviewed adapter; no files changed.');
|
|
31
|
+
await beforeInstallation(root);
|
|
32
|
+
const inventory = await inspectSourceRoutes(root);
|
|
33
|
+
if (!Object.values(inventory.files).length) fail('Native route review requires at least one existing public App Router page.');
|
|
34
|
+
const app = Object.values(inventory.files)[0].startsWith('src/app/') ? 'src/app' : 'app';
|
|
35
|
+
const candidates = [], unsupported = [];
|
|
36
|
+
for (const route of inventory.routes.sort()) {
|
|
37
|
+
const pathname = '/' + route, file = inventory.files[route || 'home'];
|
|
38
|
+
// Route groups do not imply ownership of their siblings or layouts.
|
|
39
|
+
if (!extensions.some(ext => file === `${app}${route ? '/' + route : ''}/page.${ext}`)) continue;
|
|
40
|
+
try { validateNativeRoutes({ nativePaths: [pathname], nativePatterns: [] }); }
|
|
41
|
+
catch { continue; }
|
|
42
|
+
candidates.push({ path: pathname, kind: 'static', file });
|
|
43
|
+
}
|
|
44
|
+
for (const directory of inventory.unresolvedPaths.sort()) {
|
|
45
|
+
const pathname = directory.startsWith(app + '/') ? directory.slice(app.length) : '';
|
|
46
|
+
try { validateNativeRoutes({ nativePaths: [], nativePatterns: [pathname] }); }
|
|
47
|
+
catch { unsupported.push(directory); continue; }
|
|
48
|
+
const files = [];
|
|
49
|
+
for (const extension of extensions) if (await readProjectFile(root, `${directory}/page.${extension}`) !== null) files.push(`${directory}/page.${extension}`);
|
|
50
|
+
if (files.length !== 1) { unsupported.push(directory); continue; }
|
|
51
|
+
candidates.push({ path: pathname, kind: 'dynamic', file: files[0] });
|
|
52
|
+
}
|
|
53
|
+
const safe = sanitizeSetupPaths(unsupported);
|
|
54
|
+
return { root, framework: 'next', app, candidates, unsupported: safe.paths, omittedPathCount: safe.omittedPathCount };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Only local, bounded public source modules may be inventoried. Private env
|
|
58
|
+
// files, paths outside the project, aliases and computed imports are not read.
|
|
59
|
+
async function dependencyClosure(root, app, seeds) {
|
|
60
|
+
const seen = new Map(); let bytes = 0;
|
|
61
|
+
async function visit(filename) {
|
|
62
|
+
if (seen.has(filename)) return;
|
|
63
|
+
if (!isNativeReviewFilePath(filename)) fail('A native dependency is outside ordinary application source. Review it manually; private files were not read.');
|
|
64
|
+
const content = await readProjectFile(root, filename);
|
|
65
|
+
if (content === null || (await lstat(path.join(root, filename))).nlink !== 1) fail('Native review requires existing ordinary source files without links.');
|
|
66
|
+
bytes += Buffer.byteLength(content);
|
|
67
|
+
if (seen.size >= 200 || bytes > 8 * 1024 * 1024) fail('Native source review exceeds its file or byte budget.');
|
|
68
|
+
seen.set(filename, content);
|
|
69
|
+
if (!/\.[cm]?[jt]sx?$/.test(filename)) return;
|
|
70
|
+
const ast = ts.createSourceFile(filename, content, ts.ScriptTarget.Latest, true, /x$/.test(filename) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
71
|
+
if (ast.parseDiagnostics.length) fail('A native source module cannot be parsed. Correct its syntax before route review.');
|
|
72
|
+
const imports = [];
|
|
73
|
+
function scan(node) {
|
|
74
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) imports.push(node.moduleSpecifier.text);
|
|
75
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
|
|
76
|
+
if (node.arguments.length !== 1 || !ts.isStringLiteral(node.arguments[0])) fail('Computed native imports require a reviewed adapter.');
|
|
77
|
+
imports.push(node.arguments[0].text);
|
|
78
|
+
}
|
|
79
|
+
ts.forEachChild(node, scan);
|
|
80
|
+
}
|
|
81
|
+
scan(ast);
|
|
82
|
+
for (const specifier of imports) {
|
|
83
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('@/')) continue;
|
|
84
|
+
const base = specifier.startsWith('@/') ? (app.startsWith('src/') ? 'src/' : '') + specifier.slice(2) : path.posix.normalize(path.posix.join(path.posix.dirname(filename), specifier));
|
|
85
|
+
const choices = (/\.(?:[cm]?[jt]sx?|json|css)$/.test(base) ? [base] : [base, ...['.ts','.tsx','.js','.jsx','.mjs','.cjs','.mts','.cts','.json','/index.ts','/index.tsx','/index.js','/index.jsx'].map(suffix => base + suffix)]).filter(isNativeReviewFilePath);
|
|
86
|
+
const found = [];
|
|
87
|
+
for (const choice of choices) { try { if (await readProjectFile(root, choice) !== null) found.push(choice); } catch (error) { if (choice !== base || choices.length === 1 || error.message !== 'A setup path has an incompatible file type.') throw error; } }
|
|
88
|
+
if (found.length !== 1) fail('A local native dependency is missing or ambiguous. Resolve it before route review.');
|
|
89
|
+
await visit(found[0]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const seed of seeds) await visit(seed);
|
|
93
|
+
return [...seen].sort(([a], [b]) => a.localeCompare(b)).map(([path, content]) => ({ path, content, state: 'unchanged' }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function planNativeRouteReview(directory, selection) {
|
|
97
|
+
const discovered = await inspectNativeRouteCandidates(directory), { root, app, candidates } = discovered;
|
|
98
|
+
if (!Array.isArray(selection) || !selection.length || selection.length > 1100 || selection.some(value => typeof value !== 'string') || new Set(selection).size !== selection.length) fail('Select exact existing website-owned routes for review.');
|
|
99
|
+
const selected = selection.map(value => candidates.find(row => row.path === value));
|
|
100
|
+
if (selected.some(row => !row)) fail('A selected route is not an existing supported native route.');
|
|
101
|
+
if (!candidates.some(row => row.kind === 'static' && !selection.includes(row.path) && !isExcludedPublicationPath(row.path))) fail('Keep at least one public page mapped to CMS content. A fully native application needs a settings-only adapter, not an empty publication setup.');
|
|
102
|
+
const seeds = selected.map(row => row.file);
|
|
103
|
+
for (const row of selected) {
|
|
104
|
+
let directory = path.posix.dirname(row.file);
|
|
105
|
+
while (directory !== app) {
|
|
106
|
+
for (const name of ['layout','template','loading','error','default','not-found']) for (const ext of extensions) {
|
|
107
|
+
const filename = `${directory}/${name}.${ext}`;
|
|
108
|
+
if (await readProjectFile(root, filename) !== null) seeds.push(filename);
|
|
109
|
+
}
|
|
110
|
+
directory = path.posix.dirname(directory);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const observations = await dependencyClosure(root, app, seeds);
|
|
114
|
+
const value = { version: 1, profile: REVIEWED_NEXT_NATIVE_PROFILE,
|
|
115
|
+
nativePaths: selected.filter(row => row.kind === 'static').map(row => row.path).sort(),
|
|
116
|
+
nativePatterns: selected.filter(row => row.kind === 'dynamic').map(row => row.path).sort(),
|
|
117
|
+
files: observations.map(({path, content}) => ({path, sha256: digest(content)})) };
|
|
118
|
+
const text = json(value);
|
|
119
|
+
await inspectReviewedNativeOwnership(root, { candidate: text });
|
|
120
|
+
const previous = await readProjectFile(root, NATIVE_ROUTES_FILE);
|
|
121
|
+
if (previous !== null && (await lstat(path.join(root, NATIVE_ROUTES_FILE))).nlink !== 1) fail('The existing native review is linked. Preserve it and review manually.');
|
|
122
|
+
for (const filename of configurationFiles) {
|
|
123
|
+
const content = await readProjectFile(root, filename);
|
|
124
|
+
observations.push({ path: filename, content, state: content === null ? 'absent' : 'unchanged' });
|
|
125
|
+
}
|
|
126
|
+
return { root, framework: 'next', routes: selected, unsupported: discovered.unsupported, files: [...observations, ...installedFiles.map(path => ({path,content:null,state:'absent'})), {
|
|
127
|
+
path: NATIVE_ROUTES_FILE, content: text, state: previous === null ? 'create' : previous === text ? 'unchanged' : 'update', ...(previous !== null && previous !== text ? { previous } : {}),
|
|
128
|
+
}] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function runNativeRouteReview(directory, io) {
|
|
132
|
+
const inventory = await inspectNativeRouteCandidates(directory);
|
|
133
|
+
io.write('Review website-owned application routes. Selected routes keep their existing rendering and are excluded from CMS source import and publication control. This does not install the connection or grant key access.\n');
|
|
134
|
+
inventory.candidates.forEach((row, index) => io.write(` ${index + 1}. ${row.path} (${row.kind}) — ${row.file}\n`));
|
|
135
|
+
if (inventory.unsupported.length) io.write(`Additional routes require an adapter review: ${inventory.unsupported.join(', ')}\n`);
|
|
136
|
+
if (inventory.omittedPathCount) io.write('Additional unsupported paths were omitted from output; they still require local adapter review.\n');
|
|
137
|
+
const answer = (await io.ask('Enter comma-separated route numbers to keep native (blank cancels): ')).trim();
|
|
138
|
+
if (!answer) { io.write('Route review cancelled. No files changed.\n'); return { saved: false }; }
|
|
139
|
+
if (!/^\d+(?:\s*,\s*\d+)*$/.test(answer)) fail('Use only the displayed route numbers, separated by commas.');
|
|
140
|
+
const indexes = answer.split(',').map(value => Number(value.trim()) - 1);
|
|
141
|
+
if (new Set(indexes).size !== indexes.length || indexes.some(index => !Number.isSafeInteger(index) || index < 0 || index >= inventory.candidates.length)) fail('Select each displayed route number at most once.');
|
|
142
|
+
const selection = indexes.map(index => inventory.candidates[index].path);
|
|
143
|
+
const plan = await planNativeRouteReview(directory, selection);
|
|
144
|
+
if (JSON.stringify(await inspectNativeRouteCandidates(directory)) !== JSON.stringify(inventory)) fail('Route inventory changed during review. No manifest was saved.');
|
|
145
|
+
for (const row of plan.routes) io.write(` Keep native: ${row.path}\n`);
|
|
146
|
+
io.write(`Review includes ${plan.files.length - 1} source/configuration files. It preserves all website files and changes only ${NATIVE_ROUTES_FILE}.\n`);
|
|
147
|
+
if (!['y','yes'].includes((await io.ask('Save this native route review? [y/N] ')).trim().toLowerCase())) return { saved: false };
|
|
148
|
+
if (JSON.stringify(await planNativeRouteReview(directory, selection)) !== JSON.stringify(plan)) fail('Native sources changed during review. No manifest was saved.');
|
|
149
|
+
await applyPlan(plan);
|
|
150
|
+
io.write('Native route review saved. Rerun the scoped preflight. If it reports existing header/footer ownership, run review-layout. Complete setup and running-website checks before connecting.\n');
|
|
151
|
+
return { saved: true, routes: selection };
|
|
152
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Explicit native Next route ownership. No prefix or wildcard expansion. */
|
|
2
|
+
export const REVIEWED_NEXT_NATIVE_PROFILE = 'reviewed-next-native-v1';
|
|
3
|
+
const pathPattern = /^\/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$/;
|
|
4
|
+
const dynamicPattern = /^(\/(?:[a-z0-9]+(?:-[a-z0-9]+)*\/)+)\[([a-zA-Z][a-zA-Z0-9_]*)\]$/;
|
|
5
|
+
const reserved = new Set(['api', 'admin', 'auth', 'login', 'logout', 'signin', 'signout', 'sign-in', 'sign-out', 'signup', 'sign-up', 'register', 'dashboard', 'settings', 'billing', 'preview', 'draft-preview', 'cms-preview', 'node-modules', 'src', 'public', 'assets', 'static']);
|
|
6
|
+
const safePath = value => typeof value === 'string' && value.length <= 500 && pathPattern.test(value) && !value.split('/').some(segment => reserved.has(segment));
|
|
7
|
+
export const isNativeReviewFilePath = value => typeof value === 'string' && value.length <= 500 && /^(?:src\/)?(?:app|components|context|data|lib)\/[A-Za-z0-9_()[\]./-]+\.(?:[cm]?[jt]sx?|json|css)$/.test(value) && value.split('/').every(part => !!part && part !== '.' && part !== '..' && !part.startsWith('.'));
|
|
8
|
+
const fail = () => { throw Error('Invalid reviewed native route ownership. Use exact native paths and single final dynamic segments without reserved routes or overlaps.'); };
|
|
9
|
+
export function nativePatternMatches(pattern, pathname) {
|
|
10
|
+
const match = typeof pattern === 'string' && dynamicPattern.exec(pattern);
|
|
11
|
+
if (!match || typeof pathname !== 'string' || !pathname.startsWith(match[1])) return false;
|
|
12
|
+
const value = pathname.slice(match[1].length);
|
|
13
|
+
return /^[A-Za-z0-9_-]+$/.test(value) && !reserved.has(value.toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
export function matchesNativeRoutes(value, pathname) {
|
|
16
|
+
return value?.nativePaths?.includes(pathname) || value?.nativePatterns?.some(pattern => nativePatternMatches(pattern, pathname)) || false;
|
|
17
|
+
}
|
|
18
|
+
export function validateNativeRoutes(value, cmsRoutes = []) {
|
|
19
|
+
if (!value || !Array.isArray(value.nativePaths) || !Array.isArray(value.nativePatterns) ||
|
|
20
|
+
value.nativePaths.length + value.nativePatterns.length < 1 || value.nativePaths.length > 1000 || value.nativePatterns.length > 100 ||
|
|
21
|
+
value.nativePaths.some(name => !safePath(name)) || new Set(value.nativePaths).size !== value.nativePaths.length ||
|
|
22
|
+
value.nativePatterns.some(name => typeof name !== 'string' || name.length > 500 || !dynamicPattern.test(name) || !safePath(name.replace(/\[[^/]+\]$/, 'reviewed-native-value')))) return fail();
|
|
23
|
+
const parents = value.nativePatterns.map(name => name.slice(0, name.lastIndexOf('/') + 1));
|
|
24
|
+
if (new Set(parents).size !== parents.length || value.nativePaths.some(name => value.nativePatterns.some(pattern => nativePatternMatches(pattern, name))) ||
|
|
25
|
+
cmsRoutes.some(row => matchesNativeRoutes(value, typeof row === 'string' ? row : row?.path))) return fail();
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WebsiteResult, WebsiteSnapshot } from './website-runtime.mjs';
|
|
2
|
+
import type { PublishedDocumentEntry } from './publication.mjs';
|
|
3
|
+
import type {NativeChromeDescriptor} from './native-chrome-model.mjs';
|
|
4
|
+
export type NativeWebsiteInventory = {
|
|
5
|
+
nativeChrome?: NativeChromeDescriptor;
|
|
6
|
+
identity: { kind: 'page' | 'post'; slug: string; locale: string };
|
|
7
|
+
baseline: PublishedDocumentEntry['document'];
|
|
8
|
+
bindings: Record<string, { variants: Array<{ id: string; values: string[] }> }>;
|
|
9
|
+
};
|
|
10
|
+
export type NativeWebsitePayload = {
|
|
11
|
+
pathname: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
initialResult: WebsiteResult;
|
|
14
|
+
initialSettings: WebsiteSnapshot;
|
|
15
|
+
inventory?: NativeWebsiteInventory;
|
|
16
|
+
};
|
|
17
|
+
export function nativeWebsitePayload(value: unknown): NativeWebsitePayload;
|
|
18
|
+
export function serializeNativeWebsite(value: unknown): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Public boundary for framework-native hosts. Never serialize a frozen source. */
|
|
2
|
+
import { publicPublishedEntry, normalizePublicationPath, isExcludedPublicationPath, publicationRevision } from './publication.mjs';
|
|
3
|
+
import { parseWebsiteSettings } from './website-model.mjs';
|
|
4
|
+
import { parseWebsiteSection, parseWebsiteEntryMetadata } from './website-schema.mjs';
|
|
5
|
+
import { validatePublishedDocumentEntry } from './publication-document.mjs';
|
|
6
|
+
|
|
7
|
+
import {nativeChromeDescriptor} from './native-chrome-model.mjs';
|
|
8
|
+
const fail = () => { throw Error('Native website payload is unavailable.'); };
|
|
9
|
+
const plain = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
11
|
+
const scopePattern = new RegExp(`^${uuid}:${uuid}$`, 'i');
|
|
12
|
+
const path = value => { const result = normalizePublicationPath(value); if (result !== value || isExcludedPublicationPath(result)) fail(); return result; };
|
|
13
|
+
const flatten = blocks => blocks.flatMap(block => [block, ...(block.type === 'section' ? flatten(block.data.blocks) : block.type === 'columns' ? flatten(block.data.columns.flat()) : [])]);
|
|
14
|
+
function entry(value) {
|
|
15
|
+
if (!plain(value) || !['page','post'].includes(value.kind) || typeof value.slug !== 'string' || !/^[a-z0-9]+(?:[-/][a-z0-9]+)*$/.test(value.slug) || value.slug.length > 160 || typeof value.locale !== 'string' || value.locale.length > 35) fail();
|
|
16
|
+
try { if (Intl.getCanonicalLocales(value.locale)[0] !== value.locale) fail(); } catch { fail(); }
|
|
17
|
+
const projected = publicPublishedEntry(value);
|
|
18
|
+
validatePublishedDocumentEntry(projected, {kind:value.kind,slug:value.slug,locale:value.locale});
|
|
19
|
+
return { ...projected, ...parseWebsiteEntryMetadata(value), path: path(value.path) };
|
|
20
|
+
}
|
|
21
|
+
function currentInventory(value, published) {
|
|
22
|
+
if (!plain(value) || !plain(value.identity) || !plain(value.baseline) || !Array.isArray(value.baseline.blocks) || !plain(value.bindings)) fail();
|
|
23
|
+
const identity = { kind: published.kind, slug: published.slug, locale: published.locale };
|
|
24
|
+
if (Object.keys(identity).some(key => value.identity[key] !== identity[key]) || value.baseline.blocks.length > 400 || Object.keys(value.bindings).length > 400) fail();
|
|
25
|
+
const current = new Map(flatten(published.document.blocks).map(block => [block.id, block.type]));
|
|
26
|
+
const original = new Map(flatten(value.baseline.blocks).map(block => [block.id, block.type]));
|
|
27
|
+
const bindings = {}, ids = new Set();
|
|
28
|
+
for (const [name, slot] of Object.entries(value.bindings)) {
|
|
29
|
+
if (!/^[A-Za-z0-9_.:-]{1,200}$/.test(name) || !plain(slot) || !Array.isArray(slot.variants) || slot.variants.length > 400) fail();
|
|
30
|
+
const variants = [];
|
|
31
|
+
for (const row of slot.variants) {
|
|
32
|
+
if (!plain(row) || typeof row.id !== 'string') fail();
|
|
33
|
+
if (!current.has(row.id) || original.get(row.id) !== current.get(row.id)) continue;
|
|
34
|
+
ids.add(row.id); variants.push({ id: row.id, values: [] });
|
|
35
|
+
}
|
|
36
|
+
if (variants.length) bindings[name] = { variants };
|
|
37
|
+
}
|
|
38
|
+
return { identity, baseline: { schemaVersion: 1, title: '', excerpt: '', blocks: [...ids].map(id => ({ id, version: 1, type: current.get(id), text: '' })) }, bindings, ...(value.nativeChrome ? {nativeChrome:nativeChromeDescriptor(value.nativeChrome)} : {}) };
|
|
39
|
+
}
|
|
40
|
+
export function nativeWebsitePayload(value) {
|
|
41
|
+
if (!plain(value) || !scopePattern.test(value.scope)) fail();
|
|
42
|
+
const pathname = path(value.pathname), raw = value.initialResult;
|
|
43
|
+
let result;
|
|
44
|
+
if (raw?.state === 'published') {
|
|
45
|
+
const document = entry(raw.entry);
|
|
46
|
+
if (document.path !== pathname || raw.path !== undefined && raw.path !== pathname || raw.revision !== undefined && raw.revision !== publicationRevision(document)) fail();
|
|
47
|
+
result = { state: 'published', status: 200, path: pathname, entry: document, revision: publicationRevision(document), related: [] };
|
|
48
|
+
} else if (raw?.state === 'archive') {
|
|
49
|
+
if (raw.path !== pathname || !['category', 'tag'].includes(raw.kind) || typeof raw.title !== 'string' || raw.title.length > 200 || typeof raw.description !== 'string' || raw.description.length > 1000 || !Array.isArray(raw.entries) || raw.entries.length > 1000) fail();
|
|
50
|
+
result = { state: 'archive', status: 200, path: pathname, kind: raw.kind, title: raw.title, description: raw.description, entries: raw.entries.map(entry) };
|
|
51
|
+
} else fail();
|
|
52
|
+
const settings = parseWebsiteSettings(value.initialSettings);
|
|
53
|
+
for (const [name, section] of Object.entries(settings.sections)) {
|
|
54
|
+
if (section.state === 'ready' && settings.state === 'ready') section.data = parseWebsiteSection(name, { section: name, version: section.version, data: section.data }).data;
|
|
55
|
+
}
|
|
56
|
+
const payload = { pathname, scope: value.scope, initialResult: result, initialSettings: settings };
|
|
57
|
+
if (value.inventory !== undefined) {
|
|
58
|
+
if (result.state !== 'published') fail();
|
|
59
|
+
payload.inventory = currentInventory(value.inventory, result.entry);
|
|
60
|
+
}
|
|
61
|
+
const encoded = JSON.stringify(payload);
|
|
62
|
+
if (encoded.length > 8 * 1024 * 1024 || /cms_server\.|sk-[A-Za-z0-9_-]{12,}|AIza[A-Za-z0-9_-]{20,}/i.test(encoded)) fail();
|
|
63
|
+
return payload;
|
|
64
|
+
}
|
|
65
|
+
export function serializeNativeWebsite(value) {
|
|
66
|
+
return JSON.stringify(nativeWebsitePayload(value)).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function renderNativeWebsite(value: unknown, childrenHtml?: string): string;
|