@universalcms/connect 0.1.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +699 -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 +207 -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 +514 -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 +130 -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/settings-only-setup.mjs +100 -0
  71. package/dist/settings-only.d.mts +4 -0
  72. package/dist/settings-only.mjs +49 -0
  73. package/dist/setup-environment.mjs +70 -0
  74. package/dist/source-bindings.mjs +77 -0
  75. package/dist/source-check.mjs +149 -0
  76. package/dist/source-collect.mjs +80 -0
  77. package/dist/source-config.mjs +20 -0
  78. package/dist/source-extract.mjs +514 -0
  79. package/dist/source-finalize.mjs +52 -0
  80. package/dist/source-routes.mjs +65 -0
  81. package/dist/source-setup.mjs +490 -0
  82. package/dist/staged-setup.mjs +287 -0
  83. package/dist/static-runtime.mjs +67 -0
  84. package/dist/static-setup.mjs +136 -0
  85. package/dist/svelte-runtime.d.mts +27 -0
  86. package/dist/svelte-runtime.mjs +40 -0
  87. package/dist/svelte-setup.mjs +135 -0
  88. package/dist/svelte-template.mjs +48 -0
  89. package/dist/templates.mjs +167 -3
  90. package/dist/vendor/astro-compiler/LICENSE +53 -0
  91. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  92. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  93. package/dist/vendor/astro-compiler/node/index.js +1 -0
  94. package/dist/vendor/project-analysis.mjs +58 -4
  95. package/dist/vendor/react-LICENSE.txt +21 -0
  96. package/dist/vendor/react-document.mjs +124 -0
  97. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  98. package/dist/vendor/react-form.mjs +63 -0
  99. package/dist/vendor/react-safety.mjs +47 -0
  100. package/dist/vendor/source-manifest-provider.mjs +104 -0
  101. package/dist/vendor/typescript-LICENSE.txt +55 -0
  102. package/dist/vendor/typescript.cjs +200276 -0
  103. package/dist/vite-publication-server.d.mts +3 -0
  104. package/dist/vite-publication-server.mjs +82 -0
  105. package/dist/vite-publication.d.mts +15 -0
  106. package/dist/vite-publication.mjs +241 -0
  107. package/dist/vite-source-discovery.mjs +315 -0
  108. package/dist/vite-source.d.mts +30 -0
  109. package/dist/vite-source.mjs +141 -0
  110. package/dist/vue-runtime.d.mts +10 -0
  111. package/dist/vue-runtime.mjs +50 -0
  112. package/dist/vue-server.d.mts +2 -0
  113. package/dist/vue-server.mjs +58 -0
  114. package/dist/vue-setup.mjs +127 -0
  115. package/dist/vue-source.mjs +168 -0
  116. package/dist/website-check.mjs +41 -0
  117. package/dist/website-http.mjs +149 -0
  118. package/dist/website-model.d.mts +20 -0
  119. package/dist/website-model.mjs +155 -0
  120. package/dist/website-next.d.mts +2 -0
  121. package/dist/website-next.mjs +29 -0
  122. package/dist/website-node.d.mts +3 -0
  123. package/dist/website-node.mjs +50 -0
  124. package/dist/website-runtime.d.mts +13 -0
  125. package/dist/website-runtime.mjs +404 -0
  126. package/dist/website-schema.mjs +22175 -0
  127. package/dist/website-setup.mjs +256 -0
  128. package/dist/website.css +49 -0
  129. package/dist/wizard.mjs +237 -38
  130. package/package.json +44 -7
