@universalcms/connect 0.1.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +694 -37
  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 +418 -15
  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 +32 -51
  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 +70 -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 +167 -3
  87. package/dist/vendor/astro-compiler/LICENSE +53 -0
  88. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  89. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  90. package/dist/vendor/astro-compiler/node/index.js +1 -0
  91. package/dist/vendor/project-analysis.mjs +56 -4
  92. package/dist/vendor/react-LICENSE.txt +21 -0
  93. package/dist/vendor/react-document.mjs +124 -0
  94. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  95. package/dist/vendor/react-form.mjs +63 -0
  96. package/dist/vendor/react-safety.mjs +47 -0
  97. package/dist/vendor/source-manifest-provider.mjs +104 -0
  98. package/dist/vendor/typescript-LICENSE.txt +55 -0
  99. package/dist/vendor/typescript.cjs +200276 -0
  100. package/dist/vite-publication-server.d.mts +3 -0
  101. package/dist/vite-publication-server.mjs +82 -0
  102. package/dist/vite-publication.d.mts +15 -0
  103. package/dist/vite-publication.mjs +241 -0
  104. package/dist/vite-source-discovery.mjs +315 -0
  105. package/dist/vite-source.d.mts +30 -0
  106. package/dist/vite-source.mjs +141 -0
  107. package/dist/vue-runtime.d.mts +10 -0
  108. package/dist/vue-runtime.mjs +50 -0
  109. package/dist/vue-server.d.mts +2 -0
  110. package/dist/vue-server.mjs +58 -0
  111. package/dist/vue-setup.mjs +127 -0
  112. package/dist/vue-source.mjs +168 -0
  113. package/dist/website-check.mjs +41 -0
  114. package/dist/website-http.mjs +149 -0
  115. package/dist/website-model.d.mts +20 -0
  116. package/dist/website-model.mjs +155 -0
  117. package/dist/website-next.d.mts +2 -0
  118. package/dist/website-next.mjs +29 -0
  119. package/dist/website-node.d.mts +3 -0
  120. package/dist/website-node.mjs +50 -0
  121. package/dist/website-runtime.d.mts +13 -0
  122. package/dist/website-runtime.mjs +396 -0
  123. package/dist/website-schema.mjs +22175 -0
  124. package/dist/website-setup.mjs +256 -0
  125. package/dist/website.css +49 -0
  126. package/dist/wizard.mjs +237 -38
  127. package/package.json +43 -7
