@universalcms/connect 0.0.0-stage → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +779 -2
  2. package/dist/angular-runtime.d.mts +4 -0
  3. package/dist/angular-runtime.mjs +7 -0
  4. package/dist/angular-setup.mjs +124 -0
  5. package/dist/astro-runtime.d.mts +6 -0
  6. package/dist/astro-runtime.mjs +255 -0
  7. package/dist/astro-setup.mjs +215 -0
  8. package/dist/astro-source.mjs +451 -0
  9. package/dist/astro-website.d.mts +5 -0
  10. package/dist/astro-website.mjs +42 -0
  11. package/dist/cli.mjs +552 -0
  12. package/dist/connector-runtime.mjs +130 -0
  13. package/dist/content-next-setup.mjs +200 -0
  14. package/dist/content-registry-validation.mjs +40 -0
  15. package/dist/content-registry.mjs +44 -0
  16. package/dist/content-setup.mjs +13 -0
  17. package/dist/content-storefront-setup.mjs +155 -0
  18. package/dist/content-vite-setup.mjs +107 -0
  19. package/dist/core.mjs +205 -0
  20. package/dist/framework-ownership.mjs +68 -0
  21. package/dist/framework.mjs +54 -0
  22. package/dist/guided.mjs +335 -0
  23. package/dist/native-adapters.mjs +31 -0
  24. package/dist/native-chrome-model.d.mts +3 -0
  25. package/dist/native-chrome-model.mjs +14 -0
  26. package/dist/native-chrome-review.mjs +49 -0
  27. package/dist/native-chrome-source.mjs +69 -0
  28. package/dist/native-route-review.mjs +152 -0
  29. package/dist/native-routes.mjs +27 -0
  30. package/dist/native-website-model.d.mts +18 -0
  31. package/dist/native-website-model.mjs +67 -0
  32. package/dist/native-website-server.d.mts +1 -0
  33. package/dist/native-website-server.mjs +35222 -0
  34. package/dist/native-website-server.mjs.LEGAL.txt +56 -0
  35. package/dist/native-website.d.mts +3 -0
  36. package/dist/native-website.mjs +120 -0
  37. package/dist/native-website.mjs.LEGAL.txt +56 -0
  38. package/dist/next-publication.d.mts +12 -0
  39. package/dist/next-publication.mjs +107 -0
  40. package/dist/next-settings-layout.mjs +233 -0
  41. package/dist/next-source.d.mts +8 -0
  42. package/dist/next-source.mjs +93 -0
  43. package/dist/page-discovery.mjs +236 -0
  44. package/dist/page-mapping-review.mjs +160 -0
  45. package/dist/preflight-recovery.mjs +122 -0
  46. package/dist/preflight.mjs +138 -0
  47. package/dist/project-files.mjs +56 -0
  48. package/dist/public-bindings.d.mts +3 -0
  49. package/dist/public-bindings.mjs +28 -0
  50. package/dist/publication-browser.d.mts +5 -0
  51. package/dist/publication-browser.mjs +258 -0
  52. package/dist/publication-check.mjs +141 -0
  53. package/dist/publication-document.d.mts +2 -0
  54. package/dist/publication-document.mjs +59 -0
  55. package/dist/publication-next-setup.mjs +134 -0
  56. package/dist/publication-setup.mjs +138 -0
  57. package/dist/publication-verification.mjs +85 -0
  58. package/dist/publication.css +6 -0
  59. package/dist/publication.d.mts +31 -0
  60. package/dist/publication.mjs +320 -0
  61. package/dist/react-publication.d.mts +22 -0
  62. package/dist/react-publication.mjs +301 -0
  63. package/dist/react-website.d.mts +15 -0
  64. package/dist/react-website.mjs +735 -0
  65. package/dist/reviewed-commerce-ownership.mjs +26 -0
  66. package/dist/reviewed-commerce-settings-setup.mjs +243 -0
  67. package/dist/reviewed-native-ownership.mjs +118 -0
  68. package/dist/reviewed-storefront-source.mjs +227 -0
  69. package/dist/runtime-dependencies.mjs +59 -0
  70. package/dist/setup-environment.mjs +307 -0
  71. package/dist/source-bindings.mjs +77 -0
  72. package/dist/source-check.mjs +149 -0
  73. package/dist/source-collect.mjs +80 -0
  74. package/dist/source-config.mjs +20 -0
  75. package/dist/source-extract.mjs +514 -0
  76. package/dist/source-finalize.mjs +52 -0
  77. package/dist/source-routes.mjs +65 -0
  78. package/dist/source-setup.mjs +490 -0
  79. package/dist/staged-setup.mjs +287 -0
  80. package/dist/static-runtime.mjs +67 -0
  81. package/dist/static-setup.mjs +136 -0
  82. package/dist/svelte-runtime.d.mts +27 -0
  83. package/dist/svelte-runtime.mjs +40 -0
  84. package/dist/svelte-setup.mjs +135 -0
  85. package/dist/svelte-template.mjs +48 -0
  86. package/dist/templates.mjs +248 -0
  87. package/dist/vendor/analyze-project.mjs +82 -0
  88. package/dist/vendor/astro-compiler/LICENSE +53 -0
  89. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  90. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  91. package/dist/vendor/astro-compiler/node/index.js +1 -0
  92. package/dist/vendor/check-delivery.mjs +158 -0
  93. package/dist/vendor/dependency-diagnostics.mjs +72 -0
  94. package/dist/vendor/inspect-installed-dependencies.mjs +125 -0
  95. package/dist/vendor/project-analysis.mjs +498 -0
  96. package/dist/vendor/react-LICENSE.txt +21 -0
  97. package/dist/vendor/react-document.mjs +124 -0
  98. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  99. package/dist/vendor/react-form.mjs +63 -0
  100. package/dist/vendor/react-safety.mjs +47 -0
  101. package/dist/vendor/source-manifest-provider.mjs +104 -0
  102. package/dist/vendor/typescript-LICENSE.txt +55 -0
  103. package/dist/vendor/typescript.cjs +200276 -0
  104. package/dist/vite-handler.mjs +54 -0
  105. package/dist/vite-publication-server.d.mts +3 -0
  106. package/dist/vite-publication-server.mjs +82 -0
  107. package/dist/vite-publication.d.mts +15 -0
  108. package/dist/vite-publication.mjs +241 -0
  109. package/dist/vite-source-discovery.mjs +315 -0
  110. package/dist/vite-source.d.mts +30 -0
  111. package/dist/vite-source.mjs +141 -0
  112. package/dist/vue-runtime.d.mts +10 -0
  113. package/dist/vue-runtime.mjs +50 -0
  114. package/dist/vue-server.d.mts +2 -0
  115. package/dist/vue-server.mjs +58 -0
  116. package/dist/vue-setup.mjs +127 -0
  117. package/dist/vue-source.mjs +168 -0
  118. package/dist/website-check.mjs +41 -0
  119. package/dist/website-http.mjs +149 -0
  120. package/dist/website-model.d.mts +20 -0
  121. package/dist/website-model.mjs +155 -0
  122. package/dist/website-next.d.mts +2 -0
  123. package/dist/website-next.mjs +29 -0
  124. package/dist/website-node.d.mts +3 -0
  125. package/dist/website-node.mjs +50 -0
  126. package/dist/website-runtime.d.mts +13 -0
  127. package/dist/website-runtime.mjs +396 -0
  128. package/dist/website-schema.mjs +22175 -0
  129. package/dist/website-setup.mjs +256 -0
  130. package/dist/website.css +49 -0
  131. package/dist/wizard.mjs +298 -0
  132. package/package.json +60 -4