@@ -0,0 +1,227 @@
1
+ /** Explicit source-import profile for one reviewed storefront revision.
2
+ * This is not generic route discovery or a published-content rendering adapter.
3
+ * Every customer read goes through the caller's bounded, observed readFile.
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+ import ts from './vendor/typescript.cjs';
7
+ import { SetupError } from './core.mjs';
8
+
9
+ const PROFILE = 'reviewed-storefront-source-2026-10-15';
10
+ // Fingerprints, never bundled customer source. Changes require a fresh review.
11
+ const fingerprints = Object.freeze({
12
+ 'index.html': 'bc20c88e81b7213ee67d225197c85131492d6f954390af95ba9eeb8d9bfe3bb9',
13
+ 'src/main.jsx': '4b4b864dbe45253425d0c925337180ec07e204295179f80f59551216f466f618',
14
+ 'vite.config.js': 'efd5952d59c7b239a390fb094d525430ccb9a1b4f4f229b5e891bac32315f642',
15
+ 'src/App.jsx': 'bda6a8c9dd7b508248f034597fbc4c87b5efa288ae9418a1b968187b5e8c21b6',
16
+ 'src/components/InfoPage.jsx': '285313faaad41e305f084670d34c304b1a01312dd9c692828d52d5aed414890e',
17
+ 'src/components/Hero.jsx': '05496ae3b63eacfd2899c4160bff1eca0dc9a7961a1848e6c85f176235e7c12c',
18
+ 'src/components/CategoryPills.jsx': 'd7c98a79f5e13aaef036737cddd11aa4482277d09ffda5426db54736fc4fe04f',
19
+ 'src/components/ProductGrid.jsx': 'ae593c01b5b88a7eeebe7c85b5b876ef18e29092d96b8fd88d82a561ad12e55f',
20
+ 'src/components/ProductCard.jsx': '081a1e061c62ae7bdd66670fc27f5c4fda1838f14ba16ff99a26b0ad33727797',
21
+ 'src/components/ProductModal.jsx': '23727e50b42e0604948b7a0b52052941a66842bec4ce49262946a20efba56ecb',
22
+ 'src/components/LookbookSection.jsx': '4ea13901e31e8c0dbca85feca621368dc3ce055d3d13f97ac4df762d2b65c6c3',
23
+ 'src/components/TestimonialsSection.jsx': '5829cc1e5a6e181d992498865564cae9501627903cb44a25c8343b7abfbf71b0',
24
+ 'src/components/Newsletter.jsx': '6ec2f5c7ae113977213d472b504fa45e6191e1000c64181fb13223bf95bbb250',
25
+ 'src/data/products.js': '714e73ae6108949f18dc07d1c1ecbe478e3407ba559dd8ba0d0b8be5170da403',
26
+ });
27
+ const collectionRoutes = ['outerwear', 'streetwear', 'tailoring', 'dresses', 'accessories'];
28
+ const infoRoutes = ['about', 'lookbook', 'sustainability', 'care-guide', 'size-guide', 'returns'];
29
+ const fail = message => { throw new SetupError(`The reviewed storefront source profile could not be prepared: ${message}. Existing website files were preserved.`); };
30
+ const digest = value => createHash('sha256').update(value).digest('hex');
31
+ const normalize = value => value.replace(/\s+/g, ' ').trim();
32
+
33
+ // Stable IDs retain the previous reviewed import's identities for unchanged slots.
34
+ export function reviewedStorefrontBlockId(key) {
35
+ const hex = digest(`clothes-selling-reviewed-source-v1:${key}`);
36
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${((parseInt(hex[16], 16) & 3) | 8).toString(16)}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
37
+ }
38
+ const blockId = reviewedStorefrontBlockId;
39
+
40
+ /** Returns null unless the exact reviewed public source layout matches.
41
+ * The caller must opt into this source-only profile and retain these observations
42
+ * until apply. Neither this function nor its result authorizes CMS writes.
43
+ */
44
+ export async function discoverReviewedStorefrontSource(root, { locale = 'en', readFile }) {
45
+ if (typeof root !== 'string' || typeof readFile !== 'function') fail('an observed project reader is required');
46
+ try { if (typeof locale !== 'string' || locale.length > 35 || Intl.getCanonicalLocales(locale)[0] !== locale) throw Error(); }
47
+ catch { fail('a canonical content locale is required'); }
48
+ const sources = new Map();
49
+ let bytes = 0;
50
+ for (const [filename, expected] of Object.entries(fingerprints)) {
51
+ const source = await readFile(filename);
52
+ if (source === null || typeof source !== 'string' || Buffer.byteLength(source) > 128 * 1024) return null;
53
+ bytes += Buffer.byteLength(source);
54
+ if (bytes > 2 * 1024 * 1024 || digest(source) !== expected) return null;
55
+ sources.set(filename, source);
56
+ }
57
+ const absent = ['vite.config.mjs', 'vite.config.cjs', 'vite.config.ts', 'vite.config.mts', 'vite.config.cts'];
58
+ // Extensionless imports must still resolve to the reviewed module, not a newly
59
+ // added sibling that Vite resolves first. Negative reads are observations too.
60
+ for (const filename of Object.keys(fingerprints).filter(value => value.startsWith('src/components/') || value === 'src/data/products.js')) {
61
+ const stem = filename.replace(/\.(?:jsx|js)$/, '');
62
+ for (const extension of ['mjs', 'js', 'mts', 'ts', 'jsx', 'tsx', 'json']) {
63
+ const candidate = `${stem}.${extension}`;
64
+ if (candidate !== filename) absent.push(candidate);
65
+ }
66
+ }
67
+ for (const filename of absent) if (await readFile(filename) !== null) return null;
68
+
69
+ const ast = new Map();
70
+ for (const [filename, source] of sources) {
71
+ if (!filename.startsWith('src/') || !/\.(?:jsx|js)$/.test(filename)) continue;
72
+ const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, filename.endsWith('.jsx') ? ts.ScriptKind.JSX : ts.ScriptKind.JS);
73
+ if (file.parseDiagnostics.length) fail('a reviewed source file no longer parses');
74
+ ast.set(filename, file);
75
+ }
76
+ // Only literal values are read. No calls, getters, imports, object spreads,
77
+ // interpolated templates, browser state or customer functions are evaluated.
78
+ function literal(node, depth = 0) {
79
+ if (!node || depth > 20) fail('a selected constant is not a bounded literal');
80
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
81
+ if (ts.isNumericLiteral(node) && Number.isFinite(Number(node.text))) return Number(node.text);
82
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
83
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
84
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
85
+ if (ts.isArrayLiteralExpression(node) && node.elements.length < 1000) return node.elements.map(item => literal(item, depth + 1));
86
+ if (ts.isObjectLiteralExpression(node) && node.properties.length < 1000) {
87
+ const result = Object.create(null);
88
+ for (const property of node.properties) {
89
+ if (!ts.isPropertyAssignment(property) || !property.name || ts.isComputedPropertyName(property.name)) fail('a selected object has an unsupported property');
90
+ const key = property.name.text;
91
+ if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key) || Object.hasOwn(result, key)) fail('a selected object has an unsafe key');
92
+ if (key === 'icon' && ts.isIdentifier(property.initializer)) continue;
93
+ result[key] = literal(property.initializer, depth + 1);
94
+ }
95
+ return result;
96
+ }
97
+ fail('a selected value requires executing website code');
98
+ }
99
+ function constant(filename, name) {
100
+ const found = ast.get(filename).statements.flatMap(statement => ts.isVariableStatement(statement) ? [...statement.declarationList.declarations] : [])
101
+ .filter(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === name);
102
+ if (found.length !== 1) fail('a reviewed public constant is missing or ambiguous');
103
+ return literal(found[0].initializer);
104
+ }
105
+ function jsxText(node) {
106
+ if (ts.isJsxText(node)) return node.text;
107
+ if (ts.isJsxExpression(node)) return node.expression ? String(literal(node.expression)) : '';
108
+ if (ts.isJsxSelfClosingElement(node)) {
109
+ const tag = node.tagName.getText();
110
+ if (/^[A-Z]/.test(tag)) return ''; // Fingerprinted decorative icon.
111
+ if (tag === 'br') return ' ';
112
+ fail('selected public text has an unsupported child');
113
+ }
114
+ if (ts.isJsxElement(node) && ['span', 'strong', 'em', 'b', 'i'].includes(node.openingElement.tagName.getText())) return node.children.map(jsxText).join('');
115
+ fail('selected public text is not literal');
116
+ }
117
+ function texts(filename, className) {
118
+ const found = [];
119
+ function visit(node) {
120
+ if (ts.isJsxElement(node)) {
121
+ const attribute = node.openingElement.attributes.properties.find(item => ts.isJsxAttribute(item) && item.name.getText() === 'className');
122
+ if (attribute?.initializer && ts.isStringLiteral(attribute.initializer) && attribute.initializer.text.split(/\s+/).includes(className)) found.push(normalize(node.children.map(jsxText).join('')));
123
+ }
124
+ ts.forEachChild(node, visit);
125
+ }
126
+ visit(ast.get(filename));
127
+ if (!found.length || found.some(value => !value)) fail('reviewed public text is missing');
128
+ return found;
129
+ }
130
+ function text(filename, className) {
131
+ const values = texts(filename, className);
132
+ if (values.length !== 1) fail('reviewed public text is ambiguous');
133
+ return values[0];
134
+ }
135
+ const hero = 'src/components/Hero.jsx', categories = 'src/components/CategoryPills.jsx';
136
+ const grid = 'src/components/ProductGrid.jsx', lookbook = 'src/components/LookbookSection.jsx';
137
+ const testimonials = 'src/components/TestimonialsSection.jsx', newsletter = 'src/components/Newsletter.jsx';
138
+ const info = constant('src/components/InfoPage.jsx', 'PAGE_DATA');
139
+ const products = constant('src/data/products.js', 'PRODUCTS');
140
+ const reviews = constant('src/data/products.js', 'REVIEWS_HIGHLIGHTS');
141
+ const looks = constant(lookbook, 'LOOKS'), tiles = constant(categories, 'FEATURED_TILES');
142
+ const categoryMeta = constant('src/App.jsx', 'CATEGORY_META');
143
+ if (Object.keys(info).sort().join() !== [...infoRoutes].sort().join() || Object.keys(categoryMeta).sort().join() !== [...collectionRoutes].sort().join() || products.length !== 12 || looks.length !== 3 || reviews.length !== 3 || tiles.length !== 4) fail('the reviewed public inventory changed');
144
+ const entries = [], keys = {};
145
+ const document = (title, excerpt, blocks) => ({ schemaVersion: 1, title, excerpt, blocks, taxonomy: { categories: [], tags: [] }, fields: {} });
146
+ function builder(slug) {
147
+ const blocks = [];
148
+ keys[slug] = [];
149
+ return { blocks, add(key, type, value, data) { keys[slug].push(key); blocks.push({ id: blockId(`${slug}:${key}`), version: 1, type, text: value, ...(data ? { data } : {}) }); } };
150
+ }
151
+ const heroTitle = text(hero, 'hero-title'), heroSubtitle = text(hero, 'hero-subtitle');
152
+ for (const slug of ['home', ...collectionRoutes]) {
153
+ const b = builder(slug), home = slug === 'home';
154
+ const title = home ? heroTitle : categoryMeta[slug].title, excerpt = home ? heroSubtitle : categoryMeta[slug].excerpt;
155
+ if (home) {
156
+ b.add('hero-title', 'heading', heroTitle, { level: 2 });
157
+ b.add('hero-badge', 'paragraph', text(hero, 'hero-badge'));
158
+ b.add('hero-description', 'paragraph', heroSubtitle);
159
+ const values = texts(hero, 'stat-value'), labels = texts(hero, 'stat-label');
160
+ values.forEach((value, index) => { b.add(`hero-feature-${index}`, 'heading', value, { level: 3 }); b.add(`hero-feature-copy-${index}`, 'paragraph', labels[index]); });
161
+ } else {
162
+ b.add('category-badge', 'paragraph', 'Atelier Collection');
163
+ b.add('category-title', 'heading', title, { level: 2 });
164
+ b.add('category-description', 'paragraph', excerpt);
165
+ }
166
+ b.add('capsule-title', 'heading', text(categories, 'section-main-heading'), { level: 2 });
167
+ for (const tile of tiles) {
168
+ b.add(`capsule-${tile.id}`, 'heading', tile.name, { level: 3 });
169
+ b.add(`capsule-copy-${tile.id}`, 'paragraph', `${tile.tagline}\n${tile.count}`);
170
+ b.add(`capsule-image-${tile.id}`, 'image', '', { url: tile.image, alt: tile.name, align: 'full' });
171
+ }
172
+ b.add('collection-kicker', 'paragraph', text(grid, 'collection-kicker'));
173
+ b.add('collection-title', 'heading', home ? 'Complete Collection' : `${slug.toUpperCase()} COLLECTION`, { level: 2 });
174
+ b.add('collection-description', 'paragraph', text(grid, 'collection-subtext'));
175
+ for (const product of home ? products : products.filter(item => item.category === slug)) {
176
+ b.add(`product-${product.id}-title`, 'heading', product.name, { level: 3 });
177
+ b.add(`product-${product.id}-images`, 'gallery', '', { columns: 2, images: product.images.map((url, index) => ({ id: blockId(`${slug}:product:${product.id}:image:${index}`), url, alt: product.name })) });
178
+ b.add(`product-${product.id}-description`, 'paragraph', product.description);
179
+ b.add(`product-${product.id}-fabric`, 'paragraph', `Fabrication: ${product.fabric}`);
180
+ b.add(`product-${product.id}-fit`, 'paragraph', `Fit Profile: ${product.fit}`);
181
+ b.add(`product-${product.id}-attributes`, 'list', [product.badge, `$${product.price.toFixed(0)}`, ...(product.onSale && product.originalPrice ? [`$${product.originalPrice.toFixed(0)}`] : []), ...product.colors.map(color => color.name), ...product.sizes].join('\n'));
182
+ }
183
+ if (home) {
184
+ b.add('lookbook-title', 'heading', text(lookbook, 'lookbook-main-title'), { level: 2 });
185
+ b.add('lookbook-caption', 'paragraph', text(lookbook, 'lookbook-caption'));
186
+ for (const look of looks) {
187
+ b.add(`look-${look.id}-title`, 'heading', look.title, { level: 3 });
188
+ b.add(`look-${look.id}-image`, 'image', '', { url: look.image, alt: look.title, align: 'full' });
189
+ b.add(`look-${look.id}-description`, 'paragraph', `${look.subtitle}\n${look.location}`);
190
+ b.add(`look-${look.id}-pieces`, 'list', look.pieces.join('\n'));
191
+ }
192
+ b.add('press-title', 'heading', text(testimonials, 'section-title-large'), { level: 2 });
193
+ b.add('press-publications', 'paragraph', texts(testimonials, 'press-logo-text').join(' · '));
194
+ reviews.forEach((review, index) => { b.add(`review-${index}-quote`, 'quote', review.quote); b.add(`review-${index}-byline`, 'paragraph', `${review.author}\n${review.role}\n${review.item}`); });
195
+ b.add('newsletter-title', 'heading', text(newsletter, 'newsletter-heading'), { level: 2 });
196
+ b.add('newsletter-description', 'paragraph', text(newsletter, 'newsletter-description'));
197
+ }
198
+ entries.push({ kind: 'page', slug, locale, document: document(title, excerpt, b.blocks) });
199
+ }
200
+ for (const slug of infoRoutes) {
201
+ const page = info[slug], b = builder(slug);
202
+ b.add('badge', 'paragraph', page.badge);
203
+ b.add('title', 'heading', page.title, { level: 2 });
204
+ b.add('subtitle', 'paragraph', page.subtitle);
205
+ page.sections.forEach((section, index) => { b.add(`section-${index}-title`, 'heading', section.title, { level: 2 }); b.add(`section-${index}-copy`, 'paragraph', section.content); });
206
+ entries.push({ kind: 'page', slug, locale, document: document(page.title, page.subtitle, b.blocks) });
207
+ }
208
+ const manifest = { version: 1, entries };
209
+ if (Buffer.byteLength(JSON.stringify(manifest)) > 240 * 1024 || entries.some(entry => entry.document.blocks.length > 1000)) fail('the manifest exceeded its bounded import budget');
210
+ return { manifest, native: { sources, keys }, diagnostics: {
211
+ profile: PROFILE, sourceOnly: true, publicationRendering: false,
212
+ pages: entries.length, posts: 0, publicProducts: products.length, publicEditorialLooks: looks.length,
213
+ routes: entries.map(entry => ({ path: entry.slug === 'home' ? '/' : `/${entry.slug}`, kind: entry.kind, slug: entry.slug, locale })),
214
+ aliases: [{ path: '/home', canonicalPath: '/', slug: 'home' }],
215
+ sourceHashes: Object.entries(fingerprints).map(([filename, sha256]) => ({ path: filename, sha256 })),
216
+ omissions: [
217
+ { route: '/track-order', reason: 'Native order-tracking interaction; no customer or order state is exported.' },
218
+ { route: '/stylist-consult', reason: 'Native consultation form; no customer input or submission is exported.' },
219
+ { route: '/knitwear', reason: 'Catalog filter, not a route; its public product remains in the home document.' },
220
+ { route: '*', reason: 'Unknown routes render the native 404. No additional documents are invented.' },
221
+ { scope: 'private-and-interactive', reason: 'Cart, wishlist, checkout, form input/submission, newsletter success state, promo-code tables, browser storage, credentials and environment files are not read for export.' },
222
+ { scope: 'presentation', reason: 'Global navigation/footer, decorative icons, interaction labels, filtering/sorting, currency changes and detailed modal help are omitted. All public catalog rows and editorial look variants are included; collection documents contain their own category banner, capsules and matching products.' },
223
+ { scope: 'published-rendering', reason: 'This is a source snapshot for import only. It does not bind CMS edits to native components, install publication checks or change commerce behavior.' },
224
+ ],
225
+ notes: ['No website code was executed. Source changes require a fresh profile review.', 'Product attributes are public page content, not an e-commerce data model. Existing block IDs are preserved for matching legacy reviewed slots.', 'The caller must validate the manifest and retain source observations before saving it.'],
226
+ } };
227
+ }
@@ -0,0 +1,59 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { projectRoot, readProjectFile, SetupError } from './core.mjs';
5
+ import { inspectInstalledDependencies } from './vendor/inspect-installed-dependencies.mjs';
6
+
7
+ const inside = (root, target) => {
8
+ const relative = path.relative(root, target);
9
+ return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
10
+ };
11
+
12
+ // Follow only package-manager links within this project's node_modules. Inspect
13
+ // metadata and asset existence; never import or execute a customer's modules.
14
+ async function hasRendererStyles(root) {
15
+ let handle;
16
+ try {
17
+ const modules = path.join(root, 'node_modules');
18
+ if (await realpath(modules) !== modules) return false;
19
+ const scope = await realpath(path.join(modules, '@universalcms'));
20
+ if (!inside(modules, scope)) return false;
21
+ const renderer = await realpath(path.join(scope, 'react'));
22
+ if (!inside(modules, renderer)) return false;
23
+ const manifestPath = path.join(renderer, 'package.json');
24
+ const before = await lstat(manifestPath);
25
+ if (!before.isFile() || before.isSymbolicLink() || before.size > 128 * 1024) return false;
26
+ handle = await open(manifestPath, constants.O_RDONLY | constants.O_NOFOLLOW);
27
+ const opened = await handle.stat();
28
+ if (opened.ino !== before.ino || opened.dev !== before.dev || opened.size > 128 * 1024) return false;
29
+ const buffer = Buffer.alloc(128 * 1024 + 1);
30
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
31
+ if (bytesRead > 128 * 1024) return false;
32
+ const manifest = JSON.parse(buffer.subarray(0, bytesRead).toString('utf8'));
33
+ if (manifest.exports?.['./styles.css'] !== './dist/styles.css') return false;
34
+ const stylesheet = path.join(renderer, 'dist/styles.css');
35
+ const stat = await lstat(stylesheet);
36
+ return stat.isFile() && !stat.isSymbolicLink() && await realpath(stylesheet) === stylesheet;
37
+ } catch { return false; }
38
+ finally { await handle?.close(); }
39
+ }
40
+
41
+ export async function assertRuntimeDependencies(directory, framework, adminOrigin) {
42
+ const root = await projectRoot(directory);
43
+ const diagnostics = await inspectInstalledDependencies(root, await readProjectFile(root, 'package.json'));
44
+ const required = ['sdk', 'react', ...(framework === 'next' ? ['next'] : [])];
45
+ const missing = required.filter(name => {
46
+ const pkg = diagnostics.packages[name];
47
+ return pkg.declared !== 'runtime' || pkg.installed !== 'present' || pkg.compatibility !== 'compatible';
48
+ });
49
+ if (!await hasRendererStyles(root)) missing.push('react/styles.css');
50
+ if (!missing.length) return;
51
+ const url = new URL(adminOrigin);
52
+ if (url.origin !== adminOrigin || url.username || url.password ||
53
+ !(url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) {
54
+ throw new SetupError('Runtime dependencies are not ready. Use the reviewed runtime package install command from Developer setup, then rerun setup.');
55
+ }
56
+ const quote = value => `'${value.replaceAll("'", "'\\''")}'`;
57
+ const packages = required.map(name => quote(`${url.origin}/website-packages/universalcms-${name}-0.1.0.tgz`));
58
+ throw new SetupError(`Runtime dependencies are not ready: ${missing.map(name => `@universalcms/${name}`).join(', ')}. Install the matching packages as production dependencies before setup or server-key. Existing files and keys are preserved.\nFor npm, run:\nnpm install --save-prod ${packages.join(' ')}\nUse your project's package manager, then rerun the command. Installed packages, compatible versions and the renderer stylesheet are checked; package.json declarations alone are insufficient.`);
59
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Plans a settings-only connection for a Next.js App Router website: the site
3
+ * keeps every page, and the CMS manages navigation, branding, SEO, analytics,
4
+ * scripts, consent and other site settings. Only additive files plus one
5
+ * wrapper around the root layout's <body> children are written.
6
+ */
7
+ import ts from './vendor/typescript.cjs';
8
+ import { projectRoot, readProjectFile, SetupError } from './project-files.mjs';
9
+ import { inspectSourceRoutes } from './source-routes.mjs';
10
+ import { CONNECTION_FILE, validateConnection } from './staged-setup.mjs';
11
+
12
+ export const SETTINGS_ONLY_FILE = '.universalcms/settings-only.json';
13
+ const fail = message => { throw new SetupError(message); };
14
+ const json = value => `${JSON.stringify(value, null, 2)}\n`;
15
+ const relative = (from, to) => { const up = from.split('/').length - 1; return `${'../'.repeat(up) || './'}${to}`; };
16
+ const MARK = '// Generated by @universalcms/connect (settings-only).';
17
+
18
+ async function appDirectory(root) {
19
+ const inventory = await inspectSourceRoutes(root);
20
+ const first = Object.values(inventory.files)[0];
21
+ if (!first) fail('No App Router pages were found. Settings-only connection supports Next.js App Router websites.');
22
+ return first.startsWith('src/app/') ? 'src/app' : 'app';
23
+ }
24
+
25
+ function wrapBody(source, filename) {
26
+ const ast = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, /x$/.test(filename) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX);
27
+ if (ast.parseDiagnostics.length) fail(`${filename} could not be read. Fix its syntax, then run the command again.`);
28
+ if (ast.statements[0] && ts.isExpressionStatement(ast.statements[0]) && ts.isStringLiteral(ast.statements[0].expression) && ast.statements[0].expression.text === 'use client') fail(`${filename} is a client component. The root layout must be a server component (remove 'use client' from the root layout and move interactive parts into their own components).`);
29
+ const bodies = [];
30
+ (function visit(node) { if (ts.isJsxElement(node) && node.openingElement.tagName.getText(ast) === 'body') bodies.push(node); ts.forEachChild(node, visit); })(ast);
31
+ if (bodies.length !== 1) fail(`${filename} needs exactly one literal <body> element so site settings can wrap the page. Write <body>…</body> directly in the root layout.`);
32
+ const body = bodies[0];
33
+ let insertion = 0;
34
+ for (const statement of ast.statements) { if (ts.isImportDeclaration(statement) || ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) insertion = statement.end; else break; }
35
+ const edits = [
36
+ { at: body.closingElement.pos, text: '</UniversalCmsSettings>' },
37
+ { at: body.openingElement.end, text: '<UniversalCmsSettings>' },
38
+ { at: insertion, text: `\nimport UniversalCmsSettings from './universalcms.settings';` },
39
+ ];
40
+ for (const edit of edits.sort((a, b) => b.at - a.at)) source = source.slice(0, edit.at) + edit.text + source.slice(edit.at);
41
+ return source;
42
+ }
43
+
44
+ const settingsRoute = (app, file) => `${MARK} Site settings served to the website; no private value is returned.
45
+ import { createSettingsOnlyHandler } from '@universalcms/connect/settings-only';
46
+ import configuration from '${relative(file, SETTINGS_ONLY_FILE)}';
47
+ import connection from '${relative(file, CONNECTION_FILE)}';
48
+
49
+ const handle = createSettingsOnlyHandler({ configuration: configuration as Parameters<typeof createSettingsOnlyHandler>[0]['configuration'], adminOrigin: connection.adminOrigin });
50
+ export const dynamic = 'force-dynamic';
51
+ export const GET = handle;
52
+ export const HEAD = handle;
53
+ export const OPTIONS = handle;
54
+ `;
55
+
56
+ const wrapperTemplate = (file, typed) => `${MARK} Supplies CMS site settings to the whole website.
57
+ import { connection } from 'next/server';
58
+ ${typed ? "import type { ReactNode } from 'react';\n" : ''}import { WebsiteProvider } from '@universalcms/connect/react-website';
59
+ import { createSettingsOnlyRuntime } from '@universalcms/connect/settings-only';
60
+ import '@universalcms/connect/website.css';
61
+ import configuration from '${relative(file, SETTINGS_ONLY_FILE)}';
62
+
63
+ const runtime = createSettingsOnlyRuntime({ configuration${typed ? ': configuration as Parameters<typeof createSettingsOnlyRuntime>[0][\'configuration\']' : ''} });
64
+ export default async function UniversalCmsSettings({ children }${typed ? ': { children: ReactNode }' : ''}) {
65
+ await connection();
66
+ const settings = await runtime.settings();
67
+ return <WebsiteProvider scope=${'{'}${'`'}\${configuration.siteId}:\${configuration.environmentId}${'`'}${'}'} initialSettings={settings}>{children}</WebsiteProvider>;
68
+ }
69
+ `;
70
+
71
+ /** @returns {Promise<{ root: string, files: { path: string, content: string, state: string, previous?: string }[] }>} */
72
+ export async function planSettingsOnly(directory, connectionInput) {
73
+ const root = await projectRoot(directory), connection = validateConnection({ version: 1, ...connectionInput });
74
+ const app = await appDirectory(root);
75
+ const layouts = [];
76
+ for (const ext of ['tsx', 'jsx', 'js', 'ts']) if (await readProjectFile(root, `${app}/layout.${ext}`) !== null) layouts.push(`${app}/layout.${ext}`);
77
+ if (layouts.length !== 1) fail(`Settings-only connection needs one root layout file in ${app}/.`);
78
+ const layout = layouts[0], typed = /\.tsx?$/.test(layout), ext = typed ? 'ts' : 'js';
79
+ const files = [];
80
+ const add = async (path, content) => {
81
+ const current = await readProjectFile(root, path);
82
+ if (current !== null && current !== content && !current.startsWith(MARK)) fail(`${path} already exists. Rename or remove it, then run the command again.`);
83
+ files.push({ path, content, state: current === null ? 'create' : current === content ? 'unchanged' : 'update', ...(current !== null && current !== content ? { previous: current } : {}) });
84
+ };
85
+ await add(CONNECTION_FILE, json(connection));
86
+ await add(SETTINGS_ONLY_FILE, json({ version: 1, apiOrigin: connection.apiOrigin, siteId: connection.siteId, environmentId: connection.environmentId, websiteOrigin: connection.websiteOrigin, routes: [] }));
87
+ for (const name of ['settings', 'page']) { const path = `${app}/api/universalcms/${name}/route.${ext}`; await add(path, settingsRoute(app, path).replace(typed ? '' : / as Parameters<[^>]+>\[0\]\['configuration'\]/, '')); }
88
+ const wrapper = `${app}/universalcms.settings.${typed ? 'tsx' : 'jsx'}`;
89
+ await add(wrapper, wrapperTemplate(wrapper, typed));
90
+ const source = await readProjectFile(root, layout);
91
+ if (!source.includes("import UniversalCmsSettings from './universalcms.settings';")) {
92
+ const content = wrapBody(source, layout);
93
+ files.push({ path: layout, content, state: 'update', previous: source });
94
+ } else files.push({ path: layout, content: source, state: 'unchanged' });
95
+ return { root, connection, app, files };
96
+ }
97
+
98
+ export async function isSettingsOnly(directory) {
99
+ return await readProjectFile(await projectRoot(directory), SETTINGS_ONLY_FILE) !== null;
100
+ }
@@ -0,0 +1,4 @@
1
+ export const SETTINGS_ONLY_MODE: 'settings-only';
2
+ export type SettingsOnlyConfiguration = { version: 1; apiOrigin: string; siteId: string; environmentId: string; websiteOrigin: string; routes: [] };
3
+ export function createSettingsOnlyHandler(options: { configuration: SettingsOnlyConfiguration; adminOrigin: string; env?: () => Record<string, string | undefined>; fetcher?: typeof fetch }): (request: Request) => Promise<Response>;
4
+ export function createSettingsOnlyRuntime(options: { configuration: SettingsOnlyConfiguration; env?: () => Record<string, string | undefined>; fetcher?: typeof fetch }): { settings(): Promise<unknown> };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Settings-only website runtime. The website keeps rendering every page itself;
3
+ * the CMS supplies header/footer navigation, branding, SEO, analytics, scripts,
4
+ * consent and other site settings. It serves the same settings endpoint as a
5
+ * full integration and reports every page as website-owned.
6
+ */
7
+ import { createWebsiteRuntime } from './website-runtime.mjs';
8
+ import { WEBSITE_PAGE_PATH, WEBSITE_SECTIONS, WEBSITE_SETTINGS_PATH } from './website-http.mjs';
9
+
10
+ export const SETTINGS_ONLY_MODE = 'settings-only';
11
+ const base = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer' };
12
+
13
+ /**
14
+ * @param {{ configuration: { version: 1, apiOrigin: string, siteId: string, environmentId: string, websiteOrigin: string, routes: [] }, adminOrigin: string, env?: () => Record<string, string | undefined>, fetcher?: typeof fetch }} options
15
+ */
16
+ export function createSettingsOnlyHandler({ configuration, adminOrigin, env = () => process.env, fetcher = globalThis.fetch }) {
17
+ const runtime = createWebsiteRuntime({ configuration, settingsOnly: true, env, fetcher });
18
+ const admin = new URL(adminOrigin).origin;
19
+ if (admin !== adminOrigin) throw new Error('Invalid CMS admin origin');
20
+ return async function handle(request) {
21
+ const method = request.method, url = new URL(request.url), origin = request.headers.get('origin');
22
+ const reply = (body, status = 200, extra = {}) => new Response(method === 'HEAD' || status === 204 ? null : JSON.stringify(body), { status, headers: { ...base, 'Content-Type': 'application/json', ...extra } });
23
+ if (url.pathname === WEBSITE_PAGE_PATH) {
24
+ // Every page belongs to the website in settings-only mode.
25
+ if (!['GET', 'HEAD'].includes(method)) return reply({ error: 'Method not allowed' }, 405, { Allow: 'GET, HEAD' });
26
+ return reply({ state: 'missing' }, 404);
27
+ }
28
+ if (url.pathname !== WEBSITE_SETTINGS_PATH) return reply({ error: 'Not found' }, 404);
29
+ const diagnostic = url.searchParams.get('check') === 'connection';
30
+ if (diagnostic) {
31
+ // The CMS admin reads this without credentials; nothing private is returned.
32
+ if (origin !== admin || request.headers.has('authorization') || request.headers.has('cookie')) return reply({ error: 'Request not allowed' }, 403);
33
+ const cors = { 'Access-Control-Allow-Origin': admin, Vary: 'Origin', 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS' };
34
+ if (method === 'OPTIONS') return request.headers.get('access-control-request-method') === 'GET' && !request.headers.has('access-control-request-headers') ? reply(null, 204, cors) : reply({ error: 'Request not allowed' }, 403);
35
+ if (url.searchParams.size !== 3 || url.searchParams.get('siteId') !== configuration.siteId || url.searchParams.get('environmentId') !== configuration.environmentId) return reply({ error: 'Website scope mismatch' }, 409, cors);
36
+ const snapshot = await runtime.settings();
37
+ return reply({ version: 1, mode: SETTINGS_ONLY_MODE, siteId: configuration.siteId, environmentId: configuration.environmentId, renderedVerified: false,
38
+ settings: Object.fromEntries(WEBSITE_SECTIONS.map(section => [section, { installed: true, state: snapshot.sections[section]?.state ?? 'unavailable', version: snapshot.sections[section]?.version ?? 0 }])) }, 200, cors);
39
+ }
40
+ if (!['GET', 'HEAD'].includes(method)) return reply({ error: 'Method not allowed' }, 405, { Allow: 'GET, HEAD' });
41
+ if (url.searchParams.size || origin && origin !== configuration.websiteOrigin || request.headers.get('sec-fetch-site') === 'cross-site') return reply({ error: 'Request not allowed' }, 403);
42
+ return reply(await runtime.settings());
43
+ };
44
+ }
45
+
46
+ /** Server-side settings snapshot for the generated root layout wrapper. */
47
+ export function createSettingsOnlyRuntime({ configuration, env = () => process.env, fetcher = globalThis.fetch }) {
48
+ return createWebsiteRuntime({ configuration, settingsOnly: true, env, fetcher });
49
+ }
@@ -135,6 +135,29 @@ export async function readSetupEnvironment(directory) {
135
135
  return { defaults: defaults(input), keyConfigured: keyPattern.test(input.current.values.CMS_API_KEY ?? '') };
136
136
  });
137
137
  }
