@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,26 @@
1
+ /** Exact reviewed native commerce routes. No source execution or private reads.
2
+ * These paths remain owned by the website; editorial routes are CMS managed.
3
+ * A changed source/data module requires a fresh review rather than broadening
4
+ * a route prefix and accidentally exposing newly added application URLs.
5
+ */
6
+ import { readProjectFile } from './project-files.mjs';
7
+ import { contentDigest } from './content-registry.mjs';
8
+ import { REVIEWED_ICECREAM_PROFILE, reviewedNativePaths } from './content-registry-validation.mjs';
9
+ const fingerprints = {
10
+ 'src/app/menu/[id]/page.tsx':'803a7576ee4ce35ac1e971ae9d4cc643336f3b46468f085e46d2d252462d3432',
11
+ 'src/data/products.ts':'92c20343a13f855c0f075af05ed13e18b1f5b0e37349cf61489e867b34c314f1',
12
+ 'src/components/ProductCard.tsx':'de714ca19062576f91887c5f5648b699bf51d854fec665cf006621af36a60c09',
13
+ 'src/context/CartContext.tsx':'654ce1500812ae64f24e5e206dd93519dccadb761492fd8ac89ae8c103620abf',
14
+ 'src/app/cart/page.tsx':'f621ea9dea46ef4fd748bb5414a8698cbb18b3f81d9ddd18a319d5e38d4fe037',
15
+ 'src/app/checkout/page.tsx':'ac57d998f0d9b27e8962773edf25e905b5a2226a044d5668861b351d97fca889',
16
+ 'src/app/order-success/page.tsx':'c6d8fa3addbb6d63a765d4e130fdc799da6098a8ac5993bf9fe9c36c00fd672f',
17
+ };
18
+ export async function inspectReviewedCommerceOwnership(root) {
19
+ const files = [];
20
+ for (const [path,sha256] of Object.entries(fingerprints)) {
21
+ const content = await readProjectFile(root,path);
22
+ if (content === null || contentDigest(content) !== sha256) return null;
23
+ files.push({path,content,state:'unchanged'});
24
+ }
25
+ return {profile:REVIEWED_ICECREAM_PROFILE,nativePaths:reviewedNativePaths(REVIEWED_ICECREAM_PROFILE),files};
26
+ }
@@ -0,0 +1,243 @@
1
+ /** Exact reviewed native website settings slots. No customer code execution or key reads. */
2
+ import path from 'node:path';
3
+ import { lstat, readdir } from 'node:fs/promises';
4
+ import { readProjectFile, SetupError } from './core.mjs';
5
+ import { CONTENT_BINDINGS_FILE, contentDigest, readFrozenContent, REVIEWED_ICECREAM_PROFILE } from './content-registry.mjs';
6
+ import { inspectReviewedCommerceOwnership } from './reviewed-commerce-ownership.mjs';
7
+ import { nextPublicationProxyTemplate, nextWebsiteProxyTemplate, planNextPublication } from './publication-next-setup.mjs';
8
+ import { inspectOwnedDemo, nextContentPageTemplate } from './content-next-setup.mjs';
9
+ import { inspectSourceRoutes } from './source-routes.mjs';
10
+ const FILE = '.universalcms/website.integration.json';
11
+ const sections = ['branding','social','seo','languages','analytics','navigation','experience','scripts','redirects','authors','templates','taxonomies'];
12
+ const prefixes = ['/menu/', '/cart/', '/checkout/', '/order-success/'];
13
+ const hashes = {
14
+ 'src/app/layout.tsx': '388e113c0bad1ad8524f6f56374648b94a672eca7ebebb4f39afd7c0eae62ad5',
15
+ 'src/components/Navbar.tsx': '92c74a7d63b2d5640e97cbcef0403d4959fd4f58dfd4e8c136ca27c86958ea92',
16
+ 'src/components/Footer.tsx': '0eaca8f58c2fe7f6b3725d72db11e6d882969c73ff97b9991cd70c7c85b58698',
17
+ };
18
+ const baseOutputs = [...Object.keys(hashes), 'src/proxy.ts', 'src/cms/website-navigation.server.ts', 'src/cms/website-navigation.css', 'src/cms/NativeSettings.tsx', 'src/app/[...cmsPath]/page.tsx'];
19
+ const fail = message => { throw new SetupError(message); };
20
+ const json = value => JSON.stringify(value, null, 2) + '\n';
21
+ const relative = (from,to) => { const value = path.posix.relative(path.posix.dirname(from),to); return value.startsWith('.') ? value : './' + value; };
22
+ async function ordinary(root, name) {
23
+ const content = await readProjectFile(root, name);
24
+ if (content !== null && (await lstat(path.join(root, name))).nlink !== 1) fail('Reviewed navigation cannot replace linked files.');
25
+ return content;
26
+ }
27
+ async function reviewedNavigationSources(root) {
28
+ const originals = {};
29
+ for (const [name,digest] of Object.entries(hashes)) { const content = await ordinary(root,name); if (content === null || contentDigest(content) !== digest) fail('This native header/footer layout has changed and needs reviewed Navigation slots. Existing website code was preserved.'); originals[name] = content; }
30
+ return originals;
31
+ }
32
+ /** Check the exact supported native slots before content files are installed. */
33
+ export async function reviewPlannedCommerceSettings(root, registry) {
34
+ if (registry.profile !== REVIEWED_ICECREAM_PROFILE) return false;
35
+ if (registry.routes.some(row => !row.sourceFile.startsWith('src/app/')) || !await inspectReviewedCommerceOwnership(root)) fail('Reviewed native commerce ownership changed. Existing header/footer and commerce routes were preserved.');
36
+ reviewedCommerceNavigationTemplates(await reviewedNavigationSources(root));
37
+ return true;
38
+ }
39
+ const replace = (source, before, after) => {
40
+ if (source.split(before).length !== 2) fail('A reviewed navigation slot changed. Existing header and footer were preserved.');
41
+ return source.replace(before, after);
42
+ };
43
+
44
+ /** Pure transformations, called only after exact source fingerprint validation. */
45
+ export function reviewedCommerceNavigationTemplates(sources) {
46
+ let layout = sources['src/app/layout.tsx'];
47
+ layout = replace(layout, "import './globals.css';", "import './globals.css';\nimport '@universalcms/connect/website.css';\nimport '@/cms/website-navigation.css';\nimport { WebsiteProvider } from '@universalcms/connect/react-website';\nimport { NativeSettings } from '@/cms/NativeSettings';\nimport { websiteSettings, websiteScope } from '@/cms/website-navigation.server';");
48
+ layout = replace(layout, 'export default function RootLayout(', 'export default async function RootLayout(');
49
+ layout = replace(layout, ' return (', ' const settings = await websiteSettings();\n return (');
50
+ layout = replace(layout, ' <CartProvider>', ' <WebsiteProvider initialSettings={settings} scope={websiteScope}>\n <NativeSettings><CartProvider>');
51
+ layout = replace(layout, ' </CartProvider>', ' </CartProvider></NativeSettings>\n </WebsiteProvider>');
52
+ let navbar = sources['src/components/Navbar.tsx'];
53
+ navbar = replace(navbar, "import { useCart } from '@/context/CartContext';", "import { useCart } from '@/context/CartContext';\nimport { CmsBranding, CmsLanguages, CmsNavigation } from '@universalcms/connect/react-website';");
54
+ navbar = replace(navbar, '<div className="container" style={{', '<div className="container cms-native-navbar-row" style={{');
55
+ navbar = replace(navbar, "height: '76px'", "minHeight: '76px', flexWrap: 'wrap', gap: '8px 16px'");
56
+ navbar = replace(navbar, '<Link href="/" style={{', '<Link href="/" className="cms-native-brand" style={{');
57
+ navbar = replace(navbar, "textDecoration: 'none'\n }}>", "textDecoration: 'none', order: 1, flexShrink: 0\n }}>");
58
+ const logoStart = navbar.indexOf(' <Link href="/" className="cms-native-brand"'), logoEnd = navbar.indexOf('\n {/* Desktop Navigation */}',logoStart);
59
+ if (logoStart < 0 || logoEnd < 0) fail('The reviewed branding slot changed. Native branding was preserved.');
60
+ const nativeLogo = navbar.slice(logoStart,logoEnd).trim();
61
+ navbar = navbar.slice(0,logoStart) + ' <div className="cms-native-brand-slot"><CmsBranding fallback={' + nativeLogo + '}/></div>\n' + navbar.slice(logoEnd);
62
+ navbar = replace(navbar, '<nav style={{', '<nav aria-label="Main navigation" style={{');
63
+ navbar = replace(navbar, "gap: '24px'\n }} className=\"desktop-nav\">", "gap: '8px', flexWrap: 'wrap', justifyContent: 'center', flexBasis: '100%', order: 3, paddingBottom: '10px'\n }} className=\"desktop-nav\">");
64
+ navbar = replace(navbar, ' })}\n </nav>', ' })}\n <CmsNavigation excludeUrls={[\'/\', ...navLinks.map(link => link.href)]} />\n <CmsLanguages />\n </nav>');
65
+ navbar = replace(navbar, "<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>", "<div className=\"cms-native-nav-actions\" style={{ display: 'flex', alignItems: 'center', gap: '12px', order: 2, flexShrink: 0 }}>");
66
+ navbar = replace(navbar, 'aria-label="Toggle Mobile Menu"', 'aria-label="Toggle Mobile Menu"\n aria-expanded={mobileMenuOpen}\n aria-controls="cms-native-mobile-navigation"');
67
+ navbar = replace(navbar, '<span style={{ display: \'inline-block\' }}>Box</span>', '<span className="cms-native-cart-label" style={{ display: \'inline-block\' }}>Box</span>');
68
+ navbar = replace(navbar, "{mobileMenuOpen && (\n <div style={{", "{mobileMenuOpen && (\n <div id=\"cms-native-mobile-navigation\" role=\"navigation\" aria-label=\"Mobile navigation\" className=\"cms-native-mobile-navigation\" style={{");
69
+ navbar = replace(navbar, " ))}\n <div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>", " ))}\n <CmsNavigation excludeUrls={['/', ...navLinks.map(link => link.href)]} onNavigate={() => setMobileMenuOpen(false)} />\n <CmsLanguages />\n <div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>");
70
+ let footer = sources['src/components/Footer.tsx'];
71
+ footer = replace(footer, "import Link from 'next/link';", "import Link from 'next/link';\nimport { CmsBranding, CmsNavigation, CmsSocialLinks, CmsExperience } from '@universalcms/connect/react-website';");
72
+ const footerBrandStart = footer.indexOf(" <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>"), footerBrandEnd = footer.indexOf(" <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.82rem'",footerBrandStart);
73
+ if (footerBrandStart < 0 || footerBrandEnd < 0) fail('The reviewed footer branding slot changed. Native branding was preserved.');
74
+ footer = footer.slice(0,footerBrandStart) + ' <CmsBranding fallback={<>' + footer.slice(footerBrandStart,footerBrandEnd).trim() + '</>}/>\n' + footer.slice(footerBrandEnd);
75
+ const nativeFooter = [...new Set([...footer.matchAll(/\bhref="([^"]+)"/g)].map(match => match[1]))];
76
+ footer = replace(footer, ' {/* Bottom Bar */}', ` <CmsNavigation location="footer" excludeUrls={${JSON.stringify(nativeFooter)}} />\n <CmsSocialLinks />\n <CmsExperience />\n\n {/* Bottom Bar */}`);
77
+ return {'src/app/layout.tsx': layout, 'src/components/Navbar.tsx': navbar, 'src/components/Footer.tsx': footer};
78
+ }
79
+ function serverTemplate() {
80
+ return `// Generated server-only website settings. Private keys are read by the runtime.
81
+ import { cache } from 'react';
82
+ import { connection } from 'next/server';
83
+ import { createWebsiteRuntime } from '@universalcms/connect/website-runtime';
84
+ import configuration from '../../.universalcms/publication.config.json';
85
+ import contentBindings from '../../.universalcms/content.bindings.json';
86
+ export const websiteScope = configuration.siteId + ':' + configuration.environmentId;
87
+ export const websiteOrigin = configuration.websiteOrigin;
88
+ export const website = createWebsiteRuntime({
89
+ configuration: configuration as Parameters<typeof createWebsiteRuntime>[0]['configuration'],
90
+ nativePaths: contentBindings.nativePaths,
91
+ nativePathPrefixes: ${JSON.stringify(prefixes)},
92
+ installedSections: ${JSON.stringify(sections)},
93
+ });
94
+ export const websiteSettings = cache(async () => { await connection(); return website.settings(); });
95
+ `;
96
+ }
97
+ function nativeSettingsTemplate() {
98
+ return `"use client";
99
+ import type { CSSProperties, ReactNode } from 'react';
100
+ import { useWebsiteSettings } from '@universalcms/connect/react-website';
101
+ // The reviewed theme uses these accent variables. Removing Branding restores its native CSS.
102
+ export function NativeSettings({children}: {children: ReactNode}) {
103
+ const {settings} = useWebsiteSettings(), branding = settings.sections.branding;
104
+ const color = branding.state === 'ready' && typeof branding.data?.accentColor === 'string' && /^#[a-f0-9]{6}$/i.test(branding.data.accentColor) ? branding.data.accentColor : undefined;
105
+ const style = color ? {'--cms-website-accent':color,'--berry-500':color,'--berry-600':color,'--caramel-500':color,'--caramel-600':color} as CSSProperties : undefined;
106
+ return <div className="cms-native-settings" style={style}>{children}</div>;
107
+ }
108
+ `;
109
+ }
110
+ const metadataTemplate = (entry, pathname) => `const snapshot = await websiteSettings(), entry = ${entry};
111
+ if (snapshot.sections.seo.state !== 'ready') return { title: entry?.document.title ?? (result.state === 'archive' ? result.title : ''), description: entry?.document.excerpt };
112
+ const value = websiteMetadata(entry, snapshot, ${pathname}, websiteOrigin);
113
+ return { 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,other:{'universalcms-seo':'managed'} };`;
114
+ function originalPageTemplate(row) {
115
+ const typed = row.sourceFile.endsWith('.tsx'), pathname = JSON.stringify(row.path);
116
+ return `// Generated native website content and settings; root layout owns shared header/footer effects.
117
+ import { cache } from 'react';
118
+ import { connection } from 'next/server';
119
+ import { notFound } from 'next/navigation';
120
+ import { CmsWebsite } from '@universalcms/connect/react-website';
121
+ import { PublishedDocumentProvider } from '@universalcms/connect/react-publication';
122
+ import { websiteMetadata } from '@universalcms/connect/website-runtime';
123
+ import { website, websiteSettings, websiteScope, websiteOrigin } from '@/cms/website-navigation.server';
124
+ import inventory from ${JSON.stringify(relative(row.sourceFile,row.inventoryFile))};
125
+ import NativePage from ${JSON.stringify(relative(row.sourceFile,row.nativeFile).replace(/\.[jt]sx?$/,''))};
126
+ const load = cache(async () => { await connection(); const result = await website.page(${pathname}); if (result.state === 'missing') notFound(); if (result.state !== 'published') throw new Error('Published content is temporarily unavailable'); return result; });
127
+ export async function generateMetadata() {
128
+ const result = await load();
129
+ ${metadataTemplate('result.entry',pathname).replace("result.state === 'archive' ? result.title : ''", "''").replace("result.state === 'archive' ? result.title : value.title", 'value.title')}
130
+ }
131
+ export default async function Page() {
132
+ const result = await load(), snapshot = await websiteSettings(), entry = result.entry;
133
+ const structured = websiteMetadata(entry,snapshot,${pathname},websiteOrigin).jsonLd;
134
+ return <>{structured && <script type="application/ld+json" data-universalcms-setting="seo" dangerouslySetInnerHTML={{__html:JSON.stringify(structured).replace(/</g,'\\\\u003c')}}/>}<CmsWebsite scope={websiteScope} initialSettings={snapshot} entry={entry} chrome={false}><PublishedDocumentProvider initialEntry={entry} pathname={${pathname}} route={inventory${typed ? " as import('@universalcms/connect/react-publication').Inventory" : ''}}><NativePage/></PublishedDocumentProvider></CmsWebsite></>;
135
+ }
136
+ `;
137
+ }
138
+ function catchallTemplate() {
139
+ return `// Generated CMS-created pages; native routes and commerce prefixes remain website-owned.
140
+ import { cache } from 'react';
141
+ import { connection } from 'next/server';
142
+ import { notFound } from 'next/navigation';
143
+ import { CmsWebsitePage } from '@universalcms/connect/react-website';
144
+ import { websiteMetadata } from '@universalcms/connect/website-runtime';
145
+ import { website, websiteSettings, websiteScope, websiteOrigin } from '@/cms/website-navigation.server';
146
+ const load = cache(async (pathname: string) => {
147
+ await connection();
148
+ const result = await website.resolve(pathname);
149
+ if (result.state === 'missing' || result.state === 'unmanaged') notFound();
150
+ if (result.state !== 'published' && result.state !== 'archive') throw new Error('Published website content unavailable');
151
+ return result;
152
+ });
153
+ type Props = { params: Promise<{ cmsPath: string[] }> };
154
+ export async function generateMetadata({ params }: Props) {
155
+ const pathname = '/' + (await params).cmsPath.join('/'), result = await load(pathname);
156
+ ${metadataTemplate("result.state === 'published' ? result.entry : null",'pathname')}
157
+ }
158
+ export default async function Page({ params }: Props) {
159
+ const pathname = '/' + (await params).cmsPath.join('/'), result = await load(pathname), snapshot = await websiteSettings();
160
+ const structured = websiteMetadata(result.state === 'published' ? result.entry : null,snapshot,pathname,websiteOrigin).jsonLd;
161
+ return <>{structured && <script type="application/ld+json" data-universalcms-setting="seo" dangerouslySetInnerHTML={{__html:JSON.stringify(structured).replace(/</g,'\\\\u003c')}}/>}<CmsWebsitePage pathname={pathname} scope={websiteScope} initialResult={result} initialSettings={snapshot} chrome={false}/></>;
162
+ }
163
+ `;
164
+ }
165
+ const css = `/* Preserve native menus, including mobile behavior and cart actions. */
166
+ .cms-native-settings { min-height:100vh; display:flex; flex-direction:column; }
167
+ .cms-native-brand-slot { order:1; flex-shrink:0; max-width:60%; }
168
+ .cms-native-brand-slot .cms-website-branding img { max-width:min(15rem,45vw); }
169
+ .cms-native-navbar-row { padding-top:12px; padding-bottom:12px; }
170
+ header .cms-website-navigation { color:var(--cocoa-700); }
171
+ header .cms-website-navigation > nav > ul { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
172
+ header .cms-website-navigation a, header .cms-website-navigation summary { padding:8px 12px; font-size:.92rem; font-weight:600; }
173
+ .cms-native-mobile-navigation .cms-website-navigation > nav > ul { align-items:stretch; flex-direction:column; }
174
+ .cms-native-mobile-navigation .cms-website-navigation a { display:block; }
175
+ footer .cms-website-navigation { color:var(--cream-300); margin:24px 0; }
176
+ footer .cms-website-navigation h2 { color:inherit; font-size:1.05rem; }
177
+ @media(max-width:600px) { .cms-native-brand { gap:7px!important; } .cms-native-brand > div:first-child { width:32px!important; height:36px!important; } .cms-native-brand > div:last-child > span:first-child { font-size:1.05rem!important; } .cms-native-brand > div:last-child > span:last-child { display:none!important; } .cms-native-nav-actions { gap:6px!important; } .cms-native-cart-label { display:none!important; } }
178
+ `;
179
+
180
+ export async function planReviewedCommerceSettings(root, {config, registry, source, manifest}) {
181
+ if (registry.profile !== REVIEWED_ICECREAM_PROFILE) return null;
182
+ if (!(manifest.dependencies?.next ?? manifest.devDependencies?.next) || registry.routes.some(row => !row.sourceFile.startsWith('src/app/'))) fail('The reviewed navigation profile requires its original Next application layout.');
183
+ const ownership = await inspectReviewedCommerceOwnership(root);
184
+ if (!ownership) fail('Reviewed native commerce ownership changed. Existing header/footer and commerce routes were preserved.');
185
+ const previousText = await ordinary(root, FILE), existing = previousText !== null;
186
+ const outputs = [...baseOutputs,CONTENT_BINDINGS_FILE,...registry.routes.map(row => row.sourceFile)].sort();
187
+ const connection = {siteId:config.siteId,environmentId:config.environmentId,apiOrigin:config.apiOrigin,websiteOrigin:config.websiteOrigin,adminOrigin:source.adminOrigin};
188
+ const demo = await inspectOwnedDemo(root), inventory = await inspectSourceRoutes(root);
189
+ const allowedDynamic = ['src/app/menu/[id]', ...(existing ? ['src/app/[...cmsPath]'] : []), ...(demo ? [demo.path] : [])];
190
+ if (inventory.unresolvedPaths.some(name => !allowedDynamic.includes(name)) || inventory.routes.some(route => !config.routes.some(row => row.path === '/' + route) && !ownership.nativePaths.includes('/' + route))) fail('Native routing changed. Review every new route before installing website navigation.');
191
+ let count = 0;
192
+ async function scan(directory, depth = 0) {
193
+ if (depth > 14) fail('Route inspection exceeded its bound.');
194
+ for (const item of await readdir(path.join(root, directory), {withFileTypes:true})) {
195
+ if (++count > 4000 || item.isSymbolicLink()) fail('Linked or oversized routes require manual navigation review.');
196
+ if (item.isDirectory()) { const name = directory + '/' + item.name; if (item.name.includes('[') && !allowedDynamic.includes(name)) fail('Unreviewed dynamic routes require manual navigation composition.'); await scan(name, depth + 1); }
197
+ }
198
+ }
199
+ await scan('src/app');
200
+ const publication = await planNextPublication(root, config, {websiteSettings:existing,installedSections:sections});
201
+ if (publication.files.some(file => file.state !== 'unchanged')) fail('The reviewed publication layout changed. Restore or review it before navigation setup.');
202
+ // These endpoints become settings-owned only when no native owner exists.
203
+ for (const name of ['public/sitemap.xml','public/robots.txt',...['js','jsx','ts','tsx'].flatMap(ext => [`src/app/sitemap.${ext}`,`src/app/robots.${ext}`,`src/app/api/universalcms/settings/route.${ext}`,`src/app/api/universalcms/page/route.${ext}`,`src/app/sitemap.xml/route.${ext}`,`src/app/robots.txt/route.${ext}`])]) if (await ordinary(root,name) !== null) fail('An existing settings, sitemap or robots route needs manual composition. It was preserved.');
204
+ const observations = [...ownership.files, ...(demo?.files ?? [])];
205
+ for (const row of registry.routes) {
206
+ const frozen = await readFrozenContent(root, row);
207
+ observations.push(...Object.entries(frozen.texts).map(([name,content]) => ({path:row[name + 'File'],content,state:'unchanged'})));
208
+ }
209
+ for (const name of ['package.json', CONTENT_BINDINGS_FILE, '.universalcms/publication.config.json', '.universalcms/source.config.json', ...['js','mjs','cjs','ts'].map(ext => `next.config.${ext}`)]) {
210
+ const content = await ordinary(root, name); if (content !== null) observations.push({path:name,content,state:'unchanged'});
211
+ }
212
+ if (existing) {
213
+ let previous; try { previous = JSON.parse(previousText); } catch { fail('Invalid website navigation manifest.'); }
214
+ if (Object.keys(previous).sort().join() !== 'adminOrigin,apiOrigin,environmentId,files,profile,sections,siteId,version,websiteOrigin' || previous.version !== 1 || previous.profile !== REVIEWED_ICECREAM_PROFILE || Object.entries(connection).some(([name,value]) => previous[name] !== value) || JSON.stringify(previous.sections) !== JSON.stringify(sections) || !Array.isArray(previous.files) || JSON.stringify(previous.files.map(row => row?.path).sort()) !== JSON.stringify(outputs) || previous.files.some(row => !row || Object.keys(row).sort().join() !== 'path,sha256' || !/^[a-f0-9]{64}$/.test(row.sha256))) fail('Website navigation scope or manifest is invalid.');
215
+ for (const row of previous.files) { const content = await ordinary(root, row.path); if (content === null || contentDigest(content) !== row.sha256) fail('A website navigation adapter was customized. Preserve it and review the shared integration manually.'); observations.push({path:row.path,content,state:'unchanged'}); }
216
+ observations.push({path:FILE,content:previousText,state:'unchanged'});
217
+ return {root, files:[...new Map(observations.map(file => [file.path,file])).values()], sections, config};
218
+ }
219
+ const originals = await reviewedNavigationSources(root);
220
+ const templates = {...reviewedCommerceNavigationTemplates(originals), 'src/proxy.ts':nextWebsiteProxyTemplate('src/app',{installedSections:sections}), 'src/cms/website-navigation.server.ts':serverTemplate(), 'src/cms/website-navigation.css':css, 'src/cms/NativeSettings.tsx':nativeSettingsTemplate(), 'src/app/[...cmsPath]/page.tsx':catchallTemplate()};
221
+ const nextRegistry = JSON.parse(json(registry));
222
+ for (const row of nextRegistry.routes) {
223
+ const current = await ordinary(root,row.sourceFile);
224
+ if (current !== nextContentPageTemplate(row)) fail('An original content wrapper was customized. Preserve it and review the settings integration.');
225
+ originals[row.sourceFile] = current;
226
+ templates[row.sourceFile] = originalPageTemplate(row);
227
+ row.sourceHash = contentDigest(templates[row.sourceFile]);
228
+ }
229
+ originals[CONTENT_BINDINGS_FILE] = await ordinary(root,CONTENT_BINDINGS_FILE);
230
+ templates[CONTENT_BINDINGS_FILE] = json(nextRegistry);
231
+ const files = [];
232
+ for (const [name,content] of Object.entries(templates)) {
233
+ const previous = await ordinary(root,name), expected = name === 'src/proxy.ts' ? nextPublicationProxyTemplate('src/app') : originals[name] ?? null;
234
+ if (previous !== expected) fail('A navigation adapter path already contains a custom file. Existing files were preserved.');
235
+ files.push({path:name,content,state:previous === null?'create':'update',...(previous === null?{}:{previous})});
236
+ }
237
+ // Create the supporting modules before editing native entrypoints. The final
238
+ // manifest is written last and is never evidence of a partially saved adapter.
239
+ files.sort((left,right) => Number(left.state !== 'create') - Number(right.state !== 'create'));
240
+ files.push({path:FILE,content:json({version:1,profile:REVIEWED_ICECREAM_PROFILE,...connection,sections,files:files.map(file => ({path:file.path,sha256:contentDigest(file.content)}))}),state:'create'});
241
+ const writes = new Set(files.map(file => file.path));
242
+ return {root,files:[...[...new Map(observations.map(file => [file.path,file])).values()].filter(file => !writes.has(file.path)),...files],sections,config};
243
+ }
@@ -0,0 +1,118 @@
1
+ /** Read an explicit local review without executing source or reading secrets. */
2
+ import { lstat, readdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import ts from './vendor/typescript.cjs';
5
+ import { createHash } from 'node:crypto';
6
+ import { projectRoot, readProjectFile, SetupError } from './project-files.mjs';
7
+ import { inspectSourceRoutes } from './source-routes.mjs';
8
+ import { REVIEWED_NEXT_NATIVE_PROFILE, validateNativeRoutes, isNativeReviewFilePath } from './native-routes.mjs';
9
+ export const NATIVE_ROUTES_FILE = '.universalcms/native.routes.json';
10
+ const digest = value => createHash('sha256').update(value).digest('hex');
11
+ const fail = message => { throw new SetupError(message); };
12
+ export async function inspectReviewedNativeOwnership(directory, { candidate } = {}) {
13
+ // The CLI can validate a prospective review before writing it. Runtime callers
14
+ // omit candidate and must still validate the exact installed ordinary file.
15
+ if (candidate !== undefined && (typeof candidate !== 'string' || Buffer.byteLength(candidate) > 128 * 1024)) fail('Invalid native ownership review candidate.');
16
+ const root = await projectRoot(directory), text = candidate ?? await readProjectFile(root, NATIVE_ROUTES_FILE);
17
+ if (text === null) return null;
18
+ if (candidate === undefined && (await lstat(path.join(root, NATIVE_ROUTES_FILE))).nlink !== 1) fail('Native ownership must use ordinary single-link files.');
19
+ let value;
20
+ try { value = JSON.parse(text); validateNativeRoutes(value); } catch { fail('The reviewed native ownership manifest is invalid. Preserve the files and review .universalcms/native.routes.json.'); }
21
+ if (Object.keys(value).sort().join() !== 'files,nativePaths,nativePatterns,profile,version' || value.version !== 1 || value.profile !== REVIEWED_NEXT_NATIVE_PROFILE ||
22
+ !Array.isArray(value.files) || !value.files.length || value.files.length > 200 || value.files.some(file => !file || Object.keys(file).sort().join() !== 'path,sha256' ||
23
+ !isNativeReviewFilePath(file.path) || !/^[a-f0-9]{64}$/.test(file.sha256)) ||
24
+ new Set(value.files.map(file => file.path)).size !== value.files.length) fail('Native ownership must list bounded reviewed source modules and their SHA-256 hashes.');
25
+ const observed = [];
26
+ for (const file of value.files) {
27
+ const content = await readProjectFile(root, file.path);
28
+ if (content === null || digest(content) !== file.sha256 || (await lstat(path.join(root, file.path))).nlink !== 1) fail('A reviewed native module changed or is linked. Review its ownership before setup or deployment.');
29
+ observed.push({ path: file.path, content, state: 'unchanged' });
30
+ }
31
+ const inventory = await inspectSourceRoutes(root), names = new Set(value.files.map(file => file.path));
32
+ const app = Object.values(inventory.files)[0].startsWith('src/app/') ? 'src/app' : 'app';
33
+ const pkg = JSON.parse(await readProjectFile(root, 'package.json'));
34
+ if (pkg.imports && Object.keys(pkg.imports).length) fail('Package import aliases need a separate native ownership review.');
35
+ let alias = false, configs = 0;
36
+ for (const filename of ['tsconfig.json','jsconfig.json']) {
37
+ const configText = await readProjectFile(root, filename);
38
+ if (configText === null) continue;
39
+ if (++configs > 1 || (await lstat(path.join(root, filename))).nlink !== 1) fail('Ambiguous or linked module resolution needs a separate native ownership review.');
40
+ const parsed = ts.parseConfigFileTextToJson(filename, configText), config = parsed.config;
41
+ const options = config?.compilerOptions ?? {}, expected = app.startsWith('src/') ? './src/*' : './*';
42
+ if (parsed.error || !config || config.extends || config.references || options.baseUrl !== undefined || options.rootDirs || options.moduleSuffixes ||
43
+ options.paths && (Object.keys(options.paths).join() !== '@/*' || JSON.stringify(options.paths['@/*']) !== JSON.stringify([expected]))) fail('Only conventional @/* source aliases are supported by native ownership. Review custom module resolution manually.');
44
+ alias = !!options.paths;
45
+ }
46
+ for (const extension of ['js','mjs','cjs','ts']) {
47
+ const config = await readProjectFile(root, `next.config.${extension}`);
48
+ if (config && /\b(?:webpack|resolveAlias|alias)\b/.test(config)) fail('Custom Next module aliases need a separate native ownership review.');
49
+ }
50
+ // The shared app root layout is validated by publication/settings setup.
51
+ // Route-specific boundary modules belong to the explicit native review.
52
+ const directories = new Set([...value.nativePaths, ...value.nativePatterns].flatMap(pathname => {
53
+ const result = []; let current = `${app}${pathname === '/' ? '' : pathname}`;
54
+ while (current !== app) { result.push(current); current = path.posix.dirname(current); }
55
+ return result;
56
+ }));
57
+ for (const directory of directories) for (const name of ['layout','template','loading','error','default','not-found']) for (const ext of ['js','jsx','ts','tsx']) {
58
+ const filename = `${directory}/${name}.${ext}`;
59
+ if (await readProjectFile(root, filename) !== null && !names.has(filename)) fail('Native route boundary modules must be included in the review.');
60
+ }
61
+ // Every local dependency of the reviewed modules is explicit too. Parse
62
+ // syntax only: a route review never executes application code.
63
+ for (const file of observed) {
64
+ if (!/\.[cm]?[jt]sx?$/.test(file.path)) continue;
65
+ const parsed = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, true, /x$/.test(file.path) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
66
+ if (parsed.parseDiagnostics.length) fail('A reviewed native source module cannot be parsed safely.');
67
+ const imports = [];
68
+ function visit(node) {
69
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) imports.push(node.moduleSpecifier.text);
70
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
71
+ if (node.arguments.length !== 1 || !ts.isStringLiteral(node.arguments[0])) fail('Dynamic native module loading needs manual ownership review.');
72
+ imports.push(node.arguments[0].text);
73
+ }
74
+ ts.forEachChild(node, visit);
75
+ }
76
+ visit(parsed);
77
+ for (const specifier of imports) {
78
+ if (specifier.startsWith('#') || specifier.startsWith('/') || specifier.includes('\\')) fail('Unknown local native import aliases need manual review.');
79
+ if (specifier.startsWith('@/') && !alias) fail('A native @/ import requires the conventional explicit source alias.');
80
+ if (!specifier.startsWith('.') && !specifier.startsWith('@/')) {
81
+ const packageName = specifier.startsWith('@') ? specifier.split('/').slice(0,2).join('/') : specifier.split('/')[0];
82
+ if (!specifier.startsWith('node:') && !Object.hasOwn(pkg.dependencies ?? {}, packageName) && !Object.hasOwn(pkg.devDependencies ?? {}, packageName)) fail('Unknown native import aliases need an explicit dependency or manual review.');
83
+ continue;
84
+ }
85
+ const base = specifier.startsWith('@/') ? (app.startsWith('src/') ? 'src/' : '') + specifier.slice(2) : path.posix.normalize(path.posix.join(path.posix.dirname(file.path), specifier));
86
+ const candidates = [base, ...['.ts','.tsx','.js','.jsx','.mjs','.cjs','.mts','.cts','.json','/index.ts','/index.tsx','/index.js','/index.jsx'].map(suffix => base + suffix)];
87
+ if (!candidates.some(name => names.has(name))) fail('Every local dependency of a native route must be explicitly included in its reviewed file hashes.');
88
+ }
89
+ }
90
+ for (const pathname of value.nativePaths) {
91
+ const file = inventory.files[pathname === '/' ? 'home' : pathname.slice(1)];
92
+ if (!file || !names.has(file) || !['js','jsx','ts','tsx'].some(ext => file === `${app}${pathname === '/' ? '' : pathname}/page.${ext}`)) fail('Each reviewed native path must identify an existing static page included in the file review.');
93
+ }
94
+ const dynamicDirectories = value.nativePatterns.map(pattern => `${app}${pattern}`);
95
+ let inspected = 0;
96
+ for (const directory of dynamicDirectories) {
97
+ if (!inventory.unresolvedPaths.includes(directory)) fail('Each reviewed native pattern must identify one existing final dynamic route. Route groups, parallel and catch-all routes need separate review.');
98
+ let pages = 0;
99
+ async function scan(current, depth = 0) {
100
+ if (depth > 12) fail('Native route review exceeded its directory budget.');
101
+ const stat = await lstat(path.join(root, current));
102
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail('Native route directories must be ordinary, without links.');
103
+ for (const item of await readdir(path.join(root, current), { withFileTypes: true })) {
104
+ if (++inspected > 4000 || item.isSymbolicLink()) fail('Native route review exceeds its budget or contains a link.');
105
+ const filename = `${current}/${item.name}`;
106
+ if (item.isDirectory()) { if (/[[\]@()]/.test(item.name)) fail('Nested dynamic, parallel or intercepted native routes need separate review.'); await scan(filename, depth + 1); }
107
+ else if (/^(?:page|route)\./.test(item.name)) {
108
+ if (current !== directory || !/^page\.[jt]sx?$/.test(item.name) || !names.has(filename)) fail('Unreviewed native route descendants or handlers cannot inherit a reviewed pattern.');
109
+ pages++;
110
+ } else if (/^(?:layout|template|loading|error|default|not-found)\.[jt]sx?$/.test(item.name) && !names.has(filename)) fail('Native route boundary modules must be included in the review.');
111
+ }
112
+ }
113
+ await scan(directory);
114
+ if (pages !== 1) fail('A reviewed native dynamic route must contain exactly one ordinary page module.');
115
+ }
116
+ return { profile: value.profile, nativePaths: value.nativePaths, nativePatterns: value.nativePatterns,
117
+ dynamicDirectories, files: observed, manifest: { path: NATIVE_ROUTES_FILE, content: text, state: 'unchanged' } };
118
+ }