@@ -0,0 +1,135 @@
1
+ /** Bounded SvelteKit Node setup. Reads literal public project evidence only. */
2
+ import path from 'node:path';
3
+ import {lstat,readdir,realpath} from 'node:fs/promises';
4
+ import ts from './vendor/typescript.cjs';
5
+ import {projectRoot,readProjectFile,SetupError} from './core.mjs';
6
+ import {contentDigest,readContentRegistry,readFrozenContent,validateContentRegistry,CONTENT_BINDINGS_FILE} from './content-registry.mjs';
7
+ import {validatePublicationConfig,isExcludedPublicationPath} from './publication.mjs';
8
+ import {WEBSITE_SECTIONS} from './website-http.mjs';
9
+ import {transformNativeTemplate} from './svelte-template.mjs';
10
+ export const SVELTE_MANIFEST='.universalcms/svelte.integration.json';
11
+ const json=value=>JSON.stringify(value,null,2)+'\n', fail=message=>{throw new SetupError(`SvelteKit automatic setup needs review: ${message}. Existing files were preserved.`);};
12
+ import {readNativeChromeReview,reviewedNativeChrome,nativeChromeObservation} from './native-chrome-review.mjs';
13
+ const inspections=new WeakMap();
14
+ export async function svelteRead(root,file){const text=await readProjectFile(root,file);if(text!==null&&(await lstat(path.join(root,file))).nlink!==1)fail('linked files');return text;}
15
+ export function supportsNativeNode(framework,version){const m=/^(\d+)\.(\d+)\.(\d+)$/.exec(version??'');if(!m)return false;const [,major,minor,patch]=m.map(Number);return framework==='angular'?major>=26||major===24&&minor>=15||major===22&&(minor>22||minor===22&&patch>=3):framework==='svelte'&&(major>22||major===22&&minor>=17);}
16
+ export function svelteVersion(value,major,minor=0,patch=0){const m=typeof value==='string'&&/^[~^]?(\d+)\.(\d+)\.(\d+)$/.exec(value);return !!m&&+m[1]===major&&(+m[2]>minor||+m[2]===minor&&+m[3]>=patch);}
17
+ export async function inspectSvelteAssets(root,directory='static') {
18
+ let count=0;const walk=async(relative,depth=0)=>{let stat;try{stat=await lstat(path.join(root,relative));}catch(e){if(e.code==='ENOENT')return;throw e;}
19
+ if(depth>12||stat.isSymbolicLink()||!stat.isDirectory()||await realpath(path.join(root,relative))!==path.join(root,relative))fail('static assets have unsafe ownership');
20
+ for(const item of await readdir(path.join(root,relative),{withFileTypes:true})){if(++count>5000||item.isSymbolicLink()||!/^[-a-zA-Z0-9_.]+$/.test(item.name)||item.name.startsWith('.')||/^(?:api|admin|private|internal|login|auth|robots\.txt|sitemap\.xml|.*(?:secret|credential|\.env).*)$/i.test(item.name))fail('static assets could shadow private or CMS routes');const filename=relative+'/'+item.name;if(item.isDirectory())await walk(filename,depth+1);else{const info=await lstat(path.join(root,filename));if(!info.isFile()||info.nlink!==1||! /\.(?:css|js|mjs|map|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|pdf|txt|webmanifest|mp[34]|webm|ogg)$/i.test(item.name))fail('static HTML, extensionless or non-asset files');}}
21
+ };await walk(directory);
22
+ }
23
+ function configReview(source,filename) {
24
+ if(typeof source!=='string')fail('missing Vite configuration');
25
+ const file=ts.createSourceFile(filename,source,ts.ScriptTarget.Latest,true,ts.ScriptKind.TS);if(file.parseDiagnostics.length)fail('configuration syntax');
26
+ const imports=new Map();let config;
27
+ for(const statement of file.statements){
28
+ if(ts.isImportDeclaration(statement)&&ts.isStringLiteral(statement.moduleSpecifier)){
29
+ const module=statement.moduleSpecifier.text,clause=statement.importClause;
30
+ if(['@sveltejs/adapter-node','@sveltejs/adapter-auto'].includes(module)&&clause?.name&&!clause.namedBindings)imports.set(clause.name.text,'adapter');
31
+ else if(['vite','@sveltejs/kit/vite'].includes(module)&&!clause?.name&&clause?.namedBindings&&ts.isNamedImports(clause.namedBindings)&&clause.namedBindings.elements.length===1){const item=clause.namedBindings.elements[0],expected=module==='vite'?'defineConfig':'sveltekit';if((item.propertyName?.text??item.name.text)!==expected)fail('custom configuration import');imports.set(item.name.text,expected);}
32
+ else fail('custom configuration import');continue;
33
+ }
34
+ if(ts.isExportAssignment(statement)&&ts.isCallExpression(statement.expression)&&imports.get(statement.expression.expression.getText(file))==='defineConfig'&&statement.expression.arguments.length===1){config=statement.expression.arguments[0];continue;}
35
+ if(!ts.isEmptyStatement(statement))fail('custom configuration code');
36
+ }
37
+ if(!config||!ts.isObjectLiteralExpression(config)||config.properties.length!==1)fail('custom Vite configuration');
38
+ const plugins=config.properties[0];if(!ts.isPropertyAssignment(plugins)||plugins.name.getText(file)!=='plugins'||!ts.isArrayLiteralExpression(plugins.initializer)||plugins.initializer.elements.length!==1)fail('custom Vite plugins');
39
+ const kit=plugins.initializer.elements[0];if(!ts.isCallExpression(kit)||imports.get(kit.expression.getText(file))!=='sveltekit'||kit.arguments.length!==1||!ts.isObjectLiteralExpression(kit.arguments[0])||kit.arguments[0].properties.length!==1)fail('custom Kit options');
40
+ const adapter=kit.arguments[0].properties[0];if(!ts.isPropertyAssignment(adapter)||adapter.name.getText(file)!=='adapter'||!ts.isCallExpression(adapter.initializer)||imports.get(adapter.initializer.expression.getText(file))!=='adapter'||adapter.initializer.arguments.length)fail('ordinary Node adapter required');
41
+ }
42
+ export async function inspectSvelteProject(directory){
43
+ if(!supportsNativeNode('svelte',process.versions.node))fail('Node.js 22.17 or newer is required before setup');
44
+ const root=await projectRoot(directory),manifest=JSON.parse(await svelteRead(root,'package.json')??'null'),deps={...manifest?.devDependencies,...manifest?.dependencies};
45
+ for(const [name,major,minor,patch=0] of [['@sveltejs/kit',3,1],['svelte',5,57,1],['@sveltejs/adapter-node',6,0],['@sveltejs/vite-plugin-svelte',7,3],['vite',8,0,12],['typescript',6,0,2]]){
46
+ if(!svelteVersion(deps[name],major,minor,patch))fail(`requires declared ${name} ${major}.${minor}+`);
47
+ const installed=JSON.parse(await svelteRead(root,`node_modules/${name}/package.json`)??'null');if(installed?.name!==name||!svelteVersion(installed.version,major,minor,patch))fail(`install compatible ${name}`);
48
+ }
49
+ if(manifest.workspaces||['next','nuxt','astro','@angular/core','vue'].some(name=>deps[name]))fail('competing frameworks or workspace');
50
+ const config='';
51
+ for(const name of ['svelte.config.js','svelte.config.ts','svelte.config.mjs','svelte.config.cjs','src/hooks.js','src/hooks.ts','src/hooks.client.js','src/hooks.client.ts','src/hooks.server.ts','src/service-worker.js','src/service-worker.ts'])if(await svelteRead(root,name)!==null)fail('custom hooks or alternative configuration');
52
+ let viteFile=null,viteSource=null;for(const name of ['vite.config.js','vite.config.ts']){const text=await svelteRead(root,name);if(text!==null){if(viteFile)fail('multiple Vite configurations');viteFile=name;viteSource=text;}}
53
+ configReview(viteSource,viteFile);
54
+ await inspectSvelteAssets(root);
55
+ return {root,manifest,config,configFile:viteFile,viteFile,viteSource};
56
+ }
57
+ async function routes(root){const ownershipText=await svelteRead(root,SVELTE_MANIFEST),ownership=ownershipText?JSON.parse(ownershipText):null;const rows=[];let count=0;const walk=async(directory,segments=[])=>{const stat=await lstat(path.join(root,directory));if(stat.isSymbolicLink()||!stat.isDirectory()||await realpath(path.join(root,directory))!==path.join(root,directory))fail('linked route directory');for(const item of (await readdir(path.join(root,directory),{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name))){if(++count>1000||segments.length>10||item.isSymbolicLink())fail('route inventory budget or linked route');const filename=directory+'/'+item.name;
58
+ if(item.isDirectory()){if(item.name==='[...cmsPath]'){if(segments.length||!ownership?.publication)fail('unowned CMS catch-all route');const children=await readdir(path.join(root,filename),{withFileTypes:true});if(children.length!==2||children.some(child=>!child.isFile()||!['+page.svelte','+page.server.js'].includes(child.name)))fail('custom CMS catch-all files');for(const child of children){const file=filename+'/'+child.name,expected=ownership.files?.find(row=>row.path===file);if(!expected||contentDigest(await svelteRead(root,file))!==expected.sha256)fail('changed CMS catch-all');}continue;}if(!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.name)||isExcludedPublicationPath('/'+item.name))fail('dynamic/private route');await walk(filename,[...segments,item.name]);}
59
+ else if(item.name==='+page.svelte')rows.push({path:'/'+segments.join('/'),kind:'page',slug:segments.length?segments.join('/'):'home',sourceFile:filename});
60
+ else if(item.name!=='+page.server.js')fail('custom server/layout route');
61
+ }};await walk('src/routes');if(!rows.length||rows.some(r=>r.path==='/home')&&rows.some(r=>r.path==='/'))fail('missing or duplicate home identity');return rows;}
62
+ function pageTemplate(content){return `<script>\nimport { onMount } from 'svelte';\nimport { defineNativeWebsite } from '@universalcms/connect/native-website';\nlet { data } = $props();\nlet host;\nonMount(() => { defineNativeWebsite(); const cms=data.universalcms; host.cms={pathname:cms.pathname,scope:cms.scope,initialResult:cms.result,initialSettings:cms.settings,...(cms.publicInventory?{inventory:cms.publicInventory}:{})}; });\n</script>\n<universalcms-website bind:this={host}>${content}</universalcms-website>\n`;}
63
+ export async function inspectSvelteSource(directory,{locale='en',websiteOrigin}={}){
64
+ const root=await projectRoot(directory),list=await routes(root),registry=await readContentRegistry(root),prepared=[],entries=[],inventories={};
65
+ const chromeReview=await readNativeChromeReview(root,'svelte');
66
+ if(registry?.profile||registry?.routes.some(row=>!row.sourceFile.endsWith('.svelte')))fail('existing content belongs to another adapter');
67
+ for(const route of list){const current=await svelteRead(root,route.sourceFile),installed=registry?.routes.find(row=>row.path===route.path),original=installed?(await readFrozenContent(root,installed)).texts.original:current;
68
+ const allowNativeChrome=reviewedNativeChrome(chromeReview,route.sourceFile,original);
69
+ let extracted;
70
+ try {
71
+ extracted=await transformNativeTemplate({source:original,slug:route.slug,locale,websiteOrigin,framework:'svelte',allowNativeChrome});
72
+ } catch(error) {
73
+ if(error instanceof SetupError)fail('automatic content binding requires a supported literal Svelte template; review dynamic components, controls and content ownership before setup');
74
+ throw error;
75
+ }
76
+ extracted.source = pageTemplate(extracted.source);
77
+ if(installed&&(installed.sourceFile!==route.sourceFile||installed.locale!==locale||current!==extracted.source))fail('changed native content ownership');
78
+ prepared.push({...route,locale,current,original,installed,extracted});entries.push(extracted.entry);inventories[route.path]=extracted.inventory;
79
+ }
80
+ if(registry&&registry.routes.length!==list.length)fail('removed installed route');
81
+ const result={entries,routes:prepared.map(({path,kind,slug,locale})=>({path,kind,slug,locale})),snapshot:{version:1,entries,inventories},sourceFiles:[...prepared.map(row=>({path:row.sourceFile,sha256:contentDigest(row.current)})),...(chromeReview?[{path:chromeReview.path,sha256:chromeReview.sha256}]:[])],diagnostics:{discoveredPages:list.length,automaticPages:list.length,skippedPages:0,unsupportedRoutes:0,warnings:[]}};
82
+ inspections.set(result,{root,prepared,chromeReview,digest:contentDigest(json(result))});return result;
83
+ }
84
+ export async function planSvelteContent(directory,configuration,inspection){
85
+ const root=await projectRoot(directory),config=validatePublicationConfig(configuration);inspection??=await inspectSvelteSource(root,{locale:config.routes[0]?.locale,websiteOrigin:config.websiteOrigin});const state=inspections.get(inspection);
86
+ if(!state||state.root!==root||state.digest!==contentDigest(json(inspection))||json(inspection.routes)!==json(config.routes))fail('source/publication inspection changed');
87
+ const files=await nativeChromeObservation(root,state.chromeReview),rows=[];for(const item of state.prepared){if(await svelteRead(root,item.sourceFile)!==item.current)fail('source changed after inspection');
88
+ if(item.installed){const frozen=await readFrozenContent(root,item.installed);for(const name of ['original','native','inventory','source'])files.push({path:item.installed[name+'File'],content:frozen.texts[name],state:'unchanged'});rows.push(item.installed);continue;}
89
+ const token=contentDigest(item.path+item.locale).slice(0,20),originalFile=`.universalcms/content/${token}.original.txt`,nativeFile=`.universalcms/content/${token}.native.txt`,inventoryFile=`.universalcms/content/${token}.json`,source=item.extracted.source,inventory=json(item.extracted.inventory);
90
+ for(const file of [originalFile,nativeFile,inventoryFile])if(await svelteRead(root,file)!==null)fail('frozen content collision');
91
+ files.push({path:originalFile,content:item.original,state:'create'},{path:nativeFile,content:source,state:'create'},{path:inventoryFile,content:inventory,state:'create'},{path:item.sourceFile,content:source,state:'update',previous:item.current});
92
+ rows.push({...Object.fromEntries(['path','kind','slug','locale','sourceFile'].map(key=>[key,item[key]])),originalFile,nativeFile,inventoryFile,sourceHash:contentDigest(source),nativeHash:contentDigest(source),originalHash:contentDigest(item.original),inventoryHash:contentDigest(inventory)});
93
+ }
94
+ const registry=validateContentRegistry({version:1,routes:rows}),text=json(registry),previous=await svelteRead(root,CONTENT_BINDINGS_FILE);if(previous!==null&&previous!==text)fail('content registry changed');files.push({path:CONTENT_BINDINGS_FILE,content:text,state:previous===null?'create':'unchanged'});return{root,framework:'svelte',files,routes:rows,entries:inspection.entries,snapshot:inspection.snapshot};
95
+ }
96
+ export async function planSvelteIntegration(directory,{publication=false,sourceConfiguration}={}){
97
+ const project=await inspectSvelteProject(directory),{root}=project,manifestText=await svelteRead(root,SVELTE_MANIFEST),prior=manifestText?JSON.parse(manifestText):null;
98
+ if(prior?.publication)publication=true;
99
+ const app=await svelteRead(root,'src/app.html');if(!app||!app.includes('%sveltekit.head%')||!app.includes('%sveltekit.body%')||/<script\b|\son\w+\s*=/i.test(app))fail('custom application shell');
100
+ const appSource=prior?.originalApp??app;if(prior&&prior.originalAppHash!==contentDigest(appSource))fail('application ownership changed');
101
+ const appOutput=appSource.replace(/<body\b([^>]*)>/,(_m,attrs)=>`<body${attrs.replace(/\sdata-sveltekit-(?:reload|preload-data)(?:=(?:"[^"]*"|'[^']*'))?/g,'')} data-sveltekit-reload data-sveltekit-preload-data="false">`);
102
+ const hook=`import { createSvelteRuntime } from '@universalcms/connect/svelte-runtime';\nimport { renderNativeWebsite } from '@universalcms/connect/native-website-server';\nimport sourceConfiguration from '../.universalcms/source.config.json';\nimport snapshot from '../.universalcms/source.generated.json';\n${publication?"import configuration from '../.universalcms/publication.config.json';\nimport contentBindings from '../.universalcms/content.bindings.json';\n":''}const runtime=createSvelteRuntime({${publication?'configuration,contentBindings,':''}sourceConfiguration,snapshot,sourceRoot:process.cwd(),renderWebsite:renderNativeWebsite});\nexport const handle=runtime.handle;\n`;
103
+ const load=`export const prerender=false;\nexport const load=({locals})=>({universalcms:locals.universalcms});\n`;
104
+ const generated=[['src/hooks.server.js',hook],['src/app.html',appOutput],['.universalcms/svelte.vite.config.mjs',`import adapter from '@sveltejs/adapter-node';\nimport {sveltekit} from '@sveltejs/kit/vite';\nimport config from './source.config.json' with {type:'json'};\nconst port=Number(new URL(config.websiteOrigin).port||(config.websiteOrigin.startsWith('https:')?443:80));\nconst privateFiles={name:'universalcms-private-files',enforce:'pre',configureServer(server){server.middlewares.use((request,response,next)=>{try{const pathname=decodeURIComponent(new URL(request.url,'http://localhost').pathname);if(pathname.includes('%')||pathname.split('/').some(part=>['.universalcms','.git','.ssh','.aws'].includes(part)||/\\.(?:crt|pem|key)$/i.test(part)||part==='.env'||part.startsWith('.env.'))){response.statusCode=404;response.setHeader('Cache-Control','no-store');response.end('Not found');return;}}catch{response.statusCode=400;response.end('Invalid URL');return;}next();});}};\nexport default {plugins:[privateFiles,sveltekit({adapter:adapter(),paths:{origin:config.websiteOrigin}})],server:{port,strictPort:true,fs:{deny:['**/.env','**/.env.*','**/.universalcms/**','**/.git/**','**/.ssh/**','**/.aws/**','**/*.{crt,pem,key}']}},preview:{port,strictPort:true}};\n`],['.universalcms/svelte.serve.mjs',`import {fileURLToPath} from 'node:url';\nprocess.chdir(fileURLToPath(new URL('../',import.meta.url)));\nimport config from './source.config.json' with {type:'json'};\nprocess.env.ORIGIN=config.websiteOrigin;process.env.PORT=String(new URL(config.websiteOrigin).port||(config.websiteOrigin.startsWith('https:')?443:80));\nawait import('../build/index.js');\n`]];
105
+ if(publication){for(const row of await routes(root))generated.push([path.posix.dirname(row.sourceFile)+'/+page.server.js',load]);generated.push(['src/routes/[...cmsPath]/+page.svelte',pageTemplate('')],['src/routes/[...cmsPath]/+page.server.js',load]);}
106
+ const scripts={...project.manifest.scripts};for(const name of ['dev','build','preview']){const expected=name==='build'?'vite build':`vite ${name==='dev'?'':name}`.trim(),generated=expected+' --config .universalcms/svelte.vite.config.mjs';if(scripts[name]!==undefined&&scripts[name]!==expected&&scripts[name]!==generated)fail('custom startup/build scripts');if(name!=='preview'&&scripts[name]===undefined)fail('missing startup/build scripts');if(scripts[name]!==undefined)scripts[name]=generated;}
107
+ const start='node .universalcms/svelte.serve.mjs';if(scripts.start&&scripts.start!==start&&scripts.start!=='node build')fail('custom production startup');scripts.start=start;
108
+ const verify='universalcms-connect sync-source';for(const name of ['predev','prebuild','prestart','prepreview']){if(scripts[name]&&scripts[name]!==verify)fail('custom lifecycle hooks');scripts[name]=verify;}
109
+ const oldFiles=new Map((prior?.files??[]).map(item=>[item.path,item.sha256])),files=[];
110
+ if(prior&&(prior.version!==1||prior.configHash!==contentDigest(project.config)||prior.viteHash!==contentDigest(project.viteSource)))fail('framework configuration changed');
111
+ for(const [file,content]of generated){const before=await svelteRead(root,file),owned=oldFiles.get(file);if(file==='src/app.html'){if(before!==appSource&&before!==appOutput)fail('application shell changed');}else if(before!==null&&(!owned||contentDigest(before)!==owned))fail('custom runtime or load file');files.push({path:file,content,state:before===content?'unchanged':before===null?'create':'update',...(before!==null&&before!==content?{previous:before}:{})});}
112
+ files.push({path:project.viteFile,content:project.viteSource,state:'unchanged'});
113
+ const packageBefore=await svelteRead(root,'package.json'),packageAfter=json({...project.manifest,scripts});files.push({path:'package.json',content:packageAfter,state:packageBefore===packageAfter?'unchanged':'update',previous:packageBefore});
114
+ const ownership={version:1,publication,configHash:contentDigest(project.config),viteHash:contentDigest(project.viteSource),originalApp:appSource,originalAppHash:contentDigest(appSource),files:generated.map(([path,content])=>({path,sha256:contentDigest(content)}))};
115
+ files.push({path:SVELTE_MANIFEST,content:json(ownership),state:manifestText===json(ownership)?'unchanged':manifestText===null?'create':'update',...(manifestText?{previous:manifestText}:{})});
116
+ return{root,framework:'svelte',files,project};
117
+ }
118
+ export async function validateInstalledSvelte(directory,configuration){
119
+ const plan=await planSvelteIntegration(directory,{publication:true,sourceConfiguration:configuration});
120
+ if(plan.files.some(file=>file.state!=='unchanged'))fail('integration incomplete or changed');
121
+ const inspection=await inspectSvelteSource(directory,{locale:configuration.routes[0]?.locale,websiteOrigin:configuration.websiteOrigin});
122
+ if(json(inspection.routes)!==json(configuration.routes)||await svelteRead(plan.root,'.universalcms/source.generated.json')!==json(inspection.snapshot))fail('published route or frozen source inventory changed');
123
+ const registry=await readContentRegistry(plan.root);if(!registry||registry.routes.length!==configuration.routes.length)fail('published-content bindings incomplete');
124
+ const content=await planSvelteContent(directory,configuration,inspection);if(content.files.some(file=>file.state!=='unchanged'))fail('native content bindings incomplete');
125
+ plan.files.push(...content.files);return{...plan,config:configuration};
126
+ }
127
+ export async function planSvelteSettings(directory,{config,source}){
128
+ const plan=await validateInstalledSvelte(directory,config),name='.universalcms/website.integration.json';
129
+ const unique=new Map(plan.files.filter(file=>file.path!=='package.json').map(file=>[file.path,{path:file.path,sha256:contentDigest(file.content)}]));
130
+ const content=json({version:1,framework:'svelte',siteId:config.siteId,environmentId:config.environmentId,apiOrigin:config.apiOrigin,websiteOrigin:config.websiteOrigin,adminOrigin:source.adminOrigin,sections:WEBSITE_SECTIONS,files:[...unique.values()]}),previous=await svelteRead(plan.root,name);
131
+ if(previous!==null&&previous!==content)fail('settings ownership or scope changed');
132
+ plan.files.push({path:name,content,state:previous===null?'create':'unchanged'});
133
+ for(const name of ['.universalcms/publication.config.json','.universalcms/source.config.json','.universalcms/source.generated.json'])plan.files.push({path:name,content:await svelteRead(plan.root,name),state:'unchanged'});
134
+ return{...plan,sections:[...WEBSITE_SECTIONS]};
135
+ }
@@ -0,0 +1,48 @@
1
+ /** Literal native HTML projection shared by the Svelte and Angular adapters.
2
+ * The bundled parser validates inert source; customer modules are never loaded. */
3
+ import { parse } from './vendor/astro-compiler/node/index.js';
4
+ import { extractAstroPage } from './astro-source.mjs';
5
+ import { SetupError } from './core.mjs';
6
+ const fail = () => { throw new SetupError('Native template requires a reviewed adapter. Automatic setup accepts literal editorial HTML and styles; custom components, expressions, scripts and chrome are preserved for review.'); };
7
+ const escape = value => JSON.stringify(value);
8
+ export async function transformNativeTemplate({source,slug,locale='en',websiteOrigin,framework,allowNativeChrome=false}) {
9
+ if (!['svelte','angular'].includes(framework) || typeof source !== 'string' || /[{}]|<(?:script|svelte:|ng-|router-|slot\b)|\s(?:on\w+|\[[^\]]+\]|\([^)]*\)|\*\w+)\s*=/i.test(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi,''))) fail();
10
+ const extracted = await extractAstroPage({source,slug,locale,websiteOrigin,allowNativeChrome});
11
+ const parsed = await parse(source,{position:true}), edits=[]; let cursor=0;
12
+ const offsets=new Map([[0,0]]); let bytes=0,chars=0;
13
+ for(const character of source){bytes+=Buffer.byteLength(character);chars+=character.length;offsets.set(bytes,chars);}
14
+ const start=node=>{let at=offsets.get(node.position?.start?.offset);if(source[at]!=='<'&&source[at-1]==='<')at--;if(!Number.isInteger(at)||!source.startsWith('<'+node.name,at))fail();return at;};
15
+ const opening=node=>{const at=start(node);let quote=null;for(let i=at;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();};
16
+ const end=node=>node.name==='img'?opening(node):offsets.get(node.position?.end?.offset);
17
+ const visit=node=>{
18
+ if(node.type!=='element'){for(const child of node.children??[])visit(child);return;}
19
+ if(node.name==='style'||node.name==='head'||extracted.inventory.nativeChrome&&['header','footer'].includes(node.name))return;
20
+ if(!/^(?:h[1-6]|p|ul|ol|img)$/.test(node.name)){for(const child of node.children??[])visit(child);return;}
21
+ const block=extracted.entry.document.blocks[cursor++];if(!block)fail();
22
+ const from=start(node),to=end(node);if(!Number.isInteger(to)||to<=from)fail();
23
+ const lookup=framework==='svelte'?`data.universalcms.bindings[${escape(block.id)}]`:`cms.binding(${escape(block.id)})`;
24
+ let native=source.slice(from,to),open=source.slice(from,opening(node)),close=node.name==='img'?'':`</${node.name}>`;
25
+ if(block.type==='image') {
26
+ open=open.replace(/\s(?:src|alt)\s*=\s*(?:"[^"]*"|'[^']*')/g,'');
27
+ const attrs=framework==='svelte'?` src={${lookup}.url} alt={${lookup}.alt}`:` [src]="${lookup.replaceAll('"',"'")}!.url" [alt]="${lookup.replaceAll('"',"'")}!.alt"`;
28
+ native=open.replace(/\s*\/?>$/,attrs+' />');
29
+ } else if(block.type==='list') {
30
+ const first=node.children.find(child=>child.type==='element'&&child.name==='li'),itemOpen=source.slice(start(first),opening(first));
31
+ const item=framework==='svelte'?`{#each ${lookup}.text.split('\\n') as item}${itemOpen}{item}</li>{/each}`:`@for (item of ${lookup.replaceAll('"',"'")}!.text.split('\\n'); track $index) {${itemOpen}{{item}}</li>}`;
32
+ native=open+item+close;
33
+ } else {
34
+ // Native wrappers retain their order. Editorial text is projected once;
35
+ // no frozen source copy is emitted into the browser bundle.
36
+ const value=framework==='svelte'?`{${lookup}.text}`:`{{${lookup.replaceAll('"',"'")}!.text}}`;
37
+ const replacements=[];let first=true;
38
+ const visitText=child=>{if(child.type==='text'&&child.value.trim()){const at=offsets.get(child.position?.start?.offset),to=offsets.get(child.position?.end?.offset);if(!Number.isInteger(at)||!Number.isInteger(to)||source.slice(at,to)!==child.value)fail();replacements.push({from:at-from,to:to-from,value:first?value:''});first=false;}else for(const nested of child.children??[])visitText(nested);};
39
+ for(const child of node.children??[])visitText(child);
40
+ if(first)fail();
41
+ for(const change of replacements.sort((a,b)=>b.from-a.from))native=native.slice(0,change.from)+change.value+native.slice(change.to);
42
+ }
43
+ edits.push({from,to,value:framework==='svelte'?`{#if ${lookup}}${native}{/if}`:`@if (${lookup.replaceAll('"',"'")}) {${native}}`});
44
+ };
45
+ visit(parsed.ast);if(cursor!==extracted.entry.document.blocks.length)fail();
46
+ let output=source;for(const edit of edits.sort((a,b)=>b.from-a.from))output=output.slice(0,edit.from)+edit.value+output.slice(edit.to);
47
+ return {...extracted,source:output};
48
+ }
@@ -0,0 +1,248 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ export async function createTemplates(config, { existingNodeServer = false } = {}) {
4
+ const json = value => JSON.stringify(value);
5
+ const env = `# Set these in your PRIVATE hosting/server environment. This file contains no key.\nCMS_API_BASE_URL=${config.apiOrigin}\nCMS_SITE_ID=${config.siteId}\nCMS_ENVIRONMENT_ID=${config.environmentId}\nCMS_API_KEY=\n${config.framework === 'vite' ? 'CMS_WEBSITE_ORIGIN=http://localhost:5173\nCMS_CONNECT_PORT=4318\nCMS_CONNECT_HOST=127.0.0.1\n' : ''}`;
6
+ const common = [
7
+ { path: '.universalcms/server.env.example', content: env },
8
+ { path: '.universalcms/SETUP.md', content: stagedSetupGuide(instructions(config, existingNodeServer)) },
9
+ ];
10
+ const clientOptions = `baseUrl: process.env.CMS_API_BASE_URL ?? ${json(config.apiOrigin)},\n siteId: ${json(config.siteId)},\n environmentId: ${json(config.environmentId)},\n apiKey: process.env.CMS_API_KEY,\n retries: 0,`;
11
+ if (config.framework === 'next') {
12
+ const route = `${config.appDirectory}${config.mount}`;
13
+ return [...common,
14
+ { path: `${route}/cms.server.js`, content: `import 'server-only';\nimport { createNextCmsClient } from '@universalcms/next';\n\n// Called during a server request; never imported into a client component.\nexport function getCms() {\n return createNextCmsClient({\n ${clientOptions}\n });\n}\n` },
15
+ { path: `${route}/[...slug]/page.jsx`, content: `import { notFound } from 'next/navigation';\nimport { CmsDocument } from '@universalcms/react';\nimport '@universalcms/react/styles.css';\nimport { getCms } from '../cms.server.js';\n\nexport const runtime = 'nodejs';\nexport const dynamic = 'force-dynamic';\n\nexport default async function CmsPage({ params }) {\n const { slug: segments } = await params;\n const slug = Array.isArray(segments) ? segments.join('/') : '';\n if (slug.length > 160 || !/^[a-z0-9]+(?:[-/][a-z0-9]+)*$/.test(slug)) notFound();\n const entry = await getCms().content.getBySlug(slug, { kind: 'page', locale: ${json(config.locale)} });\n if (!entry) notFound();\n return <article><h1>{entry.document.title}</h1>\n <CmsDocument document={entry.document} entryId={entry.id} />\n </article>;\n}\n` },
16
+ { path: `${route}/[...slug]/error.jsx`, content: `'use client';\nexport default function CmsError({ reset }) {\n return <section role="alert"><h1>Content is temporarily unavailable</h1>\n <button type="button" onClick={reset}>Try again</button></section>;\n}\n` },
17
+ ];
18
+ }
19
+ const handler = await readFile(new URL('./vite-handler.mjs', import.meta.url), 'utf8');
20
+ return [...common,
21
+ { path: '.universalcms/page-handler.mjs', content: handler },
22
+ { path: '.universalcms/server.mjs', content: `import { createServer } from 'node:http';\nimport { createCmsClient } from '@universalcms/sdk';\nimport { createPageHandler } from './page-handler.mjs';\n\ntry {\n const cms = createCmsClient({\n ${clientOptions}\n });\n const port = Number(process.env.CMS_CONNECT_PORT ?? '4318');\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw Error();\n const handler = createPageHandler(cms, { locale: ${json(config.locale)}, websiteOrigin: process.env.CMS_WEBSITE_ORIGIN });\n const server = createServer({ requestTimeout: 15_000, headersTimeout: 10_000, maxHeaderSize: 8192 }, (req, res) => { void handler(req, res); });\n server.on('error', () => { console.error('CMS connector could not start. Check server configuration.'); process.exitCode = 1; });\n server.listen(port, process.env.CMS_CONNECT_HOST ?? '127.0.0.1', () => console.log('CMS connector listening. Route /api/universalcms/page through your website proxy.'));\n for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { server.close(); server.closeAllConnections(); });\n} catch { console.error('CMS connector requires valid private server configuration.'); process.exitCode = 1; }\n` },
23
+ { path: '.universalcms/CmsPage.jsx', content: `import React, { useEffect, useState } from 'react';\nimport { CmsDocument } from '@universalcms/react';\nimport '@universalcms/react/styles.css';\n\n// Mount in your existing router; this module contains no CMS credentials.\nexport default function CmsPage({ slug = 'home' }) {\n const [result, setResult] = useState({ slug: '', state: 'loading', entry: null });\n useEffect(() => {\n const controller = new AbortController();\n let current = true;\n setResult({ slug, state: 'loading', entry: null });\n async function load() {\n try {\n const response = await fetch('/api/universalcms/page?slug=' + encodeURIComponent(slug), { credentials: 'omit', cache: 'no-store', redirect: 'error', signal: controller.signal });\n if (response.status === 404) { if (current) setResult({ slug, state: 'missing', entry: null }); return; }\n if (!response.ok || !response.headers.get('content-type')?.startsWith('application/json')) throw Error();\n const body = await response.json();\n if (!body.entry?.document || typeof body.entry.document.title !== 'string' || !Array.isArray(body.entry.document.blocks)) throw Error();\n if (current) setResult({ slug, state: 'ready', entry: body.entry });\n } catch { if (current) setResult({ slug, state: 'error', entry: null }); }\n }\n void load();\n return () => { current = false; controller.abort(); };\n }, [slug]);\n if (result.slug !== slug || result.state === 'loading') return <p role="status">Loading content…</p>;\n if (result.state === 'missing') return <h1>Page not found</h1>;\n if (result.state === 'error') return <p role="alert">Content is temporarily unavailable.</p>;\n return <article><h1>{result.entry.document.title}</h1><CmsDocument document={result.entry.document} entryId={result.entry.id} /></article>;\n}\n` },
24
+ { path: '.universalcms/vite.config.mjs', content: sourceAwareViteWrapper(`import { defineConfig, loadConfigFromFile } from 'vite';\n\n// Explicit wrapper: preserves the project's config without editing it.\n// Run only when you choose: npx vite --config .universalcms/vite.config.mjs\nexport default defineConfig(async env => {\n const existing = (await loadConfigFromFile(env, undefined, process.cwd()))?.config ?? {};\n const proxies = existing.server?.proxy ?? {};\n const endpoint = '^/api/universalcms/page(?:[?]|$)';\n if (Object.hasOwn(proxies, '/api/universalcms/page') || Object.hasOwn(proxies, endpoint)) throw new Error('The CMS proxy endpoint already exists. Review it manually before using this wrapper.');\n // Vite selects the first matching proxy. The exact CMS endpoint must precede\n // an existing broad /api proxy; all unrelated routes retain their destinations.\n return { ...existing, server: { ...existing.server, proxy: { [endpoint]: { target: 'http://127.0.0.1:4318', changeOrigin: false }, ...proxies } } };\n});\n`) },
25
+ { path: 'universalcms-demo.html', content: '<!doctype html>\n<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CMS connection demo</title><meta name="robots" content="noindex,nofollow"></head><body><div id="universalcms-demo"></div><script type="module" src="/.universalcms/demo.jsx"></script></body></html>\n' },
26
+ { path: '.universalcms/demo.jsx', content: `import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport CmsPage from './CmsPage.jsx';\nconst slug = new URLSearchParams(window.location.search).get('slug') || 'home';\ncreateRoot(document.getElementById('universalcms-demo')).render(<CmsPage slug={slug} />);\n` },
27
+ ];
28
+ }
29
+
30
+ // The optional demo wrapper must retain original-route publication protection
31
+ // after full setup. Switching to this documented wrapper cannot bypass the gate.
32
+ const sourceImport = "import { readFile } from 'node:fs/promises';\n";
33
+ const legacySourceStartup = ` let sourcePlugins = [];
34
+ try {
35
+ const configuration = JSON.parse(await readFile(new URL('./source.config.json', import.meta.url), 'utf8'));
36
+ const { universalCmsSource } = await import('@universalcms/connect/vite-source');
37
+ sourcePlugins = [universalCmsSource({ configuration })];
38
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
39
+ `;
40
+ const sourceStartup = ` const sourcePlugins = [];
41
+ let publicationText;
42
+ try { publicationText = await readFile(new URL('./publication.config.json', import.meta.url), 'utf8'); }
43
+ catch (error) {
44
+ if (error.code !== 'ENOENT') throw error;
45
+ for (const filename of ['./source.vite.config.mjs', './serve.mjs']) {
46
+ try {
47
+ const source = await readFile(new URL(filename, import.meta.url), 'utf8');
48
+ if (/@universalcms\\/connect\\/vite-publication|universalcms-publication-client/.test(source)) throw new Error('Original-route publication configuration is missing. Restore the reviewed setup before starting the website.');
49
+ } catch (inspectionError) { if (inspectionError.code !== 'ENOENT') throw inspectionError; }
50
+ }
51
+ }
52
+ if (publicationText !== undefined) {
53
+ const configuration = JSON.parse(publicationText);
54
+ const sourceConfiguration = JSON.parse(await readFile(new URL('./source.config.json', import.meta.url), 'utf8'));
55
+ let contentBindings;
56
+ try { contentBindings = JSON.parse(await readFile(new URL('./content.bindings.json', import.meta.url), 'utf8')); }
57
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
58
+ const { universalCmsPublication } = await import('@universalcms/connect/vite-publication');
59
+ sourcePlugins.push(universalCmsPublication({ configuration, sourceConfiguration, contentBindings }));
60
+ }
61
+ try {
62
+ const configuration = JSON.parse(await readFile(new URL('./source.config.json', import.meta.url), 'utf8'));
63
+ const { universalCmsSource } = await import('@universalcms/connect/vite-source');
64
+ sourcePlugins.push(universalCmsSource({ configuration }));
65
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
66
+ `;
67
+ function sourceAwareViteWrapper(content) {
68
+ return sourceImport + content.replace(' const proxies =', sourceStartup + ' const proxies =')
69
+ .replace('return { ...existing, server:', 'return { ...existing, plugins: [...sourcePlugins, ...(existing.plugins ?? [])], server:');
70
+ }
71
+ export function previousViteSourceWrapper(content) {
72
+ return content.replace(sourceImport, '').replace(sourceStartup, '')
73
+ .replace('return { ...existing, plugins: [...sourcePlugins, ...(existing.plugins ?? [])], server:', 'return { ...existing, server:');
74
+ }
75
+ export function previousPublicationViteWrapper(content) {
76
+ return content.replace(sourceStartup, legacySourceStartup);
77
+ }
78
+ export function previousDiagnosticViteWrapper(content) {
79
+ return content.replace(" const sourceConfiguration = JSON.parse(await readFile(new URL('./source.config.json', import.meta.url), 'utf8'));\n let contentBindings;\n try { contentBindings = JSON.parse(await readFile(new URL('./content.bindings.json', import.meta.url), 'utf8')); }\n catch (error) { if (error.code !== 'ENOENT') throw error; }\n", '')
80
+ .replace('universalCmsPublication({ configuration, sourceConfiguration, contentBindings })', 'universalCmsPublication({ configuration })');
81
+ }
82
+
83
+ // Exact prior prerelease guide variant, used only to recognize an untouched
84
+ // generated document during guided upgrades. Custom bytes still fail closed.
85
+ const priorSetupInstruction = 'Run npx --no-install universalcms-connect setup to enter and save connection values locally.';
86
+ const stagedSetupInstruction = 'After connect and setup, run npx --no-install universalcms-connect server-key to save the delivery key through hidden input. If this project has no saved connect record, setup retains its all-in-one guided flow.';
87
+ const priorPrivateInstruction = 'Guided setup saves the entered values in private .env.local.';
88
+ const stagedPrivateInstruction = 'The server-key command (or the legacy all-in-one setup flow) saves entered values in private .env.local.';
89
+ const legacyPublicationGuide = `## Connect publication to the original website URLs
90
+
91
+ Source export and importing drafts do not hide the original website content.
92
+ The scaffold does not connect existing routes to CMS publication automatically.
93
+ Review the imported public route inventory and map each original URL to its exact
94
+ CMS kind, slug and locale in the fixed site/environment. Map the home URL explicitly
95
+ (for example, / to the imported home slug); do not guess from titles or route names.
96
+ Exclude private/account, API, admin, preview and framework paths from this mapping.
97
+
98
+ For every reviewed managed public route, check published delivery before rendering
99
+ any original page content. A missing or unpublished page must return 404; a failed
100
+ delivery request must show an unavailable response without original or stale
101
+ content. When all managed pages are unpublished, all their original URLs must be
102
+ unavailable, including the home page. Never use draft content as a fallback.
103
+ Publication checks are independent of body/design mapping: preserve existing
104
+ components and interactions behind the check while mapping CMS blocks deliberately.
105
+ A source-only setup still needs this integration before publication governs pages.
106
+
107
+ Use fixtures or a disposable staging site to verify published, all-unpublished and
108
+ publish-to-unpublish transitions on the original URLs, including direct requests,
109
+ client navigation, reload and cached/prefetched routes. Test upstream failures too.
110
+ Do not publish or unpublish customer content merely to test. For static hosting,
111
+ verify the required rebuild, cache invalidation and deployment before claiming the
112
+ URLs changed. A successful API check or demo route does not verify original pages.
113
+ Report unmanaged public routes and unfinished deployments explicitly. Existing
114
+ public source exports and assets are separate from page-rendering checks.
115
+
116
+ `;
117
+ const availabilityGuide = `## Connect publication to the original website URLs
118
+
119
+ Source export and importing drafts do not hide the original website content.
120
+ The full universalcms-connect setup command separately installs publication checks
121
+ for its reviewed original URL mappings. Confirm the printed kind, slug and locale,
122
+ including / for the home page, before saving. Custom mappings belong in
123
+ .universalcms/publication.routes.json; rerun setup after adding public routes.
124
+ Exclude private/account, API, admin, preview and framework paths from this mapping.
125
+
126
+ After full setup, use your normal dev and preview commands. For a supported Vite
127
+ website, run your normal build command and then npm start (or your package-manager
128
+ equivalent) to serve the build with the generated private Node publication server.
129
+ Deploy that server with its private environment. A static-only host cannot enforce
130
+ the request-time publication checks. Next.js keeps its ordinary dev/build/start
131
+ commands and uses the generated server proxy before HTML and RSC responses.
132
+
133
+ The preceding /cms or universalcms-demo instructions apply to standalone init or
134
+ source-only installations. After full setup, verify the reviewed original URLs;
135
+ unmapped demo page URLs return 404. Their standalone page API is not the original
136
+ website's production host. The optional Vite wrapper retains installed original-
137
+ route publication checks and refuses to start when their configuration is missing.
138
+
139
+ A missing or unpublished page returns 404; failed delivery returns 503 without
140
+ original or stale content. When all managed pages are unpublished, all their original URLs must be
141
+ unavailable, including the home page. Never use draft content as a fallback.
142
+ Publication checks are independent of body/design mapping: published pages retain
143
+ their native components. CMS document edits need deliberate component mapping.
144
+ Source-only setup and unsupported custom servers still require manual integration.
145
+
146
+ Use fixtures or a disposable staging site to verify published, all-unpublished and
147
+ publish-to-unpublish transitions on the original URLs, including direct requests,
148
+ client navigation, reload and cached/prefetched routes. Test upstream failures too.
149
+ Do not publish or unpublish customer content merely to test. A successful API check
150
+ or demo route does not verify original pages. Report unmanaged public routes and
151
+ unfinished deployments explicitly. Existing public source exports and assets are
152
+ separate from page-rendering checks.
153
+
154
+ `;
155
+ const publicationGuide = availabilityGuide.replace(
156
+ 'Publication checks are independent of body/design mapping: published pages retain\ntheir native components. CMS document edits need deliberate component mapping.\nSource-only setup and unsupported custom servers still require manual integration.',
157
+ `Full setup also binds supported native JSX/TSX components to the current published
158
+ document. Existing block IDs and source baselines stay frozen across reconnects and
159
+ builds. Edited and deleted blocks update in place; new blocks appear after the
160
+ bound native content. Source-only setup and unsupported custom components require
161
+ an explicit integration through @universalcms/connect/react-publication.
162
+
163
+ Vite routes need reviewed path/sourceFile pairs in .universalcms/content.routes.json
164
+ unless a single homepage maps unambiguously to src/App.jsx or src/App.tsx. The
165
+ entire source block structure must match before imported IDs can be reused.
166
+ Native layout positions and interactions remain in their components. Arbitrary
167
+ layout reordering and private/runtime application data are not inferred.
168
+
169
+ Save draft leaves the published revision unchanged. Update live / Publish changes
170
+ public content. Open tabs check every 15 seconds and on focus; a published revision
171
+ refreshes bound content without resetting native state. Missing or failed reads
172
+ remove stale managed content. Test published A, saved draft B, then published B on
173
+ the original URL, including removed blocks and a published empty document.
174
+
175
+ Existing installations must upgrade this package, rerun setup and deploy the
176
+ updated website code and private runtime settings to each live host. A local
177
+ setup does not deploy remote websites. Verify actual changed page text separately
178
+ on local and live origins; an API or source-import check cannot prove rendering.`
179
+ );
180
+ export function previousRenderingSetupGuide(content) {
181
+ return content.replace(publicationGuide, availabilityGuide);
182
+ }
183
+ function stagedSetupGuide(content) {
184
+ return content.replace(priorSetupInstruction, stagedSetupInstruction).replace(priorPrivateInstruction, stagedPrivateInstruction)
185
+ .replace('## What remains deliberate', publicationGuide + '## What remains deliberate');
186
+ }
187
+ export function priorPublicationSetupGuide(content) {
188
+ return content.replace(publicationGuide, '');
189
+ }
190
+ export function previousPublicationSetupGuide(content) {
191
+ return content.replace(publicationGuide, legacyPublicationGuide);
192
+ }
193
+ export function previousSetupGuide(content) {
194
+ return priorPublicationSetupGuide(content).replace(stagedSetupInstruction, priorSetupInstruction).replace(stagedPrivateInstruction, priorPrivateInstruction);
195
+ }
196
+ export function legacySetupGuide(content) {
197
+ return previousSetupGuide(content)
198
+ .replace('Guided setup saves the entered values in private .env.local. The analyze, init and check commands do not load that file automatically.', 'The CLI neither saves nor reads an .env file.')
199
+ .replace('node --env-file=.env.local .universalcms/server.mjs', 'node .universalcms/server.mjs')
200
+ .replace('Run npx --no-install universalcms-connect setup to enter and save connection values locally. For hosted deployment, copy the nonsecret values in server.env.example into the private server/hosting', 'Copy the nonsecret values in server.env.example into the private server/hosting')
201
+ .replace('Init preserves existing project files. Guided setup can update its unchanged generated files and the entered connection values in .env.local; unrelated settings and custom edits are preserved.', 'Existing project configuration and source are never overwritten.');
202
+ }
203
+
204
+ function instructions(config, existingNodeServer) {
205
+ const next = config.framework === 'next';
206
+ return `# Connect this website to Universal CMS\n\nThe scaffold is created, but connection and rendering are not yet verified. No\nexisting source was replaced and no content was imported or published.\n\n## Install the runtime packages\n\nAfter the platform operator publishes the 0.1.0 packages, run:\n\n\`\`\`sh\nnpm install @universalcms/sdk@0.1.0 @universalcms/react@0.1.0${next ? ' @universalcms/next@0.1.0' : ''}\n\`\`\`\n\nFor a prerelease, install the operator's SDK and React${next ? ' and Next' : ''} tarballs together instead.\nDo not run the registry command until those reviewed packages are available.\nUse your project's package manager and commit its reviewed lockfile.\n\n## Configure server secrets and verify\n\nRun npx --no-install universalcms-connect setup to enter and save connection values locally. For hosted deployment, copy the nonsecret values in server.env.example into the private server/hosting\nenvironment. Obtain an environment-scoped server delivery key from this site's\nDeveloper setup and set CMS_API_KEY using your secret manager. Never use a\nNEXT_PUBLIC_ or VITE_ variable. Guided setup saves the entered values in private .env.local. The analyze, init and check commands do not load that file automatically.\nIf using a local env file, keep it ignored by Git and outside public directories.\nThe site/environment in generated server code are fixed; create separate reviewed\nconfiguration for another environment. CMS_API_BASE_URL may override the API\norigin only in the private server environment.\n\nRun \`npx --no-install universalcms-connect check --slug YOUR_PUBLISHED_PAGE\` from this project.\nIt uses CMS_API_KEY from the process environment or a hidden terminal prompt.\nIt honors the same private CMS_API_BASE_URL override as the generated server.\nRun it with the same private environment used by that server.\nA successful check verifies one published API response, not deployment or rendering.\n\n${next ? `## Next.js\n\nStart your existing Next.js app. Visit \`${config.mount}/YOUR_PUBLISHED_PAGE\`. Nested\nslugs retain their slash separators. The generated page uses request-time delivery\nand no persistent cache; missing/unpublished pages return 404, upstream failures\nshow a retry state. This route is an additive connection demo under ${config.mount};\nit does not replace your existing routes or imply the CMS's canonical paths changed.\nProduction requires a Node-capable Next deployment and private server secrets.\nStatic export and Edge runtimes are unsupported by this scaffold.\n` : `## React/Vite\n\n${existingNodeServer ? existingServerInstructions(config) : ""}Set CMS_WEBSITE_ORIGIN to the exact browser-facing origin (localhost:5173 during\ndevelopment). Start the private server in one terminal:\n\n\`\`\`sh\nnode --env-file=.env.local .universalcms/server.mjs\n\`\`\`\n\nStart Vite with the supplied wrapper in another terminal:\n\n\`\`\`sh\nnpx vite --config .universalcms/vite.config.mjs\n\`\`\`\n\nThe wrapper merges your existing Vite configuration and proxies only\n/api/universalcms/page to port 4318. If changing the connector port, update the\nreviewed wrapper target too. Visit\n/universalcms-demo.html?slug=YOUR_PUBLISHED_PAGE to verify rendering. The wrapper\nloads your normal Vite config only when you explicitly run Vite, never during\nanalysis/setup. No private key is passed to Vite or included in the demo.\n\nImport .universalcms/CmsPage.jsx into your existing React router and give it the\npage slug, or map the returned public document to your own components. The demo\nHTML is a development entry and is not automatically included in Vite's production\nbuild. Production needs both your normal Vite static build AND this Node server\nor an equivalent serverless handler. The host/reverse proxy must route the exact\n/api/universalcms/page path to the private connector and other paths to your static\nsite; Vite dev proxy settings do not configure a production proxy. Keep the server\non a private network, set the exact HTTPS CMS_WEBSITE_ORIGIN, apply ingress request\nlimits, and do not expose a general-purpose CMS proxy or server environment.\nThe handler provides no CORS, bounds input/concurrency, and forwards only a fixed\nsite/environment/locale published page lookup. It returns 404 for a missing page\nand 503 for upstream failure; the client clears stale content. It does not provide\nSSR or production HTTP 404 status for a client-only router.\n`}\n## What remains deliberate\n\nThis first scaffold renders published pages in one configured locale. Posts,\ncustom collections, taxonomy archive routes, navigation, forms, previews, SEO\nmetadata and existing design/content mapping use the SDK's documented APIs and\nneed explicit integration. Form blocks are displayed disabled until a reviewed\nsubmission endpoint is supplied. No existing source is sent to the platform.\n\nRun your website typecheck/build and review rendered pages before deployment.\nA backend connection does not mean existing content has been imported. Use the\nCMS's authorized import-as-drafts flow for that separate step. Nothing here\npublishes content or installs the Universal CMS backend on the website.\n\n## Optional assistance\n\n\`npx --no-install universalcms-connect analyze\` prints a sanitized local report. You may choose\nto supply that report in the CMS's setup assistant. This command never uploads\nsource or calls a model. Platform AI provider keys, limits and metering remain on\nthe platform; the deterministic/manual instructions above work without AI.\n\n## Reruns and removal\n\nThe same init command preserves identical files. Differing files stop setup before\nwriting; inspect and merge your edits manually. Remove only the generated files\nlisted by init to uninstall this scaffold, then review any router/proxy changes\nyou made. Init preserves existing project files. Guided setup can update its unchanged generated files and the entered connection values in .env.local; unrelated settings and custom edits are preserved.\n`;
207
+ }
208
+
209
+ function existingServerInstructions(config) {
210
+ return `### Existing Node server detected
211
+
212
+ Keep your current website startup (for example, npm run dev), SSR bootstrap and
213
+ production server. Starting raw Vite replaces that server and can break an SSR
214
+ website. The separate server/Vite commands below are optional connection-demo
215
+ diagnostics on unused ports, not a replacement for your website startup.
216
+
217
+ For the real website, register the published-page handler inside its existing
218
+ private server before a generic /api handler or SSR catch-all. Reuse that server's
219
+ fixed site/environment SDK client and deploy @universalcms/connect as a production
220
+ dependency when importing its Node entry point. For this prerelease, install the
221
+ reviewed connector tarball with npm install --save-prod (not --save-dev).
222
+
223
+ \`\`\`js
224
+ import { createPageHandler } from '@universalcms/connect/node';
225
+ // cms is the existing private, fixed-scope SDK client, never a browser client.
226
+ const cmsPage = createPageHandler(cms, {
227
+ locale: ${JSON.stringify(config.locale)},
228
+ websiteOrigin: process.env.CMS_WEBSITE_ORIGIN,
229
+ });
230
+ // In the existing request listener, BEFORE its /api or SSR fallback:
231
+ if (req.url?.split('?')[0] === '/api/universalcms/page') {
232
+ await cmsPage(req, res);
233
+ return;
234
+ }
235
+ \`\`\`
236
+
237
+ Mount the supplied CmsPage component explicitly through your router, or keep your
238
+ existing CMS renderer. The CLI does not replace an existing integration. Confirm
239
+ both direct page loads and client navigation using the normal website server.
240
+ The standalone demo verifies rendering separately; it does not prove SSR,
241
+ existing routes, navigation, or production routing work.
242
+
243
+ If using the optional raw-Vite demo, choose an unused port explicitly and set
244
+ CMS_WEBSITE_ORIGIN to that exact origin. Do not visit the SSR website root through
245
+ that diagnostic server; use /universalcms-demo.html?slug=YOUR_PUBLISHED_PAGE.
246
+
247
+ `;
248
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /** Local read-only inspection; neither installs dependencies nor runs project code. */
3
+ import { constants } from 'node:fs';
4
+ import { lstat, realpath, open, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { analyzeProjectFiles, PROJECT_ANALYSIS_FILES, MAX_ANALYSIS_FILE_BYTES, MAX_ANALYSIS_TOTAL_BYTES } from './project-analysis.mjs';
8
+ import { inspectInstalledDependencies } from './inspect-installed-dependencies.mjs';
9
+
10
+ function inside(root, target) { const relative = path.relative(root, target); return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); }
11
+
12
+ /** A missing file is fine; unreadable, symlinked or excessive evidence is never silently trusted. */
13
+ export async function analyzeProjectDirectory(directory) {
14
+ const requested = path.resolve(directory);
15
+ const rootStat = await lstat(requested);
16
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw Error('Select an ordinary project directory, not a symbolic link.');
17
+ const root = await realpath(requested), files = Object.create(null);
18
+ let total = 0;
19
+ for (const name of PROJECT_ANALYSIS_FILES) {
20
+ const segments = name.split('/');
21
+ let target = root, missing = false, unsafe = false;
22
+ for (let index = 0; index < segments.length; index++) {
23
+ target = path.join(target, segments[index]);
24
+ let info;
25
+ try { info = await lstat(target); }
26
+ catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { missing = true; break; } unsafe = true; break; }
27
+ if (info.isSymbolicLink() || (index < segments.length - 1 && !info.isDirectory()) || (index === segments.length - 1 && !info.isFile())) { unsafe = true; break; }
28
+ if (index === segments.length - 1 && (info.size > MAX_ANALYSIS_FILE_BYTES || total + info.size > MAX_ANALYSIS_TOTAL_BYTES)) { unsafe = true; break; }
29
+ }
30
+ if (missing) continue;
31
+ if (unsafe) { files[name] = null; continue; }
32
+ let handle;
33
+ try {
34
+ const actual = await realpath(target);
35
+ if (!inside(root, actual) || actual !== target) { files[name] = null; continue; }
36
+ handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
37
+ const opened = await handle.stat(), current = await lstat(target);
38
+ if (!opened.isFile() || opened.ino !== current.ino || opened.dev !== current.dev || current.isSymbolicLink() || opened.size > MAX_ANALYSIS_FILE_BYTES || total + opened.size > MAX_ANALYSIS_TOTAL_BYTES || await realpath(target) !== target) { files[name] = null; continue; }
39
+ // Read at most the budget plus one, even if the file grows after stat.
40
+ const buffer = Buffer.alloc(MAX_ANALYSIS_FILE_BYTES + 1);
41
+ let count = 0;
42
+ while (count < buffer.length) {
43
+ const { bytesRead } = await handle.read(buffer, count, buffer.length - count, count);
44
+ if (!bytesRead) break;
45
+ count += bytesRead;
46
+ }
47
+ total += count;
48
+ if (count > MAX_ANALYSIS_FILE_BYTES || total > MAX_ANALYSIS_TOTAL_BYTES) files[name] = null;
49
+ else files[name] = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, count));
50
+ } catch { files[name] = null; }
51
+ finally { await handle?.close(); }
52
+ }
53
+ const core = analyzeProjectFiles(files);
54
+ const diagnostics = await inspectInstalledDependencies(root, files['package.json']);
55
+ return { ...core, version: 2, diagnostics };
56
+ }
57
+
58
+ async function main(args) {
59
+ if (args.includes('--help') || args.includes('-h')) {
60
+ process.stdout.write('Usage: node analyze-project.mjs [project-directory] [--output report.json]\nReads allowlisted project files and exact installed package metadata locally. Does not execute code, install packages, read .env, contact websites, or upload source. Reports contain timestamped, sanitized setup evidence and dependency diagnostics, without package versions. Existing output files are never overwritten.\n');
61
+ return;
62
+ }
63
+ let directory = null, output = null;
64
+ for (let i = 0; i < args.length; i++) {
65
+ const arg = args[i];
66
+ if (arg === '--output' && !output && args[i + 1] && !args[i + 1].startsWith('-')) output = args[++i];
67
+ else if (!arg.startsWith('-') && directory === null) directory = arg;
68
+ else throw Error('Use one project directory and an optional --output filename.');
69
+ }
70
+ const report = await analyzeProjectDirectory(directory ?? process.cwd());
71
+ const result = `${JSON.stringify(report, null, 2)}\n`;
72
+ if (output) await writeFile(path.resolve(output), result, { flag: 'wx', mode: 0o600 });
73
+ else process.stdout.write(result);
74
+ }
75
+
76
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
77
+ main(process.argv.slice(2)).catch(() => {
78
+ // Filesystem and parse errors can contain paths or source. Never echo them.
79
+ process.stderr.write('Project analysis could not finish. Check the directory, file permissions and output filename. Existing output files are not overwritten.\n');
80
+ process.exitCode = 1;
81
+ });
82
+ }
@@ -0,0 +1,53 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 [Astro contributors](https://github.com/withastro/compiler/graphs/contributors)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ """
24
+ This license applies to parts of the `internal/` subdirectory originating from
25
+ the https://cs.opensource.google/go/x/net/+/master:html/ repository:
26
+
27
+ Copyright (c) 2009 The Go Authors. All rights reserved.
28
+
29
+ Redistribution and use in source and binary forms, with or without
30
+ modification, are permitted provided that the following conditions are
31
+ met:
32
+
33
+ * Redistributions of source code must retain the above copyright
34
+ notice, this list of conditions and the following disclaimer.
35
+ * Redistributions in binary form must reproduce the above
36
+ copyright notice, this list of conditions and the following disclaimer
37
+ in the documentation and/or other materials provided with the
38
+ distribution.
39
+ * Neither the name of Google Inc. nor the names of its
40
+ contributors may be used to endorse or promote products derived from
41
+ this software without specific prior written permission.
42
+
43
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
44
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
45
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
46
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
47
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
48
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
49
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
50
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
51
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
52
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
53
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.