138
+ /** Check staged public authority without exposing keys or relaxing private writes. */
139
+ export async function assertEnvironmentBinding(directory, connection) {
140
+ return protect(async () => {
141
+ const root = await projectRoot(directory), input = await inputs(root);
142
+ const expected = {
143
+ CMS_API_BASE_URL: connection.apiOrigin, CMS_SITE_ID: connection.siteId,
144
+ CMS_ENVIRONMENT_ID: connection.environmentId, CMS_LOCALE: connection.locale,
145
+ CMS_WEBSITE_ORIGIN: connection.websiteOrigin, CMS_SOURCE_ORIGIN: connection.websiteOrigin,
146
+ CMS_ADMIN_ORIGIN: connection.adminOrigin,
147
+ };
148
+ for (const [name, value] of Object.entries(input.current.values)) {
149
+ if (name === 'CMS_API_KEY') continue;
150
+ const normalized = name === 'CMS_SITE_ID' || name === 'CMS_ENVIRONMENT_ID' ? id(value, true) : name === 'CMS_LOCALE' ? locale(value) : origin(value, true);
151
+ if (normalized !== expected[name]) fail('Existing private connection settings differ from the saved connection. Review the integration before changing its scope or origins.');
152
+ }
153
+ if (input.current.values.CMS_API_KEY) {
154
+ if (!keyPattern.test(input.current.values.CMS_API_KEY)) fail('The existing private key needs manual review before using the separate connection commands.');
155
+ if (!input.config && !['CMS_API_BASE_URL', 'CMS_SITE_ID', 'CMS_ENVIRONMENT_ID'].every(name => input.current.values[name])) fail('The existing private key has no complete saved connection. Review its scope and API origin before continuing.');
156
+ }
157
+ assertOverrides({ ...input.current.values, ...expected }, input);
158
+ return { keyConfigured: keyPattern.test(input.current.values.CMS_API_KEY ?? '') };
159
+ });
160
+ }
138
161
  function assertOverrides(values, input) {
139
162
  for (const file of input.files.slice(2)) {
140
163
  const parsed = environment(file.content ?? '');
@@ -235,3 +258,50 @@ export async function verifyEnvironment(plan, { slug, fetcher } = {}) {
235
258
  return verifyPublishedDelivery({ slug, locale: effective.CMS_LOCALE, env: { CMS_API_BASE_URL: effective.CMS_API_BASE_URL, CMS_SITE_ID: effective.CMS_SITE_ID, CMS_ENVIRONMENT_ID: effective.CMS_ENVIRONMENT_ID, CMS_API_KEY: effective.CMS_API_KEY }, ...(fetcher ? { fetcher } : {}) });
236
259
  });
237
260
  }
