@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,127 @@
|
|
|
1
|
+
/** Reviewed ordinary Vue 3/Vite and Nuxt 4 Node deployment plans. */
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import ts from './vendor/typescript.cjs';
|
|
5
|
+
import { projectRoot, readProjectFile, SetupError } from './core.mjs';
|
|
6
|
+
import { inspectAstroPublicAssets } from './astro-setup.mjs';
|
|
7
|
+
import { inspectVueSource } from './vue-source.mjs';
|
|
8
|
+
import { contentDigest, readContentRegistry, readFrozenContent, CONTENT_BINDINGS_FILE } from './content-registry.mjs';
|
|
9
|
+
import { WEBSITE_SECTIONS } from './website-http.mjs';
|
|
10
|
+
const json=v=>JSON.stringify(v,null,2)+'\n';
|
|
11
|
+
const fail=reason=>{throw new SetupError('Vue/Nuxt setup needs review: '+reason+'. Existing files were preserved.');};
|
|
12
|
+
const version=(value,major)=>typeof value==='string'&&new RegExp('^(?:\\^|~)?'+major+'\\.\\d+\\.\\d+$').test(value);
|
|
13
|
+
async function ordinary(root,name){const text=await readProjectFile(root,name);if(text!==null&&(await lstat(path.join(root,name))).nlink!==1)fail('linked source/configuration');return text;}
|
|
14
|
+
async function absent(root,name){try{await lstat(path.join(root,name));}catch(e){if(e.code==='ENOENT')return;throw e;}fail('existing '+name+' needs explicit route and lifecycle ownership');}
|
|
15
|
+
function literalConfig(source,filename,framework){
|
|
16
|
+
if(source===null)return;
|
|
17
|
+
const file=ts.createSourceFile(filename,source,ts.ScriptTarget.Latest,true,ts.ScriptKind.TS);if(file.parseDiagnostics.length)fail('invalid framework configuration');
|
|
18
|
+
const imports=new Map();let config;
|
|
19
|
+
for(const node of file.statements){
|
|
20
|
+
if(ts.isImportDeclaration(node)&&ts.isStringLiteral(node.moduleSpecifier)){
|
|
21
|
+
if(framework!=='vue'||node.attributes)fail('custom configuration imports');
|
|
22
|
+
const clause=node.importClause,spec=node.moduleSpecifier.text;
|
|
23
|
+
if(spec==='vite'&&!clause?.name&&clause?.namedBindings&&ts.isNamedImports(clause.namedBindings)&&clause.namedBindings.elements.length===1&&clause.namedBindings.elements[0].name.text==='defineConfig')imports.set('defineConfig','vite');
|
|
24
|
+
else if(spec==='@vitejs/plugin-vue'&&clause?.name&&!clause.namedBindings)imports.set(clause.name.text,'vue');else fail('custom Vite plugins');
|
|
25
|
+
}else if(ts.isExportAssignment(node)&&!node.isExportEquals&&!config){const e=node.expression;config=ts.isCallExpression(e)&&ts.isIdentifier(e.expression)&&e.expression.text===(framework==='nuxt'?'defineNuxtConfig':'defineConfig')&&e.arguments.length===1?e.arguments[0]:e;}
|
|
26
|
+
else if(!ts.isEmptyStatement(node))fail('executable framework configuration');
|
|
27
|
+
}
|
|
28
|
+
if(!config||!ts.isObjectLiteralExpression(config))fail('literal configuration required');
|
|
29
|
+
const seen=new Set();for(const member of config.properties){
|
|
30
|
+
const name=ts.isPropertyAssignment(member)&&(ts.isIdentifier(member.name)||ts.isStringLiteral(member.name))?member.name.text:null;
|
|
31
|
+
if(!name||seen.has(name))fail('spread/computed/duplicate configuration');seen.add(name);const v=member.initializer;
|
|
32
|
+
if(framework==='vue'&&name==='plugins'&&ts.isArrayLiteralExpression(v)&&v.elements.length===1){const p=v.elements[0];if(ts.isCallExpression(p)&&ts.isIdentifier(p.expression)&&imports.get(p.expression.text)==='vue'&&!p.arguments.length)continue;}
|
|
33
|
+
if(framework==='nuxt'&&name==='compatibilityDate'&&ts.isStringLiteral(v)&&/^\d{4}-\d{2}-\d{2}$/.test(v.text))continue;
|
|
34
|
+
if(framework==='nuxt'&&name==='devtools'&&ts.isObjectLiteralExpression(v)&&v.properties.length===1){const p=v.properties[0];if(ts.isPropertyAssignment(p)&&p.name.getText(file)==='enabled'&&[ts.SyntaxKind.TrueKeyword,ts.SyntaxKind.FalseKeyword].includes(p.initializer.kind))continue;}
|
|
35
|
+
fail('custom routing, modules, hooks, aliases or output configuration');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function scripts(manifest,framework){
|
|
39
|
+
const generated=framework==='vue'?{dev:'node .universalcms/vue.serve.mjs',build:'node .universalcms/vue.build.mjs',preview:'node .universalcms/vue.serve.mjs --production',start:'node .universalcms/vue.serve.mjs --production'}:{dev:'nuxt dev --extends ./.universalcms',build:'nuxt build --extends ./.universalcms',preview:'node .universalcms/nuxt.serve.mjs',start:'node .universalcms/nuxt.serve.mjs'};
|
|
40
|
+
const allowed=framework==='vue'?{dev:['vite'],build:['vite build'],preview:['vite preview'],start:[]}:{dev:['nuxt dev'],build:['nuxt build'],preview:['nuxt preview'],start:['node .output/server/index.mjs']};
|
|
41
|
+
const result={...manifest.scripts};for(const [name,value]of Object.entries(generated)){if(result[name]!==undefined&&result[name]!==value&&!allowed[name].includes(result[name]))fail('custom '+name+' command');result[name]=value;const hook='pre'+name;if(result[hook]!==undefined&&result[hook]!=='universalcms-connect sync-source')fail('custom lifecycle hook');result[hook]='universalcms-connect sync-source';}
|
|
42
|
+
if(typeof manifest.scripts?.dev!=='string'||typeof manifest.scripts?.build!=='string')fail('ordinary dev/build commands required');
|
|
43
|
+
return {...manifest,scripts:result};
|
|
44
|
+
}
|
|
45
|
+
export async function inspectVueProject(directory,suppliedManifest,framework='vue'){
|
|
46
|
+
if(!['vue','nuxt'].includes(framework))fail('unsupported framework');
|
|
47
|
+
const root=await projectRoot(directory),packageText=await ordinary(root,'package.json');let manifest;try{manifest=suppliedManifest??JSON.parse(packageText);}catch{fail('invalid package manifest');}
|
|
48
|
+
const deps={...manifest.devDependencies,...manifest.dependencies};
|
|
49
|
+
if(manifest.type!=='module'||manifest.workspaces||!version(deps.vue,3)||Number(deps.vue.replace(/^[~^]/,'').split('.')[1])<5||framework==='vue'&&(![7,8].some(n=>version(deps.vite,n))||!version(deps['@vitejs/plugin-vue'],6))||framework==='nuxt'&&!version(deps.nuxt,4))fail('supported versions are Vue 3 + Vite 7/8 + official Vue plugin 6, or Nuxt 4, in one ESM Node project');
|
|
50
|
+
const node=process.versions.node.split('.').map(Number);
|
|
51
|
+
if(framework==='nuxt'&&!(node[0]===22&&node[1]>=18||node[0]===24&&node[1]>=11||node[0]===25||node[0]===26))fail('Nuxt 4 dependencies require Node 22.18+, 24.11+, 25 or 26');
|
|
52
|
+
await inspectAstroPublicAssets(root);
|
|
53
|
+
let configFile=null,configText=null;for(const ext of ['ts','js','mjs','mts','cjs','cts']){const name=(framework==='vue'?'vite':'nuxt')+'.config.'+ext,text=await ordinary(root,name);if(text!==null){if(configFile||['cjs','cts'].includes(ext))fail('multiple/CommonJS configurations');configFile=name;configText=text;}}
|
|
54
|
+
literalConfig(configText,configFile??'config.ts',framework);
|
|
55
|
+
const originals=[];
|
|
56
|
+
if(framework==='vue'){
|
|
57
|
+
let entryFile,entry;for(const name of ['src/main.js','src/main.ts']){const text=await ordinary(root,name);if(text!==null){if(entryFile)fail('multiple Vue entrypoints');entryFile=name;entry=text;}}
|
|
58
|
+
if(!entryFile)fail('ordinary src/main.js or src/main.ts required');
|
|
59
|
+
const stripped=entry.replace(/import\s+['"]([^'"]+\.css)['"];?/g,(_,file)=>{if(!/^\.\/[a-zA-Z0-9_./-]+\.css$/.test(file)||file.includes('..'))fail('CSS imports need review');originals.push({path:'src/'+file.slice(2),css:true});return '';}).replace(/\s|;/g,'');
|
|
60
|
+
if(!/^import\{createApp\}from['"]vue['"]importAppfrom['"]\.\/App\.vue['"]createApp\(App\)\.mount\(['"]#app['"]\)$/.test(stripped))fail('custom Vue router, stores or bootstrap');
|
|
61
|
+
const html=await ordinary(root,'index.html');if(!html||!/<head\b[^>]*>[\s\S]*?<\/head>/.test(html)||!new RegExp('<body>\\s*<div id=["\']app["\']><\\/div>\\s*<script type=["\']module["\'] src=["\']/'+entryFile.replace('.','\\.')+'["\']><\\/script>\\s*<\\/body>').test(html)||/<(?:base|iframe)|http-equiv|<script\b/.test(html.replace(/<body>[\s\S]*<\/body>/,'')))fail('custom HTML shell or scripts');
|
|
62
|
+
originals.push({path:entryFile,content:entry},{path:'index.html',content:html});
|
|
63
|
+
for(const item of originals)if(item.css){item.content=await ordinary(root,item.path);if(item.content===null||/@import|url\s*\(/i.test(item.content))fail('CSS import/asset ownership');}
|
|
64
|
+
}else{
|
|
65
|
+
for(const name of ['server','app/plugins','app/middleware','app/layouts','layers','pages','plugins','middleware','.nuxtrc','app/error.vue','app/app.config.ts','app/app.config.js','app.config.ts','app.config.js'])await absent(root,name);
|
|
66
|
+
const app=await ordinary(root,'app/app.vue');if(app!==null&&!/^\s*<template>\s*<NuxtPage\s*\/>\s*<\/template>\s*$/.test(app))fail('custom Nuxt root needs explicit layout review');
|
|
67
|
+
originals.push({path:'app/app.vue',content:app});
|
|
68
|
+
}
|
|
69
|
+
return {root,framework,manifest,packageText,configFile,configText,originals,runtimeDependencies:framework==='vue'?[{name:'vue',majors:[3],minimumMinor:5,production:true,assets:['dist/vue.runtime.esm-bundler.js','server-renderer/index.mjs']},{name:'vite',majors:[7,8],assets:['dist/node/index.js']},{name:'@vitejs/plugin-vue',majors:[6],assets:['dist/index.mjs']}]:[{name:'vue',production:true,majors:[3],minimumMinor:5,assets:['dist/vue.runtime.esm-bundler.js']},{name:'nuxt',majors:[4],assets:['dist/index.mjs','bin/nuxt.mjs']}],nextPackage:scripts(manifest,framework)};
|
|
70
|
+
}
|
|
71
|
+
function generated(project,publication){
|
|
72
|
+
const common=`import sourceConfiguration from './source.config.json' with {type:'json'};\nimport snapshot from './source.generated.json' with {type:'json'};\n${publication?"import configuration from './publication.config.json' with {type:'json'};\nimport contentBindings from './content.bindings.json' with {type:'json'};\n":''}`;
|
|
73
|
+
if(project.framework==='vue'){
|
|
74
|
+
const css=project.originals.filter(item=>item.css).map(item=>`import '../${item.path}';`).join('\n');
|
|
75
|
+
const html=project.originals.find(item=>item.path==='index.html').content.replace(/<body>[\s\S]*<\/body>/,'<body><!--universalcms-app--><!--universalcms-state--><script type="module" src="/.universalcms/vue.client.mjs"></script></body>');
|
|
76
|
+
return [
|
|
77
|
+
['.universalcms/vue.config.mjs',`import {defineConfig} from 'vite';\nimport vue from '@vitejs/plugin-vue';\nimport {fileURLToPath} from 'node:url';\nexport default defineConfig({root:fileURLToPath(new URL('../',import.meta.url)),plugins:[vue()],appType:'custom',server:{fs:{allow:[fileURLToPath(new URL('../',import.meta.url))],deny:['.env','.env.*','*.{crt,pem}','**/.git/**','**/.universalcms/content/**','**/.universalcms/*.json','**/.universalcms/*.txt','**/.universalcms/*.serve.mjs','**/.universalcms/*.build.mjs','**/.universalcms/*.entry-server.mjs','**/.universalcms/*.config.mjs']}},build:{manifest:true,rollupOptions:{input:fileURLToPath(new URL('./vue.index.html',import.meta.url))}}});\n`],
|
|
78
|
+
['.universalcms/vue.index.html',html],
|
|
79
|
+
['.universalcms/vue.app.mjs',`import {createSSRApp} from 'vue';\nimport App from '../src/App.vue';\n${css}\nexport function createApplication(payload){const app=createSSRApp(App);app.provide('universalcms',payload);return app;}\n`],
|
|
80
|
+
['.universalcms/vue.entry-server.mjs',`import {renderToString} from 'vue/server-renderer';\nimport {createApplication} from './vue.app.mjs';\nexport async function render(payload){return renderToString(createApplication(payload));}\n`],
|
|
81
|
+
['.universalcms/vue.client.mjs',`import {defineNativeWebsite} from '@universalcms/connect/native-website';\nimport {createApplication} from './vue.app.mjs';\nconst script=document.getElementById('universalcms-vue-state');\nconst payload=JSON.parse(script.textContent);script.remove();defineNativeWebsite();\nif(!payload||payload.inventory)createApplication(payload).mount(payload?'#universalcms-native':'#app');\n`],
|
|
82
|
+
['.universalcms/vue.build.mjs',`import {build} from 'vite';\nimport {fileURLToPath} from 'node:url';\nconst root=fileURLToPath(new URL('../',import.meta.url));process.chdir(root);\nconst configFile=fileURLToPath(new URL('./vue.config.mjs',import.meta.url));\nawait build({root,configFile,build:{outDir:'.universalcms/dist/client',emptyOutDir:true}});\nawait build({root,configFile,build:{ssr:'.universalcms/vue.entry-server.mjs',outDir:'.universalcms/dist/server',emptyOutDir:true,rollupOptions:{output:{entryFileNames:'vue.entry-server.mjs'}}}});\n`],
|
|
83
|
+
['.universalcms/vue.serve.mjs',`import {fileURLToPath} from 'node:url';\nimport {startVueWebsite} from '@universalcms/connect/vue-server';\nconst root=fileURLToPath(new URL('../',import.meta.url));process.chdir(root);\nconst production=process.argv.includes('--production');\nawait startVueWebsite({root,production,vite:production?undefined:await import('vite')});\n`],
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
['.universalcms/nuxt.config.ts',`import {fileURLToPath} from 'node:url';\n${project.configFile?`import existing from '../${project.configFile}';`:'const existing={};'}\nimport sourceConfiguration from './source.config.json';\nconst local=(name:string)=>fileURLToPath(new URL(name,import.meta.url));\nexport default defineNuxtConfig({...existing,rootDir:local('../'),srcDir:local('../app'),serverDir:local('./nuxt.server'),ssr:true,devServer:{host:'0.0.0.0',port:Number(new URL(sourceConfiguration.websiteOrigin).port||80)},plugins:[local('./nuxt.plugin.ts')],vue:{compilerOptions:{isCustomElement:tag=>tag==='universalcms-website'}},nitro:{preset:'node-server',prerender:{routes:[],crawlLinks:false}},hooks:{'vite:extendConfig'(config){config.server??={};config.server.fs={...config.server.fs,allow:[local('../')],deny:['.env','.env.*','*.{crt,pem}','**/.git/**','**/.universalcms/content/**','**/.universalcms/*.json','**/.universalcms/*.txt','**/.universalcms/nuxt.server/**','**/.universalcms/*.serve.mjs','**/.universalcms/*.config.ts','**/.nuxt/dev/**','**/.nuxt/dist/server/**','**/.output/**']};},'app:resolve'(app){app.mainComponent=local('./nuxt.app.vue');},'pages:extend'(pages){pages.push({name:'universalcms-new-page',path:'/:cmsPath(.*)*',file:local('./nuxt.page.vue')});}}});\n`],
|
|
88
|
+
['.universalcms/nuxt.app.vue',`<script>\nimport {defineComponent,h,inject} from 'vue';\nimport {NuxtPage} from '#components';\nexport default defineComponent({setup(){const cms=inject('universalcms');return ()=>cms?h('universalcms-website',{'data-universalcms-native':'','.cms':cms},cms.inventory?[h(NuxtPage)]:[]):h(NuxtPage);}});\n</script>\n`],
|
|
89
|
+
['.universalcms/nuxt.page.vue',`<script setup>\nimport {inject} from 'vue';\nimport {createError} from '#app';\nif(!inject('universalcms'))throw createError({statusCode:404,statusMessage:'Page not found'});\n</script>\n<template><div></div></template>\n`],
|
|
90
|
+
['.universalcms/nuxt.plugin.ts',`import {defineNuxtPlugin,useState,useRequestEvent} from '#app';\nimport {defineNativeWebsite} from '@universalcms/connect/native-website';\nexport default defineNuxtPlugin(nuxt=>{const state=useState('universalcms',()=>import.meta.server?useRequestEvent()?.context.universalcms?.payload??null:null);nuxt.vueApp.provide('universalcms',state.value);if(import.meta.client)defineNativeWebsite();});\n`],
|
|
91
|
+
['.universalcms/nuxt.server/middleware/universalcms.ts',`import {defineEventHandler,getRequestURL,getRequestHeaders,sendWebResponse,setResponseHeader} from 'h3';\nimport {createVueRuntime} from '@universalcms/connect/vue-runtime';\n${common.replaceAll("'./","'../../")}const runtime=createVueRuntime({${publication?"configuration:configuration as Parameters<typeof createVueRuntime>[0]['configuration'],contentBindings,":''}sourceConfiguration,snapshot,sourceRoot:process.cwd()});\nexport default defineEventHandler(async event=>{const raw=event.node.req.url?.split('?')[0]??'';if(raw.split('/').some(part=>part==='.'||part==='..')||/%(?:2e|2f|5c|25)/i.test(raw))return sendWebResponse(event,new Response(null,{status:400,headers:{'Cache-Control':'no-store'}}));const request=new Request(getRequestURL(event,{xForwardedHost:false,xForwardedProto:false}),{method:event.method,headers:getRequestHeaders(event) as HeadersInit});const result=await runtime.prepare(request);if(!result.pass)return sendWebResponse(event,result.response);event.context.universalcms=result;if(result.payload)for(const [name,value]of Object.entries(result.headers))if(!['content-length','content-type'].includes(name))setResponseHeader(event,name,value as string);});\n`],
|
|
92
|
+
['.universalcms/nuxt.server/plugins/universalcms.ts',`import {finishNuxtHtml} from '@universalcms/connect/vue-runtime';\nexport default defineNitroPlugin(nitro=>{nitro.hooks.hook('render:response',(response,{event})=>{if(event.context.universalcms?.payload){response.body=finishNuxtHtml(response.body,event.context.universalcms);if(response.headers)delete response.headers['content-length'];}});});\n`],
|
|
93
|
+
['.universalcms/nuxt.serve.mjs',`import {fileURLToPath} from 'node:url';\nimport source from './source.config.json' with {type:'json'};\nprocess.chdir(fileURLToPath(new URL('../',import.meta.url)));process.env.NITRO_PORT=String(Number(new URL(source.websiteOrigin).port||80));process.env.NITRO_HOST='0.0.0.0';\nawait import('../.output/server/index.mjs');\n`],
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
export async function planVueIntegration(root,{framework='vue',publication=false,sourceConfiguration}={}){
|
|
97
|
+
const project=await inspectVueProject(root,undefined,framework),manifestFile=`.universalcms/${framework}.integration.json`,previousText=await ordinary(root,manifestFile);
|
|
98
|
+
let previous;if(previousText!==null){try{previous=JSON.parse(previousText);}catch{fail('invalid ownership manifest');}
|
|
99
|
+
if(previous.version!==1||previous.framework!==framework||previous.configFile!==project.configFile||previous.configHash!==(project.configText===null?null:contentDigest(project.configText))||!Array.isArray(previous.files)||JSON.stringify(previous.originals)!==JSON.stringify(project.originals.map(item=>({path:item.path,sha256:item.content===null?null:contentDigest(item.content)}))))fail('original configuration/bootstrap ownership changed');
|
|
100
|
+
const expected=generated(project,previous.publication).map(([path,content])=>({path,sha256:contentDigest(content)}));if(JSON.stringify(expected)!==JSON.stringify(previous.files))fail('generated ownership manifest changed');
|
|
101
|
+
for(const file of previous.files)if(contentDigest(await ordinary(root,file.path)??'')!==file.sha256)fail('generated runtime changed');
|
|
102
|
+
publication ||= previous.publication;
|
|
103
|
+
}
|
|
104
|
+
const desired=generated(project,publication),files=[];
|
|
105
|
+
for(const [name,content]of desired){const current=await ordinary(root,name);if(current!==null&&!previous)fail('existing generated path '+name);files.push({path:name,content,state:current===content?'unchanged':current===null?'create':'update',...(current!==null&¤t!==content?{previous:current}:{})});}
|
|
106
|
+
const manifest=json({version:1,framework,publication,configFile:project.configFile,configHash:project.configText===null?null:contentDigest(project.configText),originals:project.originals.map(item=>({path:item.path,sha256:item.content===null?null:contentDigest(item.content)})),files:desired.map(([path,content])=>({path,sha256:contentDigest(content)}))});
|
|
107
|
+
files.push({path:manifestFile,content:manifest,state:previousText===manifest?'unchanged':previousText===null?'create':'update',...(previousText!==null&&previousText!==manifest?{previous:previousText}:{})});
|
|
108
|
+
if(framework==='nuxt'){project.nextPackage.scripts.start=project.nextPackage.scripts.preview='node .universalcms/nuxt.serve.mjs';}
|
|
109
|
+
const pkg=JSON.stringify(project.nextPackage)===JSON.stringify(project.manifest)?project.packageText:json(project.nextPackage);
|
|
110
|
+
files.push({path:'package.json',content:pkg,state:pkg===project.packageText?'unchanged':'update',...(pkg!==project.packageText?{previous:project.packageText}:{})});
|
|
111
|
+
for(const item of project.originals)if(item.content!==null)files.push({path:item.path,content:item.content,state:'unchanged'});
|
|
112
|
+
if(project.configFile)files.push({path:project.configFile,content:project.configText,state:'unchanged'});
|
|
113
|
+
return {root:project.root,framework,files};
|
|
114
|
+
}
|
|
115
|
+
export async function validateInstalledVue(root,config,framework='vue'){
|
|
116
|
+
const plan=await planVueIntegration(root,{framework,publication:true});if(plan.files.some(file=>file.state!=='unchanged'))fail('complete native runtime setup first');
|
|
117
|
+
const source=await inspectVueSource(root,{framework,locale:config.routes[0]?.locale??'en',websiteOrigin:config.websiteOrigin});
|
|
118
|
+
if(JSON.stringify(source.routes)!==JSON.stringify(config.routes)||await ordinary(root,'.universalcms/source.generated.json')!==json(source.snapshot))fail('source routes/snapshot changed');
|
|
119
|
+
const registry=await readContentRegistry(root);if(!registry||registry.routes.length!==config.routes.length)fail('native content bindings incomplete');
|
|
120
|
+
for(const row of registry.routes){const frozen=await readFrozenContent(root,row);for(const [key,content]of Object.entries(frozen.texts))plan.files.push({path:row[key+'File'],content,state:'unchanged'});}
|
|
121
|
+
plan.files.push({path:CONTENT_BINDINGS_FILE,content:await ordinary(root,CONTENT_BINDINGS_FILE),state:'unchanged'});return {...plan,config};
|
|
122
|
+
}
|
|
123
|
+
export async function planVueSettings(root,{config,source,framework='vue'}){
|
|
124
|
+
const plan=await validateInstalledVue(root,config,framework),name='.universalcms/website.integration.json';
|
|
125
|
+
const content=json({version:1,framework,siteId:config.siteId,environmentId:config.environmentId,apiOrigin:config.apiOrigin,websiteOrigin:config.websiteOrigin,adminOrigin:source.adminOrigin,sections:WEBSITE_SECTIONS,files:plan.files.filter(file=>file.path!=='package.json').map(file=>({path:file.path,sha256:contentDigest(file.content)}))});
|
|
126
|
+
const previous=await ordinary(root,name);if(previous!==null&&previous!==content)fail('settings scope or ownership changed');plan.files.push({path:name,content,state:previous===null?'create':'unchanged'});return {...plan,sections:[...WEBSITE_SECTIONS]};
|
|
127
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Bounded Vue/Nuxt native SFC extraction. Project JavaScript is never evaluated. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { lstat, readdir, realpath } from 'node:fs/promises';
|
|
4
|
+
import { parse } from './vendor/astro-compiler/node/index.js';
|
|
5
|
+
import { extractAstroPage, ASTRO_SOURCE_LIMITS } from './astro-source.mjs';
|
|
6
|
+
import { projectRoot, readProjectFile, SetupError } from './core.mjs';
|
|
7
|
+
import { CONTENT_BINDINGS_FILE, contentDigest, readContentRegistry, readFrozenContent, validateContentRegistry } from './content-registry.mjs';
|
|
8
|
+
import { isExcludedPublicationPath, validatePublicationConfig } from './publication.mjs';
|
|
9
|
+
import { readNativeChromeReview, reviewedNativeChrome, nativeChromeObservation } from './native-chrome-review.mjs';
|
|
10
|
+
const fail = reason => { throw new SetupError(`Vue/Nuxt source needs reviewed integration: ${reason} Existing files were preserved.`); };
|
|
11
|
+
const json = value => JSON.stringify(value,null,2)+'\n';
|
|
12
|
+
const token = value => contentDigest(value).slice(0,20);
|
|
13
|
+
const inspections = new WeakMap();
|
|
14
|
+
async function ordinary(root,name) { const text=await readProjectFile(root,name); if(text!==null&&(await lstat(path.join(root,name))).nlink!==1)fail('linked public source'); return text; }
|
|
15
|
+
function openingEnd(source,start) { let quote; for(let i=start;i<source.length;i++){const c=source[i];if(quote){if(c===quote)quote=null;}else if(c==='"'||c==="'")quote=c;else if(c==='>')return i+1;}fail('incomplete tag'); }
|
|
16
|
+
|
|
17
|
+
export async function extractVuePage({source,slug,locale='en',websiteOrigin,allowNativeChrome=false}={}) {
|
|
18
|
+
if(typeof source!=='string'||Buffer.byteLength(source)>ASTRO_SOURCE_LIMITS.bytes)fail('page byte budget');
|
|
19
|
+
const sfc=/^\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);
|
|
20
|
+
if(!sfc)fail('use a literal template with optional empty script setup and ordinary CSS styles');
|
|
21
|
+
const template=sfc[1];
|
|
22
|
+
if(/\{\{|\}\}|\s(?:v-|[:@#])|<\/?template\b/i.test(template))fail('computed Vue templates, directives and slots require explicit ownership');
|
|
23
|
+
const extracted=await extractAstroPage({source:template,slug,locale,websiteOrigin,allowNativeChrome});
|
|
24
|
+
const parsed=await parse(template,{position:true});
|
|
25
|
+
if(parsed.diagnostics.some(d=>d.severity===1))fail('invalid native template');
|
|
26
|
+
const offsets=new Map([[0,0]]);let byte=0,character=0;
|
|
27
|
+
for(const c of template){byte+=Buffer.byteLength(c);character+=c.length;offsets.set(byte,character);}
|
|
28
|
+
let index=0;const edits=[];
|
|
29
|
+
const visit=node=>{
|
|
30
|
+
if(extracted.inventory.nativeChrome && node.type==='element' && ['header','footer'].includes(node.name))return;
|
|
31
|
+
if(node.type==='element'&&(/^(?:h[1-6]|p|ul|ol|img)$/.test(node.name))){
|
|
32
|
+
const block=extracted.entry.document.blocks[index++];
|
|
33
|
+
const type=node.name==='p'?'paragraph':/^h/.test(node.name)?'heading':node.name==='img'?'image':'list';
|
|
34
|
+
if(!block||block.type!==type)fail('native binding order is ambiguous');
|
|
35
|
+
let start=offsets.get(node.position?.start?.offset),end=offsets.get(node.position?.end?.offset);
|
|
36
|
+
if(start!==undefined&&template[start]!=='<'&&template[start-1]==='<')start--;
|
|
37
|
+
if(start===undefined||!template.startsWith('<'+node.name,start))fail('native source offset mismatch');
|
|
38
|
+
const openEnd=openingEnd(template,start),opening=template.slice(start,openEnd);
|
|
39
|
+
if(type==='image')end=openEnd;
|
|
40
|
+
if(end===undefined||type!=='image'&&!template.slice(start,end).endsWith('</'+node.name+'>'))fail('native closing tag mismatch');
|
|
41
|
+
const expression=`__cms.bindings['${block.id}']`;
|
|
42
|
+
let output;
|
|
43
|
+
if(type==='image'){
|
|
44
|
+
output=opening;
|
|
45
|
+
for(const name of ['src','alt']){
|
|
46
|
+
const attr=node.attributes.find(a=>a.name===name),position=offsets.get(attr?.position?.start?.offset);
|
|
47
|
+
if(position===undefined||attr.kind!=='quoted')fail('image attributes require exact quoted positions');
|
|
48
|
+
const match=new RegExp('^'+name+'\\s*=\\s*').exec(template.slice(position));
|
|
49
|
+
if(!match||template.slice(position+match[0].length,position+match[0].length+attr.raw.length)!==attr.raw)fail('image attribute position mismatch');
|
|
50
|
+
}
|
|
51
|
+
const attrs=node.attributes.filter(a=>['src','alt'].includes(a.name)).map(a=>({start:offsets.get(a.position.start.offset)-start,end:offsets.get(a.position.start.offset)-start+new RegExp('^'+a.name+'\\s*=\\s*').exec(template.slice(offsets.get(a.position.start.offset)))[0].length+a.raw.length,text:`:${a.name}="${expression}.${a.name==='src'?'url':'alt'}"`}));
|
|
52
|
+
for(const edit of attrs.sort((a,b)=>b.start-a.start))output=output.slice(0,edit.start)+edit.text+output.slice(edit.end);
|
|
53
|
+
} else if(type==='list'){
|
|
54
|
+
const item=node.children.find(child=>child.type==='element');
|
|
55
|
+
const itemStart=offsets.get(item.position.start.offset),itemOpen=template.slice(itemStart,openingEnd(template,itemStart));
|
|
56
|
+
output=opening+itemOpen.replace('<li',`<li v-for="(__item, __index) in ${expression}.text.split('\\n')" :key="__index"`)+`{{ __item }}</li></${node.name}>`;
|
|
57
|
+
} else {
|
|
58
|
+
if(node.children.some(child=>child.type!=='text'&&child.type!=='comment'))fail('formatted text requires explicit native bindings');
|
|
59
|
+
output=opening+`{{ ${expression}.text }}</${node.name}>`;
|
|
60
|
+
}
|
|
61
|
+
output=output.replace('<'+node.name,'<'+node.name+` v-if="${expression}"`);
|
|
62
|
+
edits.push({start,end,text:output});return;
|
|
63
|
+
}
|
|
64
|
+
for(const child of node.children??[])visit(child);
|
|
65
|
+
};
|
|
66
|
+
visit(parsed.ast);
|
|
67
|
+
if(index!==extracted.entry.document.blocks.length)fail('incomplete native bindings');
|
|
68
|
+
let output=template;for(const edit of edits.sort((a,b)=>b.start-a.start))output=output.slice(0,edit.start)+edit.text+output.slice(edit.end);
|
|
69
|
+
return {...extracted,source:`<script setup>\nimport { inject } from 'vue';\nconst __cms = inject('universalcms');\n</script>\n<template>${output}</template>\n${sfc[2]}`,imports:[]};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function routeFiles(root,framework) {
|
|
73
|
+
if(framework==='vue'){
|
|
74
|
+
if(await ordinary(root,'src/App.vue')===null)fail('ordinary Vue setup needs src/App.vue');
|
|
75
|
+
for(const dir of ['src/router','src/pages','src/views']){try{await lstat(path.join(root,dir));fail('existing Vue route ownership needs explicit review');}catch(error){if(error.code!=='ENOENT')throw error;}}
|
|
76
|
+
return [{path:'/',sourceFile:'src/App.vue',kind:'page',slug:'home'}];
|
|
77
|
+
}
|
|
78
|
+
if(framework!=='nuxt')fail('select vue or nuxt');
|
|
79
|
+
const routes=[],seen=new Set();let dirs=0,files=0;
|
|
80
|
+
const walk=async(directory,segments=[])=>{
|
|
81
|
+
if(++dirs>256||segments.length>12)fail('route inventory budget');
|
|
82
|
+
const target=path.join(root,directory),info=await lstat(target);
|
|
83
|
+
if(!info.isDirectory()||info.isSymbolicLink()||await realpath(target)!==target)fail('linked route directory');
|
|
84
|
+
for(const item of (await readdir(target,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name))){
|
|
85
|
+
if(++files>1000)fail('route inventory budget');
|
|
86
|
+
if(!/^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.vue)?$/.test(item.name)||item.isSymbolicLink())fail('nonliteral Nuxt routes need review');
|
|
87
|
+
const file=directory+'/'+item.name;
|
|
88
|
+
if(item.isDirectory()){await walk(file,[...segments,item.name]);continue;}
|
|
89
|
+
if(!item.isFile()||!item.name.endsWith('.vue'))fail('non-page Nuxt routes need review');
|
|
90
|
+
const base=item.name.slice(0,-4),pathname='/'+[...segments,...(base==='index'?[]:[base])].join('/');
|
|
91
|
+
if(pathname.split('/').some(part=>/^(?:private|patient|patients|vault|records|account|auth|admin|dashboard|billing|checkout|orders)(?:-|$)/.test(part))||isExcludedPublicationPath(pathname)||['404','500'].includes(base)||seen.has(pathname))fail('private, error or duplicate routes need review');
|
|
92
|
+
seen.add(pathname);routes.push({path:pathname,sourceFile:file,kind:'page',slug:pathname==='/'?'home':pathname.slice(1)});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await walk('app/pages');
|
|
96
|
+
if(!routes.length||seen.has('/')&&seen.has('/home'))fail('missing or ambiguous page identity');
|
|
97
|
+
return routes;
|
|
98
|
+
}
|
|
99
|
+
/** Read public pages and immutable originals; never execute Vue/Nuxt/project code. */
|
|
100
|
+
export async function inspectVueSource(directory,{framework='vue',locale='en',websiteOrigin}={}) {
|
|
101
|
+
const root=await projectRoot(directory), routes=await routeFiles(root,framework), registry=await readContentRegistry(root);
|
|
102
|
+
if (registry?.profile || registry?.routes.some(row=>!row.sourceFile.endsWith('.vue'))) fail('existing content ownership belongs to another adapter');
|
|
103
|
+
const chromeReview=await readNativeChromeReview(root,framework);
|
|
104
|
+
const entries=[], inventories={}, prepared=[], observed=new Map(); let totalBytes=0;
|
|
105
|
+
for (const route of routes) {
|
|
106
|
+
const current=await ordinary(root,route.sourceFile); if(current===null)fail('page disappeared during inspection');
|
|
107
|
+
const installed=registry?.routes.find(row=>row.path===route.path);
|
|
108
|
+
let original=current;
|
|
109
|
+
if (installed) {
|
|
110
|
+
if (installed.sourceFile!==route.sourceFile || installed.slug!==route.slug || installed.locale!==locale || installed.kind!=='page') fail('saved Vue/Nuxt route identity changed');
|
|
111
|
+
original=(await readFrozenContent(root,installed)).texts.original;
|
|
112
|
+
}
|
|
113
|
+
const allowNativeChrome=reviewedNativeChrome(chromeReview,route.sourceFile,original);
|
|
114
|
+
let extracted;
|
|
115
|
+
try {
|
|
116
|
+
extracted=await extractVuePage({source:original,slug:route.slug,locale,filename:route.sourceFile,root,websiteOrigin,allowNativeChrome});
|
|
117
|
+
} catch(error) {
|
|
118
|
+
if(error instanceof SetupError)fail('automatic content binding requires a supported literal Vue/Nuxt template. Review dynamic components, controls and content ownership before setup.');
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
if (installed && extracted.source!==current) fail('generated Vue/Nuxt page changed');
|
|
122
|
+
totalBytes+=Buffer.byteLength(original);
|
|
123
|
+
observed.set(route.sourceFile,{path:route.sourceFile,content:current,state:'unchanged',sha256:contentDigest(current)});
|
|
124
|
+
for (const item of extracted.imports) { observed.set(item.path,item); totalBytes+=Buffer.byteLength(item.content); }
|
|
125
|
+
if(totalBytes>ASTRO_SOURCE_LIMITS.totalBytes)fail('source inventory exceeds its byte budget');
|
|
126
|
+
entries.push(extracted.entry); inventories[route.path]=extracted.inventory;
|
|
127
|
+
prepared.push({...route,locale,original,current,installed,extracted});
|
|
128
|
+
}
|
|
129
|
+
if(registry && registry.routes.length!==routes.length)fail('previously bound routes were removed');
|
|
130
|
+
if(chromeReview)observed.set(chromeReview.path,chromeReview);
|
|
131
|
+
const diagnostics={discoveredPages:routes.length,automaticPages:entries.length,skippedPages:0,unsupportedRoutes:0,warnings:[]};
|
|
132
|
+
const result={entries,snapshot:{version:1,entries,inventories},routes:prepared.map(({path,kind,slug,locale})=>({path,kind,slug,locale})),sourceFiles:[...observed.values()].map(({path,sha256})=>({path,sha256})),diagnostics};
|
|
133
|
+
inspections.set(result,{root,prepared,observed,registry,digest:contentDigest(json(result)),routeFiles:routes,websiteOrigin,framework});
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Core-compatible plan; originals and inventories precede all page replacement. */
|
|
138
|
+
export async function planVueContent(directory,configuration,inspection,framework='vue') {
|
|
139
|
+
const root=await projectRoot(directory),config=validatePublicationConfig(configuration);
|
|
140
|
+
inspection ??= await inspectVueSource(root,{locale:config.routes[0]?.locale,websiteOrigin:config.websiteOrigin,framework});
|
|
141
|
+
const state=inspections.get(inspection);
|
|
142
|
+
if(!state || state.root!==root)fail('use the current inspection for this website');
|
|
143
|
+
if(state.websiteOrigin !== undefined && state.websiteOrigin !== config.websiteOrigin)fail('source and publication website origins must match');
|
|
144
|
+
if(contentDigest(json(inspection))!==state.digest)fail('the public source inspection changed before planning');
|
|
145
|
+
if(json(await routeFiles(root,state.framework))!==json(state.routeFiles))fail('route inventory changed after inspection');
|
|
146
|
+
if(config.routes.length!==inspection.routes.length || config.routes.some(row=>!inspection.routes.some(other=>['path','kind','slug','locale'].every(key=>row[key]===other[key]))))fail('publication identities must match exact Vue/Nuxt source routes');
|
|
147
|
+
const files=[],rows=[];
|
|
148
|
+
for(const observed of state.observed.values()) if(await ordinary(root,observed.path)!==observed.content)fail('source changed after inspection');
|
|
149
|
+
for(const item of state.prepared) {
|
|
150
|
+
if(item.installed) {
|
|
151
|
+
const frozen=await readFrozenContent(root,item.installed);
|
|
152
|
+
if(json(frozen.inventory)!==json(item.extracted.inventory))fail('frozen Vue/Nuxt inventory or layout imports changed');
|
|
153
|
+
for(const name of ['original','native','inventory','source'])files.push({path:item.installed[`${name}File`],content:frozen.texts[name],state:'unchanged'});
|
|
154
|
+
rows.push(item.installed);continue;
|
|
155
|
+
}
|
|
156
|
+
const suffix=token(`${item.path}:${item.kind}:${item.slug}:${item.locale}`),nativeFile=`.universalcms/content/${suffix}.native.txt`,originalFile=`.universalcms/content/${suffix}.original.txt`,inventoryFile=`.universalcms/content/${suffix}.json`;
|
|
157
|
+
for(const filename of [nativeFile,originalFile,inventoryFile])if(await ordinary(root,filename)!==null)fail('a prospective frozen content file already exists');
|
|
158
|
+
const inventoryText=json(item.extracted.inventory),source=item.extracted.source;
|
|
159
|
+
const row={path:item.path,kind:item.kind,slug:item.slug,locale:item.locale,sourceFile:item.sourceFile,nativeFile,originalFile,inventoryFile,sourceHash:contentDigest(source),nativeHash:contentDigest(source),originalHash:contentDigest(item.original),inventoryHash:contentDigest(inventoryText)};
|
|
160
|
+
files.push({path:originalFile,content:item.original,state:'create'},{path:nativeFile,content:source,state:'create'},{path:inventoryFile,content:inventoryText,state:'create'},{path:item.sourceFile,content:source,state:'update',previous:item.current});rows.push(row);
|
|
161
|
+
}
|
|
162
|
+
for(const observed of state.observed.values())if(!files.some(file=>file.path===observed.path))files.push({path:observed.path,content:observed.content,state:'unchanged'});
|
|
163
|
+
const registry=validateContentRegistry({version:1,routes:rows}),text=json(registry),before=await ordinary(root,CONTENT_BINDINGS_FILE);
|
|
164
|
+
if(before!==null && before!==text)fail('saved Vue/Nuxt content registry changed');
|
|
165
|
+
files.push({path:CONTENT_BINDINGS_FILE,content:text,state:before===null?'create':'unchanged'});
|
|
166
|
+
if(files.some(file=>Buffer.byteLength(file.content)>ASTRO_SOURCE_LIMITS.bytes))fail('generated binding exceeds its per-file budget');
|
|
167
|
+
return {root,framework:state.framework,files,routes:rows,entries:inspection.entries,snapshot:inspection.snapshot};
|
|
168
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readSourceSettings } from './source-setup.mjs';
|
|
2
|
+
import { validateSourceConfiguration } from './source-config.mjs';
|
|
3
|
+
import { WEBSITE_SECTIONS } from './website-http.mjs';
|
|
4
|
+
import { publicationCachePolicy } from './publication-check.mjs';
|
|
5
|
+
const names=['websiteOrigin','adminOrigin','siteId','environmentId'];
|
|
6
|
+
const object=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
7
|
+
const exact=(value,keys)=>object(value)&&Object.keys(value).length===keys.length&&Object.keys(value).every(key=>keys.includes(key));
|
|
8
|
+
function untilAborted(pending,signal){
|
|
9
|
+
return new Promise((resolve,reject)=>{
|
|
10
|
+
const abort=()=>reject(Error('Settings diagnostic unavailable'));
|
|
11
|
+
signal.addEventListener('abort',abort,{once:true});if(signal.aborted)abort();
|
|
12
|
+
Promise.resolve(pending).then(value=>{signal.removeEventListener('abort',abort);resolve(value);},error=>{signal.removeEventListener('abort',abort);reject(error);});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function cancel(response){try{if(response?.body&&!response.body.locked)void response.body.cancel().catch(()=>{});}catch{/* Cleanup only. */}}
|
|
16
|
+
export async function checkWebsiteSettings(directory,options={},fetcher=globalThis.fetch,{timeoutMs=10000,maxResponseBytes=128*1024}={}) {
|
|
17
|
+
const failure=message=>({checked:false,renderedVerified:false,message});
|
|
18
|
+
if(!object(options)||Object.keys(options).some(name=>!names.includes(name))||typeof fetcher!=='function'||!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>10000||!Number.isSafeInteger(maxResponseBytes)||maxResponseBytes<1||maxResponseBytes>128*1024)return failure('Use the exact public website, CMS origin, site and environment from Developer setup.');
|
|
19
|
+
let controller,timer,response;
|
|
20
|
+
try {
|
|
21
|
+
const saved=names.some(name=>options[name]===undefined)?await readSourceSettings(directory):{};
|
|
22
|
+
const config=validateSourceConfiguration({version:1,publicStaticPages:true,...Object.fromEntries(names.map(name=>[name,options[name]??saved[name]]))});
|
|
23
|
+
if(config.websiteOrigin===config.adminOrigin)return failure('Website and CMS origins must identify their separate servers.');
|
|
24
|
+
const url=new URL('/api/universalcms/settings',config.websiteOrigin);
|
|
25
|
+
url.search=new URLSearchParams({check:'connection',siteId:config.siteId,environmentId:config.environmentId}).toString();
|
|
26
|
+
controller=new AbortController();timer=setTimeout(()=>controller.abort(),timeoutMs);
|
|
27
|
+
const pending=Promise.resolve(fetcher(url,{method:'GET',headers:{Origin:config.adminOrigin,Accept:'application/json'},credentials:'omit',redirect:'error',cache:'no-store',referrerPolicy:'no-referrer',signal:controller.signal})).then(value=>{if(controller.signal.aborted)cancel(value);return value;});
|
|
28
|
+
response=await untilAborted(pending,controller.signal);
|
|
29
|
+
const directives=(response?.headers?.get('cache-control')??'').toLowerCase().split(',').map(value=>value.trim());
|
|
30
|
+
if(!(response instanceof Response)||response.redirected||['opaque','opaqueredirect'].includes(response.type)||response.url&&response.url!==url.href||response.status!==200||response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase()!=='application/json'||response.headers.get('access-control-allow-origin')!==config.adminOrigin||response.headers.has('access-control-allow-credentials')||!directives.includes('no-store')||!publicationCachePolicy(response.headers,config.websiteOrigin))return failure('Settings endpoint is not ready at this exact website and CMS origin. Run setup-settings, restart or deploy, then check again.');
|
|
31
|
+
const length=response.headers.get('content-length');if(length!==null&&(!/^\d+$/.test(length)||Number(length)>maxResponseBytes))return failure('Settings diagnostic exceeded its response limit.');
|
|
32
|
+
const reader=response.body?.getReader();if(!reader)return failure('Settings diagnostic response is empty.');
|
|
33
|
+
let bytes=0,text='',decoder=new TextDecoder('utf-8',{fatal:true});try{while(true){const {done,value}=await untilAborted(reader.read(),controller.signal);if(controller.signal.aborted)throw Error();if(done)break;if(!(value instanceof Uint8Array)||(bytes+=value.byteLength)>maxResponseBytes)throw Error();text+=decoder.decode(value,{stream:true});}text+=decoder.decode();}finally{void reader.cancel().catch(()=>{});reader.releaseLock();}
|
|
34
|
+
const body=JSON.parse(text);
|
|
35
|
+
if(!exact(body,['version','siteId','environmentId','renderedVerified','settings'])||body.version!==1||body.siteId!==config.siteId||body.environmentId!==config.environmentId||body.renderedVerified!==false||!exact(body.settings,WEBSITE_SECTIONS))return failure('The settings diagnostic does not match this connection.');
|
|
36
|
+
const sections={};
|
|
37
|
+
for(const name of WEBSITE_SECTIONS){const row=body.settings[name];if(!exact(row,['installed','state','version'])||typeof row.installed!=='boolean'||!['ready','missing','denied','unavailable'].includes(row.state)||!Number.isSafeInteger(row.version)||(row.state==='ready'?row.version<1:row.version!==0)||(!row.installed&&(row.state!=='missing'||row.version!==0)))return failure('A settings integration is missing or invalid.');sections[name]={installed:row.installed,state:row.state,version:row.version};}
|
|
38
|
+
return {checked:true,renderedVerified:false,sections,message:'Settings diagnostic checked. Each section reports whether its adapter is installed. Missing adapters require reviewed website integration; denied sections need the intended key access; installed sections marked missing have no saved configuration. Verify each intended feature in the actual website. No settings or content were changed.'};
|
|
39
|
+
}catch{return failure('Cannot read the settings diagnostic. Check the exact saved origins and restart or deploy the website.');}
|
|
40
|
+
finally{clearTimeout(timer);controller?.abort();cancel(response);}
|
|
41
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Optional all-section website integration. Credentials stay in this server module. */
|
|
2
|
+
import { createWebsiteRuntime, websiteMetadata } from './website-runtime.mjs';
|
|
3
|
+
import { isExcludedPublicationPath, normalizePublicationPath, publicationErrorHtml, publicationRevision, validatePublicationConfig } from './publication.mjs';
|
|
4
|
+
import { nativePatternMatches } from './native-routes.mjs';
|
|
5
|
+
import { publicationVerification, publicationRouteHeaders } from './publication-verification.mjs';
|
|
6
|
+
import { REVIEWED_ICECREAM_PROFILE } from './content-registry-validation.mjs';
|
|
7
|
+
export const WEBSITE_SETTINGS_PATH = '/api/universalcms/settings';
|
|
8
|
+
export const WEBSITE_PAGE_PATH = '/api/universalcms/page';
|
|
9
|
+
export const WEBSITE_SECTIONS = Object.freeze(['branding','social','seo','languages','analytics','navigation','experience','scripts','redirects','authors','templates','taxonomies']);
|
|
10
|
+
const headers = { 'Cache-Control':'private, no-store, max-age=0', 'CDN-Cache-Control':'no-store', 'Surrogate-Control':'no-store', 'Vercel-CDN-Cache-Control':'no-store', 'X-Content-Type-Options':'nosniff', 'Referrer-Policy':'no-referrer' };
|
|
11
|
+
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character]));
|
|
12
|
+
function attribute(tag, name) {
|
|
13
|
+
const match = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(tag);
|
|
14
|
+
return (match?.[1] ?? match?.[2] ?? match?.[3] ?? '').toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
// Raw-text elements are one token, so strings inside native scripts/styles are
|
|
17
|
+
// never mistaken for metadata markup and rewritten.
|
|
18
|
+
function headTags(html, transform) {
|
|
19
|
+
return html.replace(/<(script|style|title)\b(?:[^>"']|"[^"]*"|'[^']*')*>[\s\S]*?<\/\1\s*>|<(?:meta|link)\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi, transform);
|
|
20
|
+
}
|
|
21
|
+
export function websiteHtml(html, metadata, {seo=true,branding}={}) {
|
|
22
|
+
return html.replace(/(<head\b[^>]*>)([\s\S]*?)(<\/head>)/i,(_match,opening,source,closing)=>{
|
|
23
|
+
let output=source;
|
|
24
|
+
if(metadata){
|
|
25
|
+
output=headTags(output,tag=>{
|
|
26
|
+
if(/^<title\b/i.test(tag))return '';
|
|
27
|
+
if(/^<script\b/i.test(tag)&&seo&&attribute(tag.slice(0,tag.indexOf('>')+1),'data-universalcms-setting')==='seo')return '';
|
|
28
|
+
if(!/^<(?:meta|link)\b/i.test(tag))return tag;
|
|
29
|
+
const name=attribute(tag,'name'),property=attribute(tag,'property'),rel=attribute(tag,'rel');
|
|
30
|
+
return name==='description'||seo&&(name==='robots'||name.startsWith('twitter:')||property.startsWith('og:')||rel==='canonical'||metadata.alternates&&rel==='alternate'&&attribute(tag,'hreflang'))?'':tag;
|
|
31
|
+
});
|
|
32
|
+
const marker=`data-universalcms-setting="${seo?'seo':'content'}"`;
|
|
33
|
+
const tags = [`<title ${marker}>${escape(metadata.title)}</title>`, `<meta ${marker} name="description" content="${escape(metadata.description)}">`];
|
|
34
|
+
if(seo){
|
|
35
|
+
if(metadata.canonical) tags.push(`<link ${marker} rel="canonical" href="${escape(metadata.canonical)}">`);
|
|
36
|
+
if(metadata.robots) tags.push(`<meta ${marker} name="robots" content="${metadata.robots.index ? 'index':'noindex'},${metadata.robots.follow ? 'follow':'nofollow'}">`);
|
|
37
|
+
for(const [name,value] of Object.entries(metadata.openGraph ?? {})) if(typeof value === 'string') tags.push(`<meta ${marker} property="og:${escape(name==='imageAlt'?'image:alt':name)}" content="${escape(value)}">`);
|
|
38
|
+
for(const [name,value] of Object.entries(metadata.twitter ?? {})) if(typeof value === 'string')tags.push(`<meta ${marker} name="twitter:${escape(name)}" content="${escape(value)}">`);
|
|
39
|
+
for(const alternate of metadata.alternates ?? [])tags.push(`<link ${marker} rel="alternate" hreflang="${escape(alternate.locale)}" href="${escape(alternate.url)}">`);
|
|
40
|
+
if(metadata.jsonLd) tags.push(`<script ${marker} type="application/ld+json">${JSON.stringify(metadata.jsonLd).replace(/</g,'\\u003c').replace(/\u2028/g,'\\u2028').replace(/\u2029/g,'\\u2029')}</script>`);
|
|
41
|
+
}
|
|
42
|
+
output+=tags.join('');
|
|
43
|
+
}
|
|
44
|
+
if(branding){
|
|
45
|
+
output=headTags(output,tag=>/^<link\b/i.test(tag)&&/^(?:shortcut\s+)?icon$/.test(attribute(tag,'rel'))?'':tag);
|
|
46
|
+
if(branding.faviconUrl)output+=`<link data-universalcms-setting="branding" rel="icon" href="${escape(branding.faviconUrl)}">`;
|
|
47
|
+
}
|
|
48
|
+
return opening+output+closing;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function experienceHtml(snapshot) {
|
|
52
|
+
const setting=snapshot.sections.experience;
|
|
53
|
+
if(setting.state!=='ready')return publicationErrorHtml(404);
|
|
54
|
+
const data=setting.data,languages=snapshot.sections.languages;
|
|
55
|
+
const locale=languages.state==='ready'?languages.data.fallback:'en';
|
|
56
|
+
const localized=data.translations?.find(value=>value.locale.toLowerCase()===locale.toLowerCase());
|
|
57
|
+
const copy={...data,...localized},action=copy.notFoundAction;
|
|
58
|
+
return `<!doctype html><html lang="${escape(locale)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>${escape(copy.notFoundTitle||'Page not found')}</title></head><body><main><h1>${escape(copy.notFoundTitle||'Page not found')}</h1><p>${escape(copy.notFoundMessage)}</p>${action?`<a href="${escape(action.url)}">${escape(action.label)}</a>`:''}</main></body></html>`;
|
|
59
|
+
}
|
|
60
|
+
export function createWebsiteRequestHandler({configuration, sourceConfiguration, contentBindings, env = () => process.env, fetcher = globalThis.fetch, renderHtml, installedSections, nativePathPrefixes} = {}) {
|
|
61
|
+
const config = validatePublicationConfig(configuration);
|
|
62
|
+
const verification = publicationVerification(config, sourceConfiguration, contentBindings);
|
|
63
|
+
const nativePaths = verification?.nativePaths ?? [];
|
|
64
|
+
const nativePatterns = verification?.nativePatterns ?? [];
|
|
65
|
+
const reviewedCommerce = verification?.body.contentBindings === true && contentBindings?.profile === REVIEWED_ICECREAM_PROFILE;
|
|
66
|
+
installedSections ??= WEBSITE_SECTIONS;
|
|
67
|
+
nativePathPrefixes ??= reviewedCommerce ? ['/menu/', '/cart/', '/checkout/', '/order-success/'] : [];
|
|
68
|
+
const runtime=createWebsiteRuntime({configuration:config,env,fetcher,nativePaths,nativePatterns,nativePathPrefixes,installedSections});
|
|
69
|
+
const installed = new Set(installedSections);
|
|
70
|
+
const website=new URL(config.websiteOrigin);
|
|
71
|
+
return async function handle(request) {
|
|
72
|
+
const method=request.method;
|
|
73
|
+
const reply = (body,status=200,type='application/json',extra={}) => new Response(method==='HEAD'||status===204 ? null : typeof body==='string' ? body : JSON.stringify(body), {status,headers:{...headers,'Content-Type':type,...extra}});
|
|
74
|
+
let url,path;
|
|
75
|
+
try{url=new URL(request.url);path=normalizePublicationPath(url.pathname);}catch{return reply({error:'Invalid website path'},400);}
|
|
76
|
+
const endpoint = [WEBSITE_SETTINGS_PATH,WEBSITE_PAGE_PATH,'/api/universalcms/document'].includes(url.pathname);
|
|
77
|
+
const seoFile=['/sitemap.xml','/robots.txt'].includes(url.pathname);
|
|
78
|
+
// The composed Next guard otherwise blocks unknown public routes. Preserve
|
|
79
|
+
// the whole reviewed native family, including products added after setup.
|
|
80
|
+
if (nativePatterns.some(pattern => nativePatternMatches(pattern,path)) || nativePathPrefixes.some(prefix => path.startsWith(prefix))) return {pass:true};
|
|
81
|
+
if (path.startsWith('/@') || !endpoint && !seoFile && isExcludedPublicationPath(path) || nativePaths.includes(path) || seoFile && !installed.has('seo')) return null;
|
|
82
|
+
const host=request.headers.get('host');
|
|
83
|
+
const internalLoopback=host===website.host&&['127.0.0.1','[::1]'].includes(website.hostname)&&url.hostname==='localhost'&&url.protocol===website.protocol&&url.port===website.port;
|
|
84
|
+
if (url.origin !== config.websiteOrigin&&!internalLoopback || host&&host!==website.host) return reply({error:'Website origin mismatch'},403);
|
|
85
|
+
if (!['GET','HEAD','OPTIONS'].includes(method)) return reply({error:'Method not allowed'},405,'application/json',{Allow:'GET, HEAD'});
|
|
86
|
+
const diagnostic = url.pathname===WEBSITE_SETTINGS_PATH && url.searchParams.get('check')==='connection';
|
|
87
|
+
const origin=request.headers.get('origin');
|
|
88
|
+
const publicationHeaders=(pathname,value)=>config.routes.some(route=>route.path===pathname)?publicationRouteHeaders(config,pathname,value,verification,origin,request.headers.has('authorization')||request.headers.has('cookie')):{};
|
|
89
|
+
let cors={};
|
|
90
|
+
if(diagnostic) {
|
|
91
|
+
if(!verification || origin!==verification.adminOrigin || request.headers.has('authorization') || request.headers.has('cookie')) return reply({error:'Request not allowed'},403);
|
|
92
|
+
cors={'Access-Control-Allow-Origin':origin,'Vary':'Origin','Access-Control-Allow-Methods':'GET, HEAD, OPTIONS'};
|
|
93
|
+
if(url.searchParams.size!==3 || ['check','siteId','environmentId'].some(name=>url.searchParams.getAll(name).length!==1) || url.searchParams.get('siteId')!==config.siteId || url.searchParams.get('environmentId')!==config.environmentId) return reply({error:'Connection scope mismatch'},400,'application/json',cors);
|
|
94
|
+
if(method==='OPTIONS'){
|
|
95
|
+
if(!['GET','HEAD'].includes(request.headers.get('access-control-request-method'))||request.headers.has('access-control-request-headers'))return reply({error:'Request not allowed'},403);
|
|
96
|
+
return reply(null,204,'application/json',cors);
|
|
97
|
+
}
|
|
98
|
+
} else if(endpoint && (origin && origin!==config.websiteOrigin || request.headers.get('sec-fetch-site')==='cross-site')) return reply({error:'Request not allowed'},403);
|
|
99
|
+
if(method==='OPTIONS') return reply({error:'Method not allowed'},405);
|
|
100
|
+
try {
|
|
101
|
+
if(seoFile){
|
|
102
|
+
if(url.searchParams.size)return reply('Invalid request',400,'text/plain');
|
|
103
|
+
const result=url.pathname==='/robots.txt'?await runtime.robots():await runtime.sitemap();
|
|
104
|
+
if(result.state!=='ready')return reply('Unavailable',result.state==='unavailable'?503:404,'text/plain');
|
|
105
|
+
if(url.pathname==='/robots.txt')return reply(result.body,200,'text/plain; charset=utf-8');
|
|
106
|
+
if(!result.enabled)return reply('Not found',404,'text/plain');
|
|
107
|
+
return reply(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${result.entries.map(entry=>`<url><loc>${escape(entry.url)}</loc><lastmod>${escape(entry.lastModified)}</lastmod></url>`).join('')}</urlset>`,200,'application/xml; charset=utf-8');
|
|
108
|
+
}
|
|
109
|
+
if(url.pathname===WEBSITE_SETTINGS_PATH) {
|
|
110
|
+
if(!diagnostic && url.searchParams.size) return reply({error:'Invalid settings request'},400);
|
|
111
|
+
const snapshot=await runtime.settings();
|
|
112
|
+
if(!diagnostic) return reply(snapshot);
|
|
113
|
+
return reply({version:1,siteId:config.siteId,environmentId:config.environmentId,renderedVerified:false,
|
|
114
|
+
settings:Object.fromEntries(WEBSITE_SECTIONS.map(section=>[section,{installed:installed.has(section),state:installed.has(section)?snapshot.sections[section].state:'missing',version:installed.has(section)?snapshot.sections[section].version:0}]))},200,'application/json',cors);
|
|
115
|
+
}
|
|
116
|
+
let pathname;
|
|
117
|
+
if(endpoint) {
|
|
118
|
+
if(url.searchParams.size!==1||url.searchParams.getAll('path').length!==1) return reply({error:'Invalid page request'},400);
|
|
119
|
+
try {pathname=normalizePublicationPath(url.searchParams.get('path'));} catch {return reply({error:'Invalid path'},400);}
|
|
120
|
+
} else {try {pathname=normalizePublicationPath(url.pathname==='/index.html'?'/':url.pathname);} catch {return reply(publicationErrorHtml(404),404,'text/html');}}
|
|
121
|
+
const original=config.routes.some(route=>route.path===pathname);
|
|
122
|
+
const result=url.pathname===WEBSITE_PAGE_PATH||!endpoint&&renderHtml?await runtime.page(pathname):await runtime.resolve(pathname);
|
|
123
|
+
if(result.state==='unmanaged') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if(result.state==='redirect') return reply('',result.status??302,'text/plain',{Location:result.location});
|
|
127
|
+
if(result.state==='gone') return reply('This page is no longer available.',410,'text/plain',{'X-Robots-Tag':'noindex'});
|
|
128
|
+
if(endpoint) {
|
|
129
|
+
if(result.state==='published') return reply({...result,revision:publicationRevision(result.entry)});
|
|
130
|
+
if(result.state==='archive' && url.pathname===WEBSITE_PAGE_PATH) return reply(result);
|
|
131
|
+
return reply({state:result.state==='unavailable'?'unavailable':'missing'},result.state==='unavailable'?503:404);
|
|
132
|
+
}
|
|
133
|
+
if(result.state!=='published'&&result.state!=='archive') {
|
|
134
|
+
if(result.state==='unavailable')return reply(publicationErrorHtml(503),503,'text/html; charset=utf-8',{'X-Robots-Tag':'noindex',...publicationHeaders(pathname,result)});
|
|
135
|
+
const snapshot=await runtime.settings(),status=snapshot.state==='unavailable'?503:404;
|
|
136
|
+
return reply(status===503?publicationErrorHtml(503):experienceHtml(snapshot),status,'text/html; charset=utf-8',{'X-Robots-Tag':'noindex',...publicationHeaders(pathname,{state:status===503?'unavailable':'missing'})});
|
|
137
|
+
}
|
|
138
|
+
if(!renderHtml) return {pass:true};
|
|
139
|
+
const settings=await runtime.settings();
|
|
140
|
+
if(settings.state==='unavailable')return reply(publicationErrorHtml(503),503,'text/html; charset=utf-8',{'X-Robots-Tag':'noindex',...publicationHeaders(pathname,{state:'unavailable'})});
|
|
141
|
+
const entry=result.entry ?? {document:{title:result.title,excerpt:result.description}};
|
|
142
|
+
const seo=settings.sections.seo.state==='ready';
|
|
143
|
+
const metadata=seo||!original?websiteMetadata(entry,settings,pathname,config.websiteOrigin):null;
|
|
144
|
+
const body=websiteHtml(await renderHtml(pathname),metadata,{seo,branding:settings.sections.branding.state==='ready'?settings.sections.branding.data:undefined});
|
|
145
|
+
const routeHeaders=publicationHeaders(pathname,result);
|
|
146
|
+
return reply(body,200,'text/html; charset=utf-8',routeHeaders);
|
|
147
|
+
} catch {return reply(endpoint?{state:'unavailable'}:publicationErrorHtml(503),503,endpoint?'application/json':'text/html; charset=utf-8',{...cors,...(!endpoint?publicationHeaders(path==='/'||path==='/index.html'?'/':path,{state:'unavailable'}):{})});}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const WEBSITE_SECTIONS: readonly string[];
|
|
2
|
+
export type WebsiteSettings = { version: 1; state: 'ready' | 'unavailable'; sections: Record<string, { state: 'ready' | 'missing' | 'denied' | 'unavailable'; version: number; data?: Record<string, any> }> };
|
|
3
|
+
export type Consent = { version: string; analytics: boolean; marketing: boolean; savedAt: number; expiresAt: number };
|
|
4
|
+
export type ScriptDescriptor = { src: string; placement: string; category: string; provider: string; id?: string; tracker?: string; attributes: Record<string, string> };
|
|
5
|
+
export function record(value: unknown): Record<string, any>;
|
|
6
|
+
export function text(value: unknown, limit?: number): string;
|
|
7
|
+
export function emptyWebsiteSettings(): WebsiteSettings;
|
|
8
|
+
export function parseWebsiteSettings(value: unknown): WebsiteSettings;
|
|
9
|
+
export function sectionData(settings: unknown, name: string): Record<string, any> | null;
|
|
10
|
+
export function publicHref(value: unknown, options?: { mail?: boolean; relative?: boolean }): string | null;
|
|
11
|
+
export function navigationLinks(settings: unknown, location: string, originalLinks?: unknown[], excludeUrls?: string[]): Array<{ label: string; url?: string; children?: Array<{ label: string; url: string }> }>;
|
|
12
|
+
export function languageLinks(settings: unknown, entry: unknown): Array<{ locale: string; path: string; title: string; current: boolean }>;
|
|
13
|
+
export function experienceCopy(settings: unknown, language?: string): Record<string, any> | null;
|
|
14
|
+
export function consentPolicy(settings: unknown): { version: string; maxAgeDays: number } | null;
|
|
15
|
+
export function parseConsent(value: unknown, policy: ReturnType<typeof consentPolicy>, now?: number): Consent | null;
|
|
16
|
+
export function createConsent(policy: ReturnType<typeof consentPolicy>, choices: { analytics?: boolean; marketing?: boolean }, now?: number): Consent | null;
|
|
17
|
+
export function scriptDescriptors(settings: unknown, decision: unknown, privacySignal?: boolean, now?: number): ScriptDescriptor[];
|
|
18
|
+
export function pagePresentation(settings: unknown, entry: unknown): Record<string, any>;
|
|
19
|
+
export function websiteJsonLd(settings: unknown, entry: unknown, fields?: { title?: string; description?: string; canonical?: string | null; image?: string | null }): Record<string, any> | null;
|
|
20
|
+
export function readWebsiteJson(response: Response, signal?: AbortSignal, limit?: number): Promise<any>;
|