@@ -0,0 +1,256 @@
1
+ import { planAstroSettings, inspectAstroProject } from './astro-setup.mjs';
2
+ import { isNativeFramework, nativeFramework, inspectNativeProject, planNativeSettings } from './native-adapters.mjs';
3
+ /** Explicit reviewed extension to full setup. Never overwrite a custom website adapter. */
4
+ import path from 'node:path';
5
+ import { lstat, readdir } from 'node:fs/promises';
6
+ import { projectRoot, readProjectFile, SetupError, applyPlan } from './core.mjs';
7
+ import { readContentRegistry, readFrozenContent, contentDigest, CONTENT_BINDINGS_FILE } from './content-registry.mjs';
8
+ import { readPublicationConfiguration, viteServerTemplate } from './publication-setup.mjs';
9
+ import { nextContentPageTemplate, inspectOwnedDemo } from './content-next-setup.mjs';
10
+ import { REVIEWED_NEXT_NATIVE_PROFILE } from './native-routes.mjs';
11
+ import { inspectReviewedNativeOwnership } from './reviewed-native-ownership.mjs';
12
+ import { planReviewedCommerceSettings, reviewPlannedCommerceSettings } from './reviewed-commerce-settings-setup.mjs';
13
+ import { viteContentPageTemplate } from './content-vite-setup.mjs';
14
+ import { nextPublicationProxyTemplate, nextWebsiteProxyTemplate, planNextPublication } from './publication-next-setup.mjs';
15
+ import { viteSourceWrapper, readSourceSettings } from './source-setup.mjs';
16
+ import { inspectSourceRoutes } from './source-routes.mjs';
17
+ import { isExcludedPublicationPath } from './publication.mjs';
18
+ import { setupReviewError } from './preflight-recovery.mjs';
19
+ import { inspectSettingsLayoutReview, planNextSettingsLayout, SETTINGS_LAYOUT_FILE } from './next-settings-layout.mjs';
20
+ const FILE='.universalcms/website.integration.json';
21
+ const json=value=>JSON.stringify(value,null,2)+'\n';
22
+ const fail=message=>{throw new SetupError(message);};
23
+ const relative=(from,to)=>{const value=path.posix.relative(path.posix.dirname(from),to);return value.startsWith('.')?value:`./${value}`;};
24
+ const sections=['branding','social','seo','languages','analytics','navigation','experience','scripts','redirects','authors','templates','taxonomies'];
25
+ function vitePage(row,config,old) {
26
+ const scope=`${config.siteId}:${config.environmentId}`;
27
+ const setup=`import { CmsWebsite, CmsWebsitePage } from '@universalcms/connect/react-website';\nimport '@universalcms/connect/website.css';\n`;
28
+ return setup+old.replace('return createElement(PublishedDocumentProvider,',`const current = window.location.pathname === '/index.html' ? '/' : window.location.pathname;\n if (current !== ${JSON.stringify(row.path)}) return createElement(CmsWebsitePage, { pathname: current, scope: ${JSON.stringify(scope)} });\n return createElement(CmsWebsite, { scope: ${JSON.stringify(scope)}, children: createElement(PublishedDocumentProvider,`).replace('children: createElement(NativePage, props) });','children: createElement(NativePage, props) }) });');
29
+ }
30
+ function nextPage(row, config, ownership, layout = false) {
31
+ const typed = row.sourceFile.endsWith('.tsx');
32
+ const pathname = JSON.stringify(row.path);
33
+ const native = relative(row.sourceFile, row.nativeFile).replace(/\.(?:[jt]sx?)$/, '');
34
+ return `// Generated by @universalcms/connect. Original content and layout stay bound to their fixed route.
35
+ import { cache } from 'react';
36
+ import { connection } from 'next/server';
37
+ import { notFound } from 'next/navigation';
38
+ import { CmsWebsite } from '@universalcms/connect/react-website';
39
+ import { PublishedDocumentProvider } from '@universalcms/connect/react-publication';
40
+ import { publicBindingInventory } from '@universalcms/connect/public-bindings';
41
+ import { createWebsiteRuntime, websiteMetadata } from '@universalcms/connect/website-runtime';
42
+ import '@universalcms/connect/website.css';
43
+ import configuration from ${JSON.stringify(relative(row.sourceFile,'.universalcms/publication.config.json'))};
44
+ import inventory from ${JSON.stringify(relative(row.sourceFile,row.inventoryFile))};
45
+ import NativePage from ${JSON.stringify(native)};
46
+ const website = createWebsiteRuntime({ configuration: configuration${typed ? " as Parameters<typeof createWebsiteRuntime>[0]['configuration']" : ''}${ownership ? ', nativePaths: ' + JSON.stringify(ownership.nativePaths) + ', nativePatterns: ' + JSON.stringify(ownership.nativePatterns) : ''} });
47
+ const settings = cache(async () => { await connection(); return website.settings(); });
48
+ const load = cache(async () => {
49
+ await connection();
50
+ const result = await website.page(${pathname});
51
+ if (result.state === 'missing') notFound();
52
+ if (result.state !== 'published') throw new Error('Published website content unavailable');
53
+ return result.entry;
54
+ });
55
+ export async function generateMetadata() {
56
+ const entry = await load(), snapshot = await settings();
57
+ if (snapshot.sections.seo.state !== 'ready') return { title: entry.document.title, description: entry.document.excerpt };
58
+ const value = websiteMetadata(entry, snapshot, ${pathname}, configuration.websiteOrigin);
59
+ return { other: { 'universalcms-seo': 'managed' }, title: value.title, description: value.description, robots: value.robots, alternates: { canonical: value.canonical, languages: Object.fromEntries((value.alternates ?? []).map(item => [item.locale, item.url])) }, openGraph: value.openGraph ? { ...value.openGraph, images: value.openGraph.image ? [value.openGraph.image] : [] } : undefined, twitter: value.twitter };
60
+ }
61
+ export default async function Page() {
62
+ const entry = await load(), snapshot = await settings();
63
+ const structured = websiteMetadata(entry, snapshot, ${pathname}, configuration.websiteOrigin).jsonLd;
64
+ const publicInventory = publicBindingInventory(inventory${typed ? " as import('@universalcms/connect/react-publication').Inventory" : ''});
65
+ return <>{structured && <script type="application/ld+json" data-universalcms-setting="seo" dangerouslySetInnerHTML={{ __html: JSON.stringify(structured).replace(/</g, '\\\\u003c') }}/>}<CmsWebsite scope=${JSON.stringify(config.siteId+':'+config.environmentId)} initialSettings={snapshot} entry={entry}${layout ? ' chrome={false}' : ''}><PublishedDocumentProvider initialEntry={entry} pathname=${pathname} route={publicInventory}><NativePage /></PublishedDocumentProvider></CmsWebsite></>;
66
+ }
67
+ `;
68
+ }
69
+ function catchall(app, config, native, layout = false) {
70
+ const filename = `${app}/[...cmsPath]/page.tsx`, importPath = relative(filename,'.universalcms/publication.config.json');
71
+ return `// Generated website routes: originals and reserved paths retain their ownership.
72
+ import { cache } from 'react';
73
+ import { connection } from 'next/server';
74
+ import { notFound } from 'next/navigation';
75
+ import { createWebsiteRuntime, websiteMetadata } from '@universalcms/connect/website-runtime';
76
+ import { CmsWebsitePage } from '@universalcms/connect/react-website';
77
+ import '@universalcms/connect/website.css';
78
+ import configuration from ${JSON.stringify(importPath)};
79
+ const runtime = createWebsiteRuntime({ configuration: configuration as Parameters<typeof createWebsiteRuntime>[0]['configuration']${native ? ', nativePaths: ' + JSON.stringify(native.nativePaths) + ', nativePatterns: ' + JSON.stringify(native.nativePatterns) : ''} });
80
+ const settings = cache(async () => { await connection(); return runtime.settings(); });
81
+ const load = cache(async (pathname: string) => {
82
+ await connection();
83
+ const result = await runtime.resolve(pathname);
84
+ if (result.state === 'missing' || result.state === 'unmanaged') notFound();
85
+ if (result.state !== 'published' && result.state !== 'archive') throw new Error('Website unavailable');
86
+ return result;
87
+ });
88
+ type Props = { params: Promise<{ cmsPath: string[] }> };
89
+ export async function generateMetadata({params}: Props) {
90
+ const pathname = '/' + (await params).cmsPath.join('/'), result = await load(pathname), snapshot = await settings();
91
+ const entry = result.state === 'published' ? result.entry : null;
92
+ if (snapshot.sections.seo.state !== 'ready') return { title: entry?.document.title ?? (result.state === 'archive' ? result.title : ''), description: entry?.document.excerpt };
93
+ const value = websiteMetadata(entry, snapshot, pathname, configuration.websiteOrigin);
94
+ return { other: { 'universalcms-seo': 'managed' }, title: result.state === 'archive' ? result.title : value.title, description: value.description, robots: value.robots, alternates: { canonical: value.canonical, languages: Object.fromEntries((value.alternates ?? []).map(item => [item.locale, item.url])) }, openGraph: value.openGraph ? { ...value.openGraph, images: value.openGraph.image ? [value.openGraph.image] : [] } : undefined, twitter: value.twitter };
95
+ }
96
+ export default async function Page({params}: Props) {
97
+ const pathname = '/' + (await params).cmsPath.join('/'), result = await load(pathname), snapshot = await settings();
98
+ const structured = websiteMetadata(result.state === 'published' ? result.entry : null, snapshot, pathname, configuration.websiteOrigin).jsonLd;
99
+ return <>{structured && <script type="application/ld+json" data-universalcms-setting="seo" dangerouslySetInnerHTML={{ __html: JSON.stringify(structured).replace(/</g, '\\\\u003c') }}/>}<CmsWebsitePage pathname={pathname} scope=${JSON.stringify(config.siteId+':'+config.environmentId)} initialResult={result} initialSettings={snapshot}${layout ? ' chrome={false}' : ''}/></>;
100
+ }
101
+ `;
102
+ }
103
+ async function ordinary(root,name) {
104
+ const content=await readProjectFile(root,name);if(content!==null&&(await lstat(path.join(root,name))).nlink!==1)fail('Website settings cannot replace linked files.');return content;
105
+ }
106
+ async function preserveSeoAndSettingsRoutes(root,app) {
107
+ for(const name of ['public/sitemap.xml','public/robots.txt',...(app?[...['js','jsx','ts','tsx'].flatMap(ext=>[`${app}/sitemap.${ext}`,`${app}/robots.${ext}`,`${app}/api/universalcms/settings/route.${ext}`,`${app}/api/universalcms/page/route.${ext}`,`${app}/sitemap.xml/route.${ext}`,`${app}/robots.txt/route.${ext}`]),`${app}/sitemap.xml`,`${app}/robots.txt`]:[])])if(await ordinary(root,name)!==null)fail('An existing settings, sitemap or robots route needs manual composition. It was preserved.');
108
+ }
109
+ async function inspectNativeChrome(root, registry, app, plannedFiles = [], layoutReview = null) {
110
+ const observed = new Map();
111
+ const nativeChrome = /<\s*(?:header|footer)\b|<\s*(?:[A-Z][\w$]*(?:Header|Footer|Navbar|NavBar|Shell)|Header|Footer|Navbar|NavBar|AppShell)\b|\bimport\s[\s\S]*?\bfrom\s*['"][^'"]*(?:^|\/)(?:[^/'"]*(?:[Hh]eader|[Ff]ooter|[Nn]avbar|[Nn]av[Bb]ar|[Ss]hell))[^/'"]*['"]/;
112
+ const inspect = (name, content) => {
113
+ if (nativeChrome.test(content) && name !== layoutReview?.rootLayout) throw setupReviewError('native-settings-layout', [name]);
114
+ observed.set(name, {path:name,content,state:'unchanged'});
115
+ };
116
+ for (const row of registry.routes) {
117
+ const planned = plannedFiles.find(file => file.path === row.originalFile);
118
+ const original = planned?.content ?? (await readFrozenContent(root,row)).texts.original;
119
+ inspect(row.originalFile,original);
120
+ if (!app) continue;
121
+ let directory = path.posix.dirname(row.sourceFile);
122
+ while (directory === app || directory.startsWith(app + '/')) {
123
+ for (const ext of ['js','jsx','ts','tsx']) { const name = `${directory}/layout.${ext}`, content = await ordinary(root,name); if (content !== null && !observed.has(name)) inspect(name,content); }
124
+ if (directory === app) break;
125
+ directory = path.posix.dirname(directory);
126
+ }
127
+ }
128
+ return [...observed.values()];
129
+ }
130
+ // New explicit native reviews must discover unsupported custom chrome before
131
+ // full setup writes source endpoints, publication guards or bound pages.
132
+ export async function assertPlannedNativeSettings(root, plan, publication) {
133
+ root = await projectRoot(root);
134
+ if (isNativeFramework(publication.framework)) { await inspectNativeProject(root, publication.framework); return; }
135
+ if (publication.framework === 'astro') { await inspectAstroProject(root); return; }
136
+ const registry = JSON.parse(plan.files.find(file => file.path === CONTENT_BINDINGS_FILE).content);
137
+ if (registry.profile !== REVIEWED_NEXT_NATIVE_PROFILE) return;
138
+ const ownership = await inspectReviewedNativeOwnership(root);
139
+ if (ownership.files.some(observed => publication?.files.some(file => file.path === observed.path && file.state !== 'unchanged'))) fail('A reviewed native dependency also needs publication edits. Review shared ownership before setup; existing files were preserved.');
140
+ const app = registry.routes[0].sourceFile.startsWith('src/app/') ? 'src/app' : 'app';
141
+ await inspectNativeChrome(root, registry, app, plan.files, await inspectSettingsLayoutReview(root));
142
+ }
143
+ // Preflight checks known settings conflicts against prospective original files.
144
+ // This is not a future installed settings plan and must never apply content.
145
+ export async function assertPlannedSettingsCompatibility(root, plan, publication) {
146
+ root = await projectRoot(root);
147
+ if (isNativeFramework(publication.framework)) { await inspectNativeProject(root, publication.framework); return; }
148
+ if (publication.framework === 'astro') { await inspectAstroProject(root); return; }
149
+ const registry = JSON.parse(plan.files.find(file => file.path === CONTENT_BINDINGS_FILE).content);
150
+ const isNext = publication.framework === 'next';
151
+ const app = isNext ? (registry.routes[0].sourceFile.startsWith('src/app/') ? 'src/app' : 'app') : null;
152
+ await preserveSeoAndSettingsRoutes(root, app);
153
+ if (await reviewPlannedCommerceSettings(root, registry)) return;
154
+ if (registry.profile === REVIEWED_NEXT_NATIVE_PROFILE) {
155
+ await assertPlannedNativeSettings(root, plan, publication);
156
+ return;
157
+ }
158
+ if (registry.profile) fail('This reviewed native application needs explicit shared settings slots. Existing routing, commerce and custom adapters were preserved.');
159
+ if (!isNext && (registry.routes.length !== 1 || registry.routes[0].path !== '/' || !/^src\/App\.(jsx|tsx)$/.test(registry.routes[0].sourceFile))) fail('A custom Vite router needs reviewed settings and new-page slots. Original pages were preserved.');
160
+ await inspectNativeChrome(root, registry, app, plan.files, await inspectSettingsLayoutReview(root));
161
+ }
162
+ export async function planWebsiteSettings(directory) {
163
+ const root=await projectRoot(directory),config=await readPublicationConfiguration(root),registry=await readContentRegistry(root),source=await readSourceSettings(root);
164
+ if(!config||!registry||!source.adminOrigin)fail('Complete reviewed original-page setup before setup-settings. Keep existing custom integrations and use the shared website runtime manually.');
165
+ if(['siteId','environmentId','websiteOrigin'].some(name=>source[name]!==config[name])||registry.routes.length!==config.routes.length||config.routes.some(route=>!registry.routes.some(row=>['path','kind','slug','locale'].every(name=>row[name]===route[name]))))fail('Source, publication and content bindings must match the exact saved website connection.');
166
+ const manifest=JSON.parse(await ordinary(root,'package.json'));
167
+ if(nativeFramework(manifest))return planNativeSettings(root,nativeFramework(manifest),{config,source});
168
+ if(manifest.dependencies?.astro??manifest.devDependencies?.astro)return planAstroSettings(root,{config,source});
169
+ const isNext=Boolean(manifest.dependencies?.next??manifest.devDependencies?.next);
170
+ const app=registry.routes[0].sourceFile.startsWith('src/app/')?'src/app':'app';
171
+ const connection={apiOrigin:config.apiOrigin,websiteOrigin:config.websiteOrigin,adminOrigin:source.adminOrigin};
172
+ const reviewed = await planReviewedCommerceSettings(root, {config,registry,source,manifest});
173
+ if (reviewed) return reviewed;
174
+ const native = registry.profile === REVIEWED_NEXT_NATIVE_PROFILE && isNext ? await inspectReviewedNativeOwnership(root) : null;
175
+ const layout = isNext ? await planNextSettingsLayout(root,config,native) : null;
176
+ const chromeObservations = await inspectNativeChrome(root, registry, isNext ? app : null, [], layout?.review);
177
+ const demo = isNext ? await inspectOwnedDemo(root) : null;
178
+ const ownedNativePath = pathname => native?.nativePaths.includes(pathname);
179
+ const ownedNativeDirectory = directory => native?.dynamicDirectories.includes(directory);
180
+ const nativeLayoutUpdate = !!(layout && native && layout.files.some(file=>native.files.some(observed=>observed.path===file.path)));
181
+ const layoutPaths = layout ? [...layout.files.map(file=>file.path), SETTINGS_LAYOUT_FILE, ...(nativeLayoutUpdate ? [native.manifest.path] : [])] : [];
182
+ const expectedPaths=[...layoutPaths,CONTENT_BINDINGS_FILE,...registry.routes.map(row=>row.sourceFile),...(isNext?[app==='src/app'?'src/proxy.ts':'proxy.ts',`${app}/[...cmsPath]/page.tsx`]:['.universalcms/source.vite.config.mjs','.universalcms/serve.mjs'])].sort();
183
+ await preserveSeoAndSettingsRoutes(root,isNext?app:null);
184
+ const previousText=await ordinary(root,FILE);
185
+ if(previousText!==null){
186
+ let previous;try{previous=JSON.parse(previousText);}catch{fail('Invalid website integration manifest.');}
187
+ if(previous.version!==1||previous.siteId!==config.siteId||previous.environmentId!==config.environmentId||Object.entries(connection).some(([name,value])=>previous[name]!==value)||JSON.stringify(previous.sections)!==JSON.stringify(sections)||!Array.isArray(previous.files)||previous.files.length!==expectedPaths.length||previous.files.some(row=>!row||Object.keys(row).sort().join()!=='path,sha256'||! /^[a-f0-9]{64}$/.test(row.sha256))||JSON.stringify(previous.files.map(row=>row.path).sort())!==JSON.stringify(expectedPaths))fail('Website settings scope or manifest is invalid.');
188
+ if(isNext){
189
+ const inventory=await inspectSourceRoutes(root);
190
+ if(inventory.unresolvedPaths.some(name=>name!==`${app}/[...cmsPath]`&&name!==demo?.path&&!ownedNativeDirectory(name))||inventory.routes.some(route=>!isExcludedPublicationPath(`/${route}`)&&!ownedNativePath(`/${route}`)&&!config.routes.some(row=>row.path===`/${route}`)))fail('Native routing changed after settings setup. Review new routes before continuing.');
191
+ const publication=await planNextPublication(root,config,{websiteSettings:true});
192
+ if(publication.files.some(file=>file.state!=='unchanged'))fail('The Next publication layout changed. Review settings integration before continuing.');
193
+ }else{
194
+ const suffix=' --config .universalcms/source.vite.config.mjs';
195
+ if(!manifest.scripts?.dev?.endsWith(suffix)||!manifest.scripts?.build?.endsWith(suffix)||manifest.scripts.start!=='node .universalcms/serve.mjs')fail('The website startup scripts changed. Review settings integration before continuing.');
196
+ }
197
+ const files=[];
198
+ for(const row of previous.files){const content=await ordinary(root,row.path);if(content===null||contentDigest(content)!==row.sha256)fail('A website settings adapter was customized. Preserve the changes and review the shared integration manually.');files.push({path:row.path,content,state:'unchanged'});}
199
+ for(const row of registry.routes)await readFrozenContent(root,row);
200
+ for(const name of ['package.json','.universalcms/publication.config.json','.universalcms/source.config.json'])files.push({path:name,content:await ordinary(root,name),state:'unchanged'});
201
+ return {root,files:[...files,...(layout?.observations??[]).filter(file=>file.content!==null),...chromeObservations,...(demo?.files??[]),...(native ? [...native.files,native.manifest] : []),{path:FILE,content:previousText,state:'unchanged'}],sections,config};
202
+ }
203
+ if(registry.profile && !native)fail('This reviewed native application needs explicit shared settings slots. Existing routing, commerce and custom adapters were preserved.');
204
+ const files=[],observed=[...(layout?.observations??[]),...chromeObservations,...(demo?.files??[]),...(native ? [...native.files,native.manifest] : [])];
205
+ const own=async(name,content,expected=null)=>{const current=await ordinary(root,name);if(current!==expected)fail(`Existing ${name} needs manual settings integration; it was preserved.`);files.push({path:name,content,state:current===null?'create':current===content?'unchanged':'update',...(current!==null&&current!==content?{previous:current}:{})});};
206
+ for(const row of registry.routes){const frozen=await readFrozenContent(root,row);observed.push(...Object.entries(frozen.texts).map(([key,content])=>({path:row[`${key}File`],content,state:'unchanged'})));}
207
+ const nextRegistry=JSON.parse(json(registry));
208
+ if(isNext){
209
+ const publication=await planNextPublication(root,config);
210
+ if(publication.files.some(file=>file.state!=='unchanged'))fail('The Next publication layout changed. Review settings integration before continuing.');
211
+ const inventory=await inspectSourceRoutes(root);
212
+ if(inventory.routes.some(route=>!isExcludedPublicationPath(`/${route}`)&&!ownedNativePath(`/${route}`)&&!config.routes.some(row=>row.path===`/${route}`)))fail('Native routing changed after original-page setup. Review new routes before continuing.');
213
+ if(native && inventory.unresolvedPaths.some(name=>name!==demo?.path&&!ownedNativeDirectory(name)))fail('Unreviewed dynamic or parallel routes need explicit settings ownership.');
214
+ // Reject competing dynamic ownership, including nested dynamic routes.
215
+ let inspected=0;
216
+ async function scan(directory,depth=0){if(depth>14)fail('Route inspection exceeded its bound.');for(const item of await readdir(path.join(root,directory),{withFileTypes:true})){if(++inspected>4000)fail('Route inspection exceeded its bound.');if(item.isSymbolicLink())fail('Linked routes need manual settings integration.');if(item.isDirectory()){const target=`${directory}/${item.name}`;if(item.name.includes('[')&&target!==demo?.path&&!ownedNativeDirectory(target))fail('Existing dynamic routes require reviewed settings composition.');await scan(target,depth+1);}}}
217
+ await scan(app);
218
+ const proxy=app==='src/app'?'src/proxy.ts':'proxy.ts',prior=nextPublicationProxyTemplate(app);
219
+ await own(proxy,nextWebsiteProxyTemplate(app),prior);
220
+ await own(`${app}/[...cmsPath]/page.tsx`,catchall(app,config,native,!!layout));
221
+ for(const row of nextRegistry.routes){const current=await ordinary(root,row.sourceFile),expected=nextContentPageTemplate(row);if(current!==expected)fail('A customized native page needs manual settings slots.');const content=nextPage(row,config,native,!!layout);await own(row.sourceFile,content,current);row.sourceHash=contentDigest(content);}
222
+ }else{
223
+ if(registry.routes.length!==1||registry.routes[0].path!=='/'||!/^src\/App\.(jsx|tsx)$/.test(registry.routes[0].sourceFile))fail('A custom Vite router needs reviewed settings and new-page slots. Original pages were preserved.');
224
+ const wrapper=viteSourceWrapper(true);
225
+ const updated=wrapper.includes('universalCmsWebsite') ? wrapper : `import { universalCmsWebsite } from '@universalcms/connect/website-node';\n`+wrapper.replace('plugins: [','plugins: [universalCmsWebsite({ configuration: publication, sourceConfiguration: configuration, contentBindings }), ');
226
+ await own('.universalcms/source.vite.config.mjs',updated,wrapper);
227
+ const serve=await ordinary(root,'.universalcms/serve.mjs');
228
+ if(serve!==viteServerTemplate())fail('The website server was customized; compose createWebsiteServer manually.');
229
+ await own('.universalcms/serve.mjs',serve.replaceAll('createVitePublicationServer','createWebsiteServer').replace('@universalcms/connect/vite-publication-server','@universalcms/connect/website-node'),serve);
230
+ const row=nextRegistry.routes[0],current=await ordinary(root,row.sourceFile);
231
+ if(current!==viteContentPageTemplate(row,(await readFrozenContent(root,row)).inventory))fail('The existing Vite wrapper needs public binding review; preserve older or customized adapters and compose CmsWebsite manually.');
232
+ const content=vitePage(row,config,current);await own(row.sourceFile,content,current);row.sourceHash=contentDigest(content);
233
+ }
234
+ if(layout){
235
+ for(const file of layout.files)await own(file.path,file.content,file.previous??(file.state==='create'?null:file.content));
236
+ await own(SETTINGS_LAYOUT_FILE,layout.review.observations.find(file=>file.path===SETTINGS_LAYOUT_FILE).content,layout.review.observations.find(file=>file.path===SETTINGS_LAYOUT_FILE).content);
237
+ if(nativeLayoutUpdate){
238
+ const value=JSON.parse(native.manifest.content),changed=new Map(layout.files.map(file=>[file.path,file.content]));
239
+ for(const row of value.files)if(changed.has(row.path))row.sha256=contentDigest(changed.get(row.path));
240
+ // A reviewed native module importing the root layout also owns the new
241
+ // server wrapper dependency. Keep its complete closure synchronized.
242
+ if(value.files.some(row=>row.path===layout.review.rootLayout)&&!value.files.some(row=>row.path===layout.review.serverFile))value.files.push({path:layout.review.serverFile,sha256:contentDigest(changed.get(layout.review.serverFile))});
243
+ value.files.sort((a,b)=>a.path.localeCompare(b.path));
244
+ const content=json(value);await own(native.manifest.path,content,native.manifest.content);
245
+ nextRegistry.files=[...value.files,{path:native.manifest.path,sha256:contentDigest(content)}];
246
+ }
247
+ }
248
+ const registryText=await ordinary(root,CONTENT_BINDINGS_FILE);await own(CONTENT_BINDINGS_FILE,json(nextRegistry),registryText);
249
+ const hashes=files.map(file=>({path:file.path,sha256:contentDigest(file.content)}));
250
+ await own(FILE,json({version:1,siteId:config.siteId,environmentId:config.environmentId,...connection,sections,files:hashes}));
251
+ const writes=new Set(files.map(file=>file.path));
252
+ for(const observation of observed)if(!writes.has(observation.path)){files.unshift(observation);writes.add(observation.path);}
253
+ for(const name of ['package.json','.universalcms/publication.config.json','.universalcms/source.config.json'])files.unshift({path:name,content:await ordinary(root,name),state:'unchanged'});
254
+ return {root,files,sections,config};
255
+ }
256
+ export async function setupWebsiteSettings(directory,{apply=false}={}) {const plan=await planWebsiteSettings(directory);if(apply)await applyPlan(plan);return {applied:apply,sections:plan.sections,files:plan.files.map(({path,state})=>({path,state})),renderedVerified:false,message:apply?`Website settings installed: ${plan.sections.join(', ')}. Restart or deploy, verify each selected scope, then test the actual website. No key, CMS content or settings were changed.`:'Plan only. Review the files and rerun setup-settings --apply. Custom layouts need explicit shared component slots.'};}
@@ -0,0 +1,49 @@
1
+ .cms-website { --cms-website-accent: #2563eb; }
2
+ .cms-website-header:empty, .cms-website-footer:empty { display: none; }
3
+ .cms-website-header, .cms-website-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem clamp(1rem, 4vw, 3rem); color: inherit; background: inherit; }
4
+ .cms-website-header:not(:has(> *)), .cms-website-footer:not(:has(> *)) { display: none; }
5
+ .cms-website-header { border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent); }
6
+ .cms-website-footer { border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); }
7
+ .cms-website a { overflow-wrap: anywhere; }
8
+ .cms-website-header a, .cms-website-footer a, .cms-website-supplement a, .cms-website-page a { color: var(--cms-website-accent); }
9
+ .cms-website-branding { max-width: 40rem; }
10
+ .cms-website-branding img { display: block; max-width: min(15rem, 70vw); max-height: 4rem; width: auto; height: auto; object-fit: contain; }
11
+ .cms-website-branding p { margin: .5rem 0 0; font-size: .9rem; }
12
+ .cms-website-navigation { max-width: 100%; }
13
+ .cms-website-navigation ul, .cms-website-social, .cms-website-languages, .cms-website-terms { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; margin: 0; padding: 0; list-style: none; }
14
+ .cms-website-navigation a, .cms-website-navigation summary, .cms-website-social a, .cms-website-languages a, .cms-website-terms a { display: inline-flex; gap: .5rem; align-items: center; min-height: 2.75rem; }
15
+ .cms-website-navigation details { position: relative; }
16
+ .cms-website-navigation summary { cursor: pointer; }
17
+ .cms-website-navigation summary::after { content: '⌄'; margin-inline-start: .3rem; }
18
+ .cms-website-navigation details > ul { position: absolute; z-index: 40; top: 100%; min-width: 12rem; max-width: min(20rem, 85vw); flex-direction: column; align-items: stretch; gap: 0; padding: .5rem 1rem; border: 1px solid #64748b; border-radius: .5rem; color: #0f172a; background: #fff; box-shadow: 0 .5rem 1.5rem #0002; }
19
+ .cms-website-footer-groups { display: flex; gap: 2rem; flex-wrap: wrap; }
20
+ .cms-website-footer-groups h2 { font-size: 1rem; margin: .75rem 0; }
21
+ .cms-website-footer-groups ul { flex-direction: column; align-items: start; gap: 0; }
22
+ .cms-website-social img { width: 1.25rem; height: 1.25rem; object-fit: contain; }
23
+ .cms-website-align-left { justify-content: flex-start; }.cms-website-align-center { justify-content: center; }.cms-website-align-right { justify-content: flex-end; }
24
+ .cms-website-languages [aria-current=page] { font-weight: 700; text-decoration: underline; }
25
+ .cms-website-page { width: min(100% - 2rem, 72rem); margin: 0 auto; padding: 3rem 0; overflow-wrap: anywhere; }
26
+ .cms-website-page.cms-website-layout-narrow, .cms-website-native.cms-website-layout-narrow { max-width: 48rem; margin-inline: auto; }
27
+ .cms-website-page.cms-website-layout-wide { width: min(100% - 2rem, 90rem); }
28
+ .cms-website-native.cms-website-layout-standard { max-width: 80rem; margin-inline: auto; }
29
+ .cms-website-native.cms-website-layout-wide { max-width: 100%; }
30
+ .cms-website-page > h1 { font-size: clamp(2rem, 5vw, 3.5rem); line-height: 1.15; margin: 1rem 0; }
31
+ .cms-website-excerpt { font-size: 1.15rem; line-height: 1.7; }
32
+ .cms-website-featured { display: block; width: 100%; height: auto; max-height: 36rem; object-fit: cover; margin: 1.5rem 0; border-radius: .75rem; }
33
+ .cms-website-toc, .cms-website-author, .cms-website-related { margin-block: 2rem; padding: 1.25rem; border: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius: .75rem; }
34
+ .cms-website-toc h2, .cms-website-author h2, .cms-website-related h2 { font-size: 1.25rem; }
35
+ .cms-website-author { display: flex; align-items: flex-start; gap: 1rem; }
36
+ .cms-website-author img { width: 4rem; height: 4rem; object-fit: cover; border-radius: 50%; }
37
+ .cms-website-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1.5rem; }
38
+ .cms-website-cards article { padding: 1rem; border: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius: .5rem; }
39
+ .cms-website-share { display: flex; gap: .75rem; align-items: center; margin-block: 1rem; }
40
+ .cms-website-share button, .cms-website-preferences, .cms-website-consent button { min-height: 2.75rem; padding: .5rem 1rem; font: inherit; border: 1px solid #64748b; border-radius: .4rem; cursor: pointer; background: transparent; color: inherit; }
41
+ .cms-website-supplement { margin: 1rem clamp(1rem, 4vw, 3rem); }
42
+ .cms-website-consent { position: fixed; z-index: 1000; bottom: 1rem; width: min(32rem, calc(100vw - 2rem)); max-height: calc(100dvh - 2rem); overflow-y: auto; box-sizing: border-box; padding: 1.25rem; border: 1px solid #cbd5e1; border-radius: .75rem; background: #fff; color: #0f172a; box-shadow: 0 .5rem 3rem #0003; font: 1rem/1.5 system-ui, sans-serif; }
43
+ .cms-website-consent h2 { margin: 0 0 .5rem; font-size: 1.25rem; }.cms-website-consent p { margin: .5rem 0; }
44
+ .cms-website-consent fieldset { margin: 1rem 0; padding: .75rem; }.cms-website-consent label { display: flex; align-items: center; gap: .5rem; min-height: 2.75rem; }.cms-website-consent input { width: 1.2rem; height: 1.2rem; }
45
+ .cms-website-consent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
46
+ .cms-website-bottom-left { left: 1rem; }.cms-website-bottom-right { right: 1rem; }.cms-website-bottom-center { left: 50%; transform: translateX(-50%); }
47
+ .cms-website-error { margin: 4rem auto; padding: 1rem; max-width: 48rem; }
48
+ .cms-website :focus-visible { outline: 3px solid var(--cms-website-accent); outline-offset: 3px; }
49
+ @media(max-width: 640px) { .cms-website-header, .cms-website-footer { align-items: stretch; flex-direction: column; }.cms-website-navigation details > ul { position: static; max-width: 100%; }.cms-website-page { padding-block: 1.5rem; }.cms-website-author { flex-wrap: wrap; } }