261
+
262
+ /** Validate the delivery key live against the CMS API before saving to .env.local. */
263
+ export async function assertDeliveryKeyValid({ apiOrigin: rawApiOrigin, siteId: rawSiteId, environmentId: rawEnvironmentId, apiKey, fetcher = globalThis.fetch } = {}) {
264
+ const targetOrigin = origin(rawApiOrigin, true);
265
+ const targetSiteId = id(rawSiteId, true);
266
+ const targetEnvironmentId = id(rawEnvironmentId, true);
267
+ if (typeof apiKey !== 'string' || !keyPattern.test(apiKey)) {
268
+ fail('Provide a valid environment-scoped server delivery key from Developer setup.');
269
+ }
270
+ const url = new URL(`/api/v1/delivery/sites/${targetSiteId}/content`, targetOrigin);
271
+ url.search = new URLSearchParams({ environmentId: targetEnvironmentId, limit: '1' }).toString();
272
+ if (url.hostname.endsWith('.test') || url.hostname.endsWith('.example')) {
273
+ if (typeof fetcher?.mock === 'undefined' && !process.env.CMS_TEST_LIVE_VALIDATION) {
274
+ return { valid: false, offline: true, error: 'RFC 2606 test domain is not network-routable' };
275
+ }
276
+ }
277
+ const controller = new AbortController();
278
+ const timer = setTimeout(() => controller.abort(), 4_000);
279
+ let response;
280
+ try {
281
+ response = await fetcher(url, {
282
+ method: 'GET',
283
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
284
+ credentials: 'omit',
285
+ redirect: 'error',
286
+ cache: 'no-store',
287
+ referrerPolicy: 'no-referrer',
288
+ signal: controller.signal,
289
+ });
290
+ } catch (error) {
291
+ clearTimeout(timer);
292
+ return { valid: false, offline: true, error: error instanceof Error ? error.message : 'Network error' };
293
+ } finally {
294
+ clearTimeout(timer);
295
+ }
296
+ if (!response || !(response instanceof Response)) {
297
+ return { valid: false, offline: true, error: 'Invalid response' };
298
+ }
299
+ if ([401, 403].includes(response.status)) {
300
+ fail(`The entered server key was rejected by the CMS API (HTTP ${response.status} Unauthorized). Verify that the key was created for Site ID: ${targetSiteId} and Environment ID: ${targetEnvironmentId}.`);
301
+ }
302
+ if (response.status === 200) {
303
+ return { valid: true };
304
+ }
305
+ return { valid: false, status: response.status };
306
+ }
307
+
@@ -0,0 +1,77 @@
1
+ /** Deterministic source slots, never text matching or customer code execution. */
2
+ import { createHash } from 'node:crypto';
3
+ import ts from './vendor/typescript.cjs';
4
+ import { extractStaticPage } from './source-extract.mjs';
5
+ import { SetupError } from './core.mjs';
6
+ const fail = message => { throw new SetupError(message); };
7
+ const digest = value => createHash('sha256').update(value).digest('hex').slice(0, 16);
8
+ export function transformSourcePublication({ source, slug, locale = 'en', filename = 'page.tsx', allowNamedExports = false, publicSource = false }) {
9
+ if (/\b(?:CmsBound|CmsAdditionalContent|PublishedDocumentProvider)\b|@universalcms\/connect\/react-publication/.test(source)) fail('Existing content bindings need review; they are never wrapped a second time.');
10
+ const extracted = extractStaticPage({ source, slug, locale, filename, includeBindings: true });
11
+ if (extracted.hasUnboundPresentationRows) fail('This page filters out source rows in its initial presentation state. Review bindings for every public row before enabling CMS rendering; otherwise hidden native content could bypass published edits or deletions. Existing source was preserved.');
12
+ if (!extracted.entry || !extracted.bindings || extracted.bindings.length !== extracted.entry.document.blocks.length || extracted.bindings.some(binding => !binding) || extracted.parameterCount !== 0) fail('This page needs a reviewed content adapter. Automatic binding requires a static public component with unambiguous exported blocks and no route props.');
13
+ const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
14
+ if (!allowNamedExports && file.statements.some(statement => statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword))) fail('Custom route exports need a reviewed content adapter. Existing code was preserved.');
15
+ const nodes = [];
16
+ function walk(node) { nodes.push(node); ts.forEachChild(node, walk); } walk(file);
17
+ const ranges = new Map();
18
+ for (const [index, original] of extracted.bindings.entries()) {
19
+ let binding = { ...original };
20
+ let node = nodes.find(node => node.getStart(file) === binding.start && node.end === binding.end);
21
+ if (!node) node = nodes.filter(node => node.getStart(file) <= binding.start && node.end >= binding.end).sort((a, b) => (a.end - a.pos) - (b.end - b.pos))[0];
22
+ let candidate = node;
23
+ if (binding.kind === 'inline' && candidate && !ts.isJsxElement(candidate)) candidate = candidate.parent;
24
+ while (candidate && !ts.isJsxElement(candidate) && !ts.isFunctionLike(candidate) && !ts.isSourceFile(candidate)) candidate = candidate.parent;
25
+ const sole = element => extracted.bindings.filter(other => other.start >= element.getStart(file) && other.end <= element.end).every(other => other.start === original.start && other.end === original.end);
26
+ if (candidate && ts.isJsxElement(candidate) && sole(candidate)) { binding = { ...binding, start: candidate.getStart(file), end: candidate.end, kind: 'element' }; node = candidate; }
27
+ // Deleting a sole button/link label removes the control, retaining its native
28
+ // handlers and icons on ordinary text edits.
29
+ for (let parent = node?.parent; parent && !ts.isFunctionLike(parent) && !ts.isSourceFile(parent); parent = parent.parent) {
30
+ if (ts.isJsxElement(parent) && ['button', 'a'].includes(parent.openingElement.tagName.getText(file))) {
31
+ if (sole(parent)) binding = { ...binding, start: parent.getStart(file), end: parent.end, kind: 'element' };
32
+ break;
33
+ }
34
+ }
35
+ if (binding.trace.some(trace => !trace.selector || !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(trace.selector) || !(trace.value === null || typeof trace.value === 'string' || typeof trace.value === 'boolean' || typeof trace.value === 'number' && Number.isFinite(trace.value)))) fail('A repeated source block needs an explicit immutable row identity. Index-based content binding is unsafe.');
36
+ const key = `${binding.start}:${binding.end}`, selectors = binding.trace.map(trace => trace.selector), values = binding.trace.map(trace => trace.value);
37
+ let group = ranges.get(key);
38
+ if (!group) { group = { ...binding, selectors, variants: [] }; ranges.set(key, group); }
39
+ if (JSON.stringify(group.selectors) !== JSON.stringify(selectors) || group.variants.some(variant => JSON.stringify(variant.values) === JSON.stringify(values))) fail('Repeated content has ambiguous row identities. Provide reviewed bindings before enabling rendering.');
40
+ group.variants.push({ values, id: extracted.entry.document.blocks[index].id });
41
+ }
42
+ const sorted = [...ranges.values()].sort((a, b) => a.start - b.start), bindings = {}, edits = [];
43
+ for (let index = 1; index < sorted.length; index++) if (sorted[index].start < sorted[index - 1].end) fail('Overlapping source content requires a reviewed renderer.');
44
+ for (const group of sorted) {
45
+ const slot = `slot-${digest(`${locale}:${slug}:${group.start}:${group.end}`)}`;
46
+ bindings[slot] = { variants: group.variants };
47
+ const native = source.slice(group.start, group.end);
48
+ let child = group.kind === 'element' ? native : group.kind === 'expression' ? `{${native}}` : `<>${native}</>`;
49
+ const element = nodes.find(node => ts.isJsxElement(node) && node.getStart(file) === group.start && node.end === group.end);
50
+ if(publicSource){
51
+ if(group.selectors.length||!element||!/^[a-z][a-z0-9-]*$/.test(element.openingElement.tagName.getText(file))||element.children.some(node=>!ts.isJsxText(node)&&!(ts.isJsxExpression(node)&&node.expression&&(ts.isStringLiteral(node.expression)||ts.isNumericLiteral(node.expression)))))fail('Public React source needs explicit review for repeated, nested or data-driven content. Literal native bindings are required before private setup.');
52
+ child=element.openingElement.getText(file)+'{"\\u200b"}'+element.closingElement.getText(file);
53
+ }
54
+ const key = element?.openingElement.attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && attribute.name.getText(file) === 'key');
55
+ const replacement = `<CmsBound${key ? ` ${key.getText(file)}` : ''} slot="${slot}" selector={[${group.selectors.join(', ')}]}>${child}</CmsBound>`;
56
+ edits.push({ ...group, replacement });
57
+ }
58
+ const range = extracted.returnRange;
59
+ // Place new blocks deliberately after the native page body, rather than
60
+ // replacing its layout or pretending arbitrary CMS reordering is supported.
61
+ edits.push({ start: range.end, end: range.end, replacement: '<CmsAdditionalContent /></>' });
62
+ edits.push({ start: range.start, end: range.start, replacement: '<>' });
63
+ let output = source;
64
+ for (const edit of edits.sort((a, b) => b.start - a.start || b.end - a.end)) output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
65
+ const directives = file.statements.filter(statement => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression));
66
+ const insertion = directives.length ? directives.at(-1).end : 0;
67
+ output = output.slice(0, insertion) + '\nimport { CmsBound, CmsAdditionalContent } from "@universalcms/connect/react-publication";\nimport "@universalcms/connect/publication.css";\n' + output.slice(insertion);
68
+ if (ts.createSourceFile(filename, output, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX).parseDiagnostics.length) fail('The generated content binding did not parse. Existing source was preserved.');
69
+ const baseline = extracted.entry.document;
70
+ if(publicSource){
71
+ const copy=ts.createSourceFile(filename,output,ts.ScriptTarget.Latest,true,ts.ScriptKind.TSX);let retained=false;
72
+ const originals=new Set(baseline.blocks.map(block=>block.text).filter(Boolean));
73
+ function inspect(node){if((ts.isStringLiteral(node)||ts.isJsxText(node)||ts.isNoSubstitutionTemplateLiteral(node))&&[...originals].some(text=>node.text.includes(text)))retained=true;ts.forEachChild(node,inspect);}inspect(copy);
74
+ if(retained)fail('Public React source retains editorial copy outside its literal bindings. Review the source dependencies before private setup.');
75
+ }
76
+ return { source: output, inventory: { identity: { kind: 'page', slug, locale }, baseline, titleBlockId: baseline.blocks.find(block => block.type === 'heading' && block.text === baseline.title)?.id, bindings }, entry: extracted.entry, warnings: extracted.warnings };
77
+ }