@universalcms/connect 0.1.0 → 0.13.1
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 +699 -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 +207 -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 +514 -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 +130 -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/settings-only-setup.mjs +100 -0
- package/dist/settings-only.d.mts +4 -0
- package/dist/settings-only.mjs +49 -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 +58 -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 +404 -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 +44 -7
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Explicit literal layout review; no key, network, scope or publication effects. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {lstat,readdir,realpath} from 'node:fs/promises';
|
|
4
|
+
import {projectRoot,readProjectFile,applyPlan,SetupError} from './core.mjs';
|
|
5
|
+
import {setupReviewError} from './preflight-recovery.mjs';
|
|
6
|
+
import {contentDigest} from './content-registry.mjs';
|
|
7
|
+
import {nativeFramework,inspectNativeProject} from './native-adapters.mjs';
|
|
8
|
+
import {inspectNativeChromeTemplate} from './native-chrome-source.mjs';
|
|
9
|
+
export const NATIVE_CHROME_FILE='.universalcms/native.chrome.json';
|
|
10
|
+
const installedFiles=['.universalcms/content.bindings.json','.universalcms/publication.config.json','.universalcms/website.integration.json'];
|
|
11
|
+
const json=value=>JSON.stringify(value,null,2)+'\n';
|
|
12
|
+
const fail=message=>{throw new SetupError(message);};
|
|
13
|
+
async function ordinary(root,name){const text=await readProjectFile(root,name);if(text!==null&&(await lstat(path.join(root,name))).nlink!==1)fail('Native layout review requires ordinary files without links.');return text;}
|
|
14
|
+
const safePath=value=>typeof value==='string'&&/^(?:src\/|app\/pages\/|(?:[a-z0-9-]+\/)*index\.html$)/.test(value)&&/^[A-Za-z0-9_+./-]+$/.test(value)&&!value.split('/').some(part=>!part||part==='.'||part==='..'||part.startsWith('.'))&&value.length<300;
|
|
15
|
+
export async function readNativeChromeReview(root,framework){
|
|
16
|
+
const text=await ordinary(root,NATIVE_CHROME_FILE);if(text===null)return null;
|
|
17
|
+
let value;try{value=JSON.parse(text);}catch{fail('Native layout review manifest is invalid.');}
|
|
18
|
+
if(!value||typeof value!=='object'||Object.keys(value).some(key=>!['version','framework','files'].includes(key))||value.version!==1||value.framework!==framework||!Array.isArray(value.files)||!value.files.length||value.files.length>1000||value.files.some(row=>!row||Object.keys(row).some(key=>!['path','sha256'].includes(key))||!safePath(row.path)||!/^[a-f0-9]{64}$/.test(row.sha256))||new Set(value.files.map(row=>row.path)).size!==value.files.length)fail('Native layout review manifest does not match this framework.');
|
|
19
|
+
return {path:NATIVE_CHROME_FILE,content:text,state:'unchanged',sha256:contentDigest(text),value};
|
|
20
|
+
}
|
|
21
|
+
export function reviewedNativeChrome(review,filename,source){
|
|
22
|
+
const row=review?.value.files.find(row=>row.path===filename);
|
|
23
|
+
if(!row){if(/<(?:header|footer)(?:\s|>)/.test(source))throw setupReviewError('native-chrome-layout',[filename]);return false;}
|
|
24
|
+
if(contentDigest(source)!==row.sha256)fail('Reviewed native layout changed. Preserve the integration and review its source before setup.');
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
export async function nativeChromeObservation(root,review){if(!review)return[];if(await ordinary(root,NATIVE_CHROME_FILE)!==review.content)fail('Native layout review changed during planning. No integration files were saved.');return[{path:review.path,content:review.content,state:'unchanged'}];}
|
|
28
|
+
async function candidateFiles(root,framework,project){
|
|
29
|
+
if(framework==='vue')return['src/App.vue'];
|
|
30
|
+
if(framework==='angular')return[project.review.template];
|
|
31
|
+
if(framework==='static')return project.pages.map(row=>row.sourceFile);
|
|
32
|
+
const base=framework==='nuxt'?'app/pages':'src/routes',files=[];let count=0;
|
|
33
|
+
async function walk(directory,depth=0){if(depth>12||++count>1200)fail('Native layout route inventory exceeds its budget.');const target=path.join(root,directory),stat=await lstat(target);if(!stat.isDirectory()||stat.isSymbolicLink()||await realpath(target)!==target)fail('Native layout review refuses linked source directories.');for(const item of(await readdir(target,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name))){if(++count>1200||item.isSymbolicLink())fail('Native layout review refuses linked or oversized routes.');const filename=directory+'/'+item.name;if(item.isDirectory()){if(!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.name))fail('Custom route shapes need their existing reviewed adapter.');await walk(filename,depth+1);}else if(item.isFile()&&(framework==='nuxt'?/^[a-z0-9]+(?:-[a-z0-9]+)*\.vue$/.test(item.name):item.name==='+page.svelte'))files.push(filename);else fail('Custom route components or server files need their existing reviewed adapter.');}}
|
|
34
|
+
await walk(base);return files;
|
|
35
|
+
}
|
|
36
|
+
function templateRegion(source,framework){if(!['vue','nuxt'].includes(framework))return {template:source,start:0,end:source.length};const match=/^\s*(?:<script\s+setup(?:\s+lang="(?:ts|js)")?\s*>\s*<\/script>\s*)?<template\s*>([\s\S]*?)<\/template>\s*((?:<style(?:\s+scoped)?\s*>[\s\S]*?<\/style>\s*)*)$/.exec(source);if(!match)fail('Vue layout review requires one literal template, optional empty script setup and ordinary styles.');const start=source.indexOf('>',source.indexOf('<template'))+1;return {template:match[1],start,end:start+match[1].length};}
|
|
37
|
+
export async function planNativeChromeReview(directory){
|
|
38
|
+
const root=await projectRoot(directory);
|
|
39
|
+
for(const name of installedFiles)if(await readProjectFile(root,name)!==null)fail('An integration is already installed. Review its existing adapter update; review-layout never rewrites installed native content.');
|
|
40
|
+
const packageText=await ordinary(root,'package.json');let manifest;try{manifest=JSON.parse(packageText);}catch{fail('A valid website package.json is required.');}
|
|
41
|
+
const framework=nativeFramework(manifest);if(!framework)fail('This layout review supports Vue, Nuxt, Angular SSR, SvelteKit and literal HTML. Use the framework-specific review for other websites.');
|
|
42
|
+
const project=await inspectNativeProject(root,framework,manifest),paths=await candidateFiles(root,framework,project),files=[{path:'package.json',content:packageText,state:'unchanged'},...installedFiles.map(path=>({path,content:null,state:'absent'}))],reviewed=[],pages=[];
|
|
43
|
+
for(const filename of paths){const source=await ordinary(root,filename);if(typeof source!=='string')fail('A native source disappeared during layout review.');const region=templateRegion(source,framework),checked=await inspectNativeChromeTemplate(region.template,{mark:true});const content=source.slice(0,region.start)+checked.source+source.slice(region.end);files.push({path:filename,content,state:content===source?'unchanged':'update',...(content===source?{}:{previous:source})});if(checked.descriptor){reviewed.push({path:filename,sha256:contentDigest(content)});pages.push({path:filename,slots:checked.descriptor.slots,change:checked.changed?'insert-slots':'verify-slots'});}}
|
|
44
|
+
if(!reviewed.length)fail('No unambiguous literal header and footer were found. Preserve the native layout and review its component composition.');
|
|
45
|
+
const before=await ordinary(root,NATIVE_CHROME_FILE),content=json({version:1,framework,files:reviewed});if(before!==null&&before!==content)fail('An existing layout declaration differs. Preserve it and review the changed source explicitly.');
|
|
46
|
+
files.push({path:NATIVE_CHROME_FILE,content,state:before===null?'create':'unchanged'});
|
|
47
|
+
return {root,framework,files,pages,review:{preserves:['native-children','native-links','classes','private-credentials','connection-scope','published-content'],adds:'Empty CMS settings slots inside the existing native header and footer; current grants control their contents.',complete:false}};
|
|
48
|
+
}
|
|
49
|
+
export async function runNativeChromeReview(directory,io){const plan=await planNativeChromeReview(directory);io.write('Review native layout changes. Existing native links, labels, controls and CSS classes remain website-owned. CMS settings occupy new empty slots; this does not publish or install the website integration.\n');for(const page of plan.pages)io.write(` ${page.change}: ${page.path} (${page.slots.length} settings slots)\n`);if(!['y','yes'].includes((await io.ask('Save this native header/footer integration review? [y/N] ')).trim().toLowerCase()))return{saved:false};if(json(await planNativeChromeReview(directory))!==json(plan))fail('Native layout changed during review. No declaration was saved.');await applyPlan(plan);io.write('Native layout review saved. Rerun the scoped preflight and setup, then verify the running website before connecting.\n');return{saved:true,pages:plan.pages.map(row=>row.path)};}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Bounded inert literal chrome review. Never evaluates a website module. */
|
|
2
|
+
import { parse } from './vendor/astro-compiler/node/index.js';
|
|
3
|
+
import { SetupError } from './core.mjs';
|
|
4
|
+
import { publicHref } from './website-model.mjs';
|
|
5
|
+
import { nativeChromeDescriptor, NATIVE_CHROME_SLOTS } from './native-chrome-model.mjs';
|
|
6
|
+
const fail = reason => { throw new SetupError(`Native layout needs review: ${reason} Existing files were preserved.`); };
|
|
7
|
+
const tags = new Set(['header','footer','nav','div','span','a','ul','ol','li','details','summary','small','strong','em','b','i','p','br']);
|
|
8
|
+
const decode = value => value.replace(/&(?:amp|quot|apos|lt|gt);/g, value => ({'&':'&','"':'"',''':"'",'<':'<','>':'>'}[value]));
|
|
9
|
+
const empty = nodes => (nodes ?? []).every(node => node.type === 'text' && !node.value.trim());
|
|
10
|
+
export async function inspectNativeChromeTemplate(source, { mark = false } = {}) {
|
|
11
|
+
if (typeof source !== 'string' || Buffer.byteLength(source) > 128*1024 || source.includes('\0')) fail('template exceeds the literal source budget');
|
|
12
|
+
const tree = await parse(source, {position:true});
|
|
13
|
+
if (tree.diagnostics.some(row => row.severity === 1)) fail('invalid native template');
|
|
14
|
+
const offsets = new Map([[0,0]]); let bytes=0,characters=0,count=0;
|
|
15
|
+
for (const character of source) { bytes+=Buffer.byteLength(character); characters+=character.length; offsets.set(bytes,characters); }
|
|
16
|
+
const roots=[], markers=[];
|
|
17
|
+
function collect(node, depth=0) {
|
|
18
|
+
if (++count > 30000 || depth > 80) fail('template exceeds the AST budget');
|
|
19
|
+
if (node.type === 'element' && ['header','footer'].includes(node.name)) roots.push(node);
|
|
20
|
+
for (const attribute of node.attributes ?? []) if (/^data-universalcms/i.test(attribute.name)) markers.push({node,attribute});
|
|
21
|
+
for (const child of node.children ?? []) collect(child,depth+1);
|
|
22
|
+
}
|
|
23
|
+
collect(tree.ast);
|
|
24
|
+
if (!roots.length && !markers.length) return {source, descriptor:undefined, changed:false};
|
|
25
|
+
if (roots.length !== 2 || roots.filter(node => node.name === 'header').length !== 1 || roots.filter(node => node.name === 'footer').length !== 1) fail('exactly one literal header and footer are required');
|
|
26
|
+
const slots = new Map(), exclude={header:[],footer:[]}, edits=[], owned=new Set();
|
|
27
|
+
function range(node) {
|
|
28
|
+
let start=offsets.get(node.position?.start?.offset), end=offsets.get(node.position?.end?.offset);
|
|
29
|
+
if (source[start]!=='<' && source[start-1]==='<') start--;
|
|
30
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || !source.startsWith('<'+node.name,start) || !source.slice(start,end).endsWith('</'+node.name+'>')) fail('explicit closing tags and exact source positions are required');
|
|
31
|
+
let quote=null,openEnd;
|
|
32
|
+
for(let i=start+1;i<end;i++){const char=source[i];if(quote){if(char===quote)quote=null;}else if(char==='"'||char==="'")quote=char;else if(char==='>'){openEnd=i+1;break;}}
|
|
33
|
+
if(!openEnd)fail('opening tag is incomplete');
|
|
34
|
+
return {start,end,openEnd,closeStart:end-node.name.length-3};
|
|
35
|
+
}
|
|
36
|
+
for (const root of roots) {
|
|
37
|
+
const wanted=root.name==='header'?NATIVE_CHROME_SLOTS.slice(0,4):NATIVE_CHROME_SLOTS.slice(4);
|
|
38
|
+
function visit(node) {
|
|
39
|
+
if (node.type === 'text') { if (/[{}]/.test(node.value)) fail('computed chrome text needs a reviewed adapter'); return; }
|
|
40
|
+
if (node.type === 'comment') return;
|
|
41
|
+
if (node.type !== 'element' || !tags.has(node.name) || node !== root && ['header','footer'].includes(node.name)) fail('chrome must contain literal native elements without scripts, forms, images or components');
|
|
42
|
+
if(node.name!=='br')range(node);
|
|
43
|
+
owned.add(node); const names=new Set(); let slot;
|
|
44
|
+
for(const attribute of node.attributes ?? []) {
|
|
45
|
+
const name=attribute.name;
|
|
46
|
+
if(names.has(name) || !['quoted','empty'].includes(attribute.kind) || /^(?:on|v-|[:@#*[(])/i.test(name)) fail('dynamic or duplicate chrome attributes need a reviewed adapter');
|
|
47
|
+
names.add(name);
|
|
48
|
+
if(name==='data-universalcms-chrome') { if(node!==root || attribute.value!==root.name)fail('chrome markers must identify their exact native header/footer'); continue; }
|
|
49
|
+
if(name==='data-universalcms-slot') { if(node.name!=='div' || !wanted.includes(attribute.value) || slots.has(attribute.value) || !empty(node.children))fail('each settings slot must be one empty div in its matching header/footer');slot=attribute.value;slots.set(slot,node);continue; }
|
|
50
|
+
if(!/^(?:class|id|title|role|aria-[a-z-]+|data-[a-z-]+|href|target|rel|open)$/.test(name) || /^data-(?:cms|universalcms)/.test(name))fail('unsupported chrome attributes need a reviewed adapter');
|
|
51
|
+
if(name==='href') { const href=decode(attribute.value??''); if(node.name!=='a'||!publicHref(href,{mail:true}) || /&/.test(href.replace(/&(?:amp;)?/g,'')))fail('native links need safe literal URLs'); if(!exclude[root.name].includes(href))exclude[root.name].push(href); }
|
|
52
|
+
if(name==='target' && !['_self','_blank'].includes(attribute.value))fail('unsupported native link target');
|
|
53
|
+
if(name==='open' && node.name!=='details')fail('only native details controls may use open');
|
|
54
|
+
}
|
|
55
|
+
if(node.name==='a'&&!names.has('href'))fail('native links require a literal href');
|
|
56
|
+
if(slot && (node.attributes??[]).some(attribute=>!['data-universalcms-slot','class','id'].includes(attribute.name)))fail('settings slots may only have marker, class and id attributes');
|
|
57
|
+
for(const child of node.children??[])visit(child);
|
|
58
|
+
}
|
|
59
|
+
visit(root);
|
|
60
|
+
const region=range(root),marked=(root.attributes??[]).some(attribute=>attribute.name==='data-universalcms-chrome');
|
|
61
|
+
if(mark && !markers.length) {
|
|
62
|
+
edits.push({start:region.openEnd-1,end:region.openEnd-1,text:` data-universalcms-chrome="${root.name}"`});
|
|
63
|
+
edits.push({start:region.closeStart,end:region.closeStart,text:wanted.map(name=>`<div data-universalcms-slot="${name}"></div>`).join('')});
|
|
64
|
+
} else if(!marked || wanted.some(name=>!slots.has(name))) fail('run review-layout to review and add the complete explicit settings slots');
|
|
65
|
+
}
|
|
66
|
+
if(markers.some(({node})=>!owned.has(node)))fail('settings markers outside the inspected chrome require review');
|
|
67
|
+
if(edits.length){let output=source;for(const edit of edits.sort((a,b)=>b.start-a.start))output=output.slice(0,edit.start)+edit.text+output.slice(edit.end);const checked=await inspectNativeChromeTemplate(output);return {...checked,changed:true};}
|
|
68
|
+
return {source,descriptor:nativeChromeDescriptor({version:1,slots:[...NATIVE_CHROME_SLOTS],exclude}),changed:false};
|
|
69
|
+
}
|
|
@@ -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;
|