@universalcms/connect 0.0.0-stage → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +779 -2
- package/dist/angular-runtime.d.mts +4 -0
- package/dist/angular-runtime.mjs +7 -0
- package/dist/angular-setup.mjs +124 -0
- package/dist/astro-runtime.d.mts +6 -0
- package/dist/astro-runtime.mjs +255 -0
- package/dist/astro-setup.mjs +215 -0
- package/dist/astro-source.mjs +451 -0
- package/dist/astro-website.d.mts +5 -0
- package/dist/astro-website.mjs +42 -0
- package/dist/cli.mjs +552 -0
- package/dist/connector-runtime.mjs +130 -0
- package/dist/content-next-setup.mjs +200 -0
- package/dist/content-registry-validation.mjs +40 -0
- package/dist/content-registry.mjs +44 -0
- package/dist/content-setup.mjs +13 -0
- package/dist/content-storefront-setup.mjs +155 -0
- package/dist/content-vite-setup.mjs +107 -0
- package/dist/core.mjs +205 -0
- package/dist/framework-ownership.mjs +68 -0
- package/dist/framework.mjs +54 -0
- package/dist/guided.mjs +335 -0
- package/dist/native-adapters.mjs +31 -0
- package/dist/native-chrome-model.d.mts +3 -0
- package/dist/native-chrome-model.mjs +14 -0
- package/dist/native-chrome-review.mjs +49 -0
- package/dist/native-chrome-source.mjs +69 -0
- package/dist/native-route-review.mjs +152 -0
- package/dist/native-routes.mjs +27 -0
- package/dist/native-website-model.d.mts +18 -0
- package/dist/native-website-model.mjs +67 -0
- package/dist/native-website-server.d.mts +1 -0
- package/dist/native-website-server.mjs +35222 -0
- package/dist/native-website-server.mjs.LEGAL.txt +56 -0
- package/dist/native-website.d.mts +3 -0
- package/dist/native-website.mjs +120 -0
- package/dist/native-website.mjs.LEGAL.txt +56 -0
- package/dist/next-publication.d.mts +12 -0
- package/dist/next-publication.mjs +107 -0
- package/dist/next-settings-layout.mjs +233 -0
- package/dist/next-source.d.mts +8 -0
- package/dist/next-source.mjs +93 -0
- package/dist/page-discovery.mjs +236 -0
- package/dist/page-mapping-review.mjs +160 -0
- package/dist/preflight-recovery.mjs +122 -0
- package/dist/preflight.mjs +138 -0
- package/dist/project-files.mjs +56 -0
- package/dist/public-bindings.d.mts +3 -0
- package/dist/public-bindings.mjs +28 -0
- package/dist/publication-browser.d.mts +5 -0
- package/dist/publication-browser.mjs +258 -0
- package/dist/publication-check.mjs +141 -0
- package/dist/publication-document.d.mts +2 -0
- package/dist/publication-document.mjs +59 -0
- package/dist/publication-next-setup.mjs +134 -0
- package/dist/publication-setup.mjs +138 -0
- package/dist/publication-verification.mjs +85 -0
- package/dist/publication.css +6 -0
- package/dist/publication.d.mts +31 -0
- package/dist/publication.mjs +320 -0
- package/dist/react-publication.d.mts +22 -0
- package/dist/react-publication.mjs +301 -0
- package/dist/react-website.d.mts +15 -0
- package/dist/react-website.mjs +735 -0
- package/dist/reviewed-commerce-ownership.mjs +26 -0
- package/dist/reviewed-commerce-settings-setup.mjs +243 -0
- package/dist/reviewed-native-ownership.mjs +118 -0
- package/dist/reviewed-storefront-source.mjs +227 -0
- package/dist/runtime-dependencies.mjs +59 -0
- package/dist/setup-environment.mjs +307 -0
- package/dist/source-bindings.mjs +77 -0
- package/dist/source-check.mjs +149 -0
- package/dist/source-collect.mjs +80 -0
- package/dist/source-config.mjs +20 -0
- package/dist/source-extract.mjs +514 -0
- package/dist/source-finalize.mjs +52 -0
- package/dist/source-routes.mjs +65 -0
- package/dist/source-setup.mjs +490 -0
- package/dist/staged-setup.mjs +287 -0
- package/dist/static-runtime.mjs +67 -0
- package/dist/static-setup.mjs +136 -0
- package/dist/svelte-runtime.d.mts +27 -0
- package/dist/svelte-runtime.mjs +40 -0
- package/dist/svelte-setup.mjs +135 -0
- package/dist/svelte-template.mjs +48 -0
- package/dist/templates.mjs +248 -0
- package/dist/vendor/analyze-project.mjs +82 -0
- package/dist/vendor/astro-compiler/LICENSE +53 -0
- package/dist/vendor/astro-compiler/astro.wasm +0 -0
- package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
- package/dist/vendor/astro-compiler/node/index.js +1 -0
- package/dist/vendor/check-delivery.mjs +158 -0
- package/dist/vendor/dependency-diagnostics.mjs +72 -0
- package/dist/vendor/inspect-installed-dependencies.mjs +125 -0
- package/dist/vendor/project-analysis.mjs +498 -0
- package/dist/vendor/react-LICENSE.txt +21 -0
- package/dist/vendor/react-document.mjs +124 -0
- package/dist/vendor/react-dom-LICENSE.txt +21 -0
- package/dist/vendor/react-form.mjs +63 -0
- package/dist/vendor/react-safety.mjs +47 -0
- package/dist/vendor/source-manifest-provider.mjs +104 -0
- package/dist/vendor/typescript-LICENSE.txt +55 -0
- package/dist/vendor/typescript.cjs +200276 -0
- package/dist/vite-handler.mjs +54 -0
- package/dist/vite-publication-server.d.mts +3 -0
- package/dist/vite-publication-server.mjs +82 -0
- package/dist/vite-publication.d.mts +15 -0
- package/dist/vite-publication.mjs +241 -0
- package/dist/vite-source-discovery.mjs +315 -0
- package/dist/vite-source.d.mts +30 -0
- package/dist/vite-source.mjs +141 -0
- package/dist/vue-runtime.d.mts +10 -0
- package/dist/vue-runtime.mjs +50 -0
- package/dist/vue-server.d.mts +2 -0
- package/dist/vue-server.mjs +58 -0
- package/dist/vue-setup.mjs +127 -0
- package/dist/vue-source.mjs +168 -0
- package/dist/website-check.mjs +41 -0
- package/dist/website-http.mjs +149 -0
- package/dist/website-model.d.mts +20 -0
- package/dist/website-model.mjs +155 -0
- package/dist/website-next.d.mts +2 -0
- package/dist/website-next.mjs +29 -0
- package/dist/website-node.d.mts +3 -0
- package/dist/website-node.mjs +50 -0
- package/dist/website-runtime.d.mts +13 -0
- package/dist/website-runtime.mjs +396 -0
- package/dist/website-schema.mjs +22175 -0
- package/dist/website-setup.mjs +256 -0
- package/dist/website.css +49 -0
- package/dist/wizard.mjs +298 -0
- package/package.json +60 -4
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Inspect the website's runtime package without importing customer package code. */
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { supportedAstroVersion } from './astro-setup.mjs';
|
|
7
|
+
import { projectRoot, readProjectFile, SetupError } from './core.mjs';
|
|
8
|
+
import { isNativeFramework, inspectNativeProject } from './native-adapters.mjs';
|
|
9
|
+
|
|
10
|
+
const metadataLimit = 128 * 1024;
|
|
11
|
+
const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
const inside = (root, target) => {
|
|
13
|
+
const relative = path.relative(root, target);
|
|
14
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
15
|
+
};
|
|
16
|
+
const exportsFor = framework => ({
|
|
17
|
+
'./publication': './dist/publication.mjs',
|
|
18
|
+
'./react-publication': './dist/react-publication.mjs',
|
|
19
|
+
'./publication.css': './dist/publication.css',
|
|
20
|
+
'./website-runtime': './dist/website-runtime.mjs',
|
|
21
|
+
'./react-website': './dist/react-website.mjs',
|
|
22
|
+
'./website.css': './dist/website.css',
|
|
23
|
+
...(isNativeFramework(framework)
|
|
24
|
+
? { './native-website': './dist/native-website.mjs', './native-website-server': './dist/native-website-server.mjs', [`./${framework === 'nuxt' ? 'vue' : framework}-runtime`]: `./dist/${framework === 'nuxt' ? 'vue' : framework}-runtime.mjs`, ...(framework === 'vue' ? {'./vue-server':'./dist/vue-server.mjs'} : {}) }
|
|
25
|
+
: framework === 'astro'
|
|
26
|
+
? { './astro-runtime': './dist/astro-runtime.mjs', './astro-website': './dist/astro-website.mjs' }
|
|
27
|
+
: framework === 'next'
|
|
28
|
+
? { './public-bindings': './dist/public-bindings.mjs', './next-source': './dist/next-source.mjs', './next-publication': './dist/next-publication.mjs', './website-next': './dist/website-next.mjs' }
|
|
29
|
+
: { './public-bindings': './dist/public-bindings.mjs', './vite-source': './dist/vite-source.mjs', './vite-publication': './dist/vite-publication.mjs', './vite-publication-server': './dist/vite-publication-server.mjs', './website-node': './dist/website-node.mjs' }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function metadata(filename) {
|
|
33
|
+
let handle;
|
|
34
|
+
try {
|
|
35
|
+
const before = await lstat(filename);
|
|
36
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > metadataLimit || await realpath(filename) !== filename) return null;
|
|
37
|
+
handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
38
|
+
const opened = await handle.stat();
|
|
39
|
+
if (!opened.isFile() || opened.ino !== before.ino || opened.dev !== before.dev || opened.size > metadataLimit) return null;
|
|
40
|
+
const buffer = Buffer.alloc(metadataLimit + 1); let size = 0;
|
|
41
|
+
while (size < buffer.length) {
|
|
42
|
+
const part = await handle.read(buffer, size, buffer.length - size, size);
|
|
43
|
+
if (!part.bytesRead) break;
|
|
44
|
+
size += part.bytesRead;
|
|
45
|
+
}
|
|
46
|
+
if (size > metadataLimit) return null;
|
|
47
|
+
const current = await lstat(filename), after = await handle.stat();
|
|
48
|
+
if (current.isSymbolicLink() || current.ino !== opened.ino || current.dev !== opened.dev || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) return null;
|
|
49
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, size)));
|
|
50
|
+
return plain(value) ? value : null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
finally { await handle?.close(); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function regularAsset(directory, relative) {
|
|
56
|
+
const filename = path.join(directory, relative), info = await lstat(filename);
|
|
57
|
+
return info.isFile() && !info.isSymbolicLink() && info.size > 0 && info.size <= 8 * 1024 * 1024 && inside(directory, filename) && await realpath(filename) === filename;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A cached/global CLI is not proof that the website can resolve its generated imports. */
|
|
61
|
+
export async function assertConnectorRuntime(directory, framework) {
|
|
62
|
+
const root = await projectRoot(directory);
|
|
63
|
+
const fail = reason => { throw new SetupError(`Website connector runtime is not ready: ${reason} Install @universalcms/connect as a production dependency in this website folder using the install command from Developer setup, then rerun setup. The local runtime must match the CLI version; running a cached or external CLI alone is insufficient. Existing website files and keys were preserved.`); };
|
|
64
|
+
let manifest;
|
|
65
|
+
try { manifest = JSON.parse(await readProjectFile(root, 'package.json')); } catch { fail('The website package manifest is invalid.'); }
|
|
66
|
+
if (!['next', 'vite', 'astro'].includes(framework) && !isNativeFramework(framework)) fail('The selected framework needs a reviewed connector runtime.');
|
|
67
|
+
const declaration = manifest?.dependencies?.['@universalcms/connect'];
|
|
68
|
+
if (typeof declaration !== 'string' || !declaration.trim() || declaration.length > 4096 || /[\r\n\0]/.test(declaration)) fail('@universalcms/connect is not declared in dependencies. A development-only or temporary installation cannot support the website runtime.');
|
|
69
|
+
const own = await metadata(fileURLToPath(new URL('../package.json', import.meta.url)));
|
|
70
|
+
if (own?.name !== '@universalcms/connect' || typeof own.version !== 'string') fail('The executing CLI package metadata is unavailable.');
|
|
71
|
+
try {
|
|
72
|
+
const modules = path.join(root, 'node_modules');
|
|
73
|
+
if (!(await lstat(modules)).isDirectory() || await realpath(modules) !== modules) fail('The website node_modules directory is unavailable or externally linked.');
|
|
74
|
+
const scope = await realpath(path.join(modules, '@universalcms'));
|
|
75
|
+
if (!inside(modules, scope) || !(await lstat(scope)).isDirectory()) fail('The local connector package scope is unavailable.');
|
|
76
|
+
// pnpm's package directory link is accepted only within this node_modules.
|
|
77
|
+
const installed = await realpath(path.join(scope, 'connect'));
|
|
78
|
+
if (!inside(modules, installed) || !(await lstat(installed)).isDirectory()) fail('The connector package must resolve inside this website node_modules.');
|
|
79
|
+
const pkg = await metadata(path.join(installed, 'package.json'));
|
|
80
|
+
if (pkg?.name !== '@universalcms/connect' || pkg.type !== 'module' || pkg.version !== own.version || !plain(pkg.exports)) fail('The installed connector metadata or version does not match this CLI.');
|
|
81
|
+
for (const [name, target] of Object.entries(exportsFor(framework))) {
|
|
82
|
+
if (pkg.exports[name] !== target || !await regularAsset(installed, target)) fail(`The required ${name} export or file is missing or incompatible.`);
|
|
83
|
+
}
|
|
84
|
+
if (isNativeFramework(framework)) {
|
|
85
|
+
const inspection = await inspectNativeProject(root, framework, manifest);
|
|
86
|
+
for (const spec of inspection.runtimeDependencies ?? []) {
|
|
87
|
+
const declared = manifest.dependencies?.[spec.name] ?? (spec.production ? undefined : manifest.devDependencies?.[spec.name]);
|
|
88
|
+
if (spec.production && declared === undefined) fail('The native server framework must be installed as a production dependency.');
|
|
89
|
+
if (typeof declared !== 'string' || !/^[~^]?\d+\.\d+\.\d+$/.test(declared)) fail('Native framework dependencies must declare reviewed versions.');
|
|
90
|
+
const directory = await realpath(path.join(modules, spec.name));
|
|
91
|
+
if (!inside(modules, directory)) fail('Native framework dependencies must resolve inside this website node_modules.');
|
|
92
|
+
const dependency = await metadata(path.join(directory, 'package.json'));
|
|
93
|
+
const installed = dependency?.version, required = declared.replace(/^[~^]/, '');
|
|
94
|
+
const actual = String(installed).split('.').map(Number), baseline = required.split('.').map(Number);
|
|
95
|
+
if (dependency?.name !== spec.name || !/^\d+\.\d+\.\d+$/.test(installed ?? '') || !spec.majors.includes(actual[0]) || actual[1] < (spec.minimumMinor ?? 0) || actual[0] !== baseline[0] || actual[1] < baseline[1] || actual[1] === baseline[1] && actual[2] < baseline[2] || !/^[~^]/.test(declared) && installed !== declared || declared.startsWith('~') && actual[1] !== baseline[1]) fail('An installed native framework dependency is missing or incompatible with its declaration.');
|
|
96
|
+
for (const asset of spec.assets) if (!await regularAsset(directory, asset)) fail('A native framework package is incomplete. Reinstall its reviewed dependencies.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (framework === 'astro') {
|
|
100
|
+
const supported = (value, major, minimum = 0) => new RegExp(`^${major}\\.(\\d+)\\.\\d+$`).test(value ?? '') && +value.split('.')[1] >= minimum;
|
|
101
|
+
const targets = [
|
|
102
|
+
['astro', value => supportedAstroVersion(value), ['./dist/index.js', './dist/config/entrypoint.js']],
|
|
103
|
+
['@astrojs/node', value => supported(value, 11), ['./dist/index.js', './dist/server.js']],
|
|
104
|
+
['@astrojs/react', value => supported(value, 7), ['./dist/index.js', './dist/client.js', './dist/server.js']],
|
|
105
|
+
['react', value => supported(value, 19, 2), ['./index.js', './jsx-runtime.js']],
|
|
106
|
+
['react-dom', value => supported(value, 19, 2), ['./index.js', './client.js', './server.node.js']],
|
|
107
|
+
];
|
|
108
|
+
const versions = {};
|
|
109
|
+
for (const [name, compatible, assets] of targets) {
|
|
110
|
+
const declared = manifest.dependencies?.[name];
|
|
111
|
+
if (typeof declared !== 'string' || !/^[~^]?\d+\.\d+\.\d+$/.test(declared)) fail('Astro, its official Node/React integrations, React and React DOM must be supported production dependencies.');
|
|
112
|
+
const directory = await realpath(path.join(modules, name));
|
|
113
|
+
if (!inside(modules, directory)) fail('Astro runtime dependencies must resolve inside this website node_modules.');
|
|
114
|
+
const dependency = await metadata(path.join(directory, 'package.json'));
|
|
115
|
+
const installed = dependency?.version, required = declared.replace(/^[~^]/, '');
|
|
116
|
+
const actual = String(installed).split('.').map(Number), baseline = required.split('.').map(Number);
|
|
117
|
+
if (dependency?.name !== name || !compatible(installed) || actual[0] !== baseline[0] || actual[1] < baseline[1] || actual[1] === baseline[1] && actual[2] < baseline[2] || !/^[~^]/.test(declared) && installed !== declared || declared.startsWith('~') && actual[1] !== baseline[1]) fail('An installed Astro runtime dependency is missing or incompatible with its declaration.');
|
|
118
|
+
for (const asset of assets) if (!await regularAsset(directory, asset)) fail('An Astro runtime package is incomplete. Reinstall the reviewed production dependencies.');
|
|
119
|
+
versions[name] = installed;
|
|
120
|
+
}
|
|
121
|
+
if (versions.react !== versions['react-dom']) fail('React and React DOM must use matching versions for Astro rendering.');
|
|
122
|
+
}
|
|
123
|
+
for (const target of ['dist/vendor/react-document.mjs', 'dist/vendor/react-form.mjs', 'dist/vendor/react-safety.mjs']) {
|
|
124
|
+
if (!await regularAsset(installed, target)) fail('The bundled published-content renderer is incomplete.');
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof SetupError) throw error;
|
|
128
|
+
fail('The installed connector package or a required runtime file is missing or unreadable.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Render publications on the original Next routes with frozen source slots. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { lstat, readdir } from 'node:fs/promises';
|
|
4
|
+
import ts from './vendor/typescript.cjs';
|
|
5
|
+
import { isExcludedPublicationPath, validatePublicationConfig } from './publication.mjs';
|
|
6
|
+
import { CONFIG_FILE, readProjectFile, projectRoot, SetupError, validateConfig } from './core.mjs';
|
|
7
|
+
import { createTemplates } from './templates.mjs';
|
|
8
|
+
import { inspectSourceRoutes } from './source-routes.mjs';
|
|
9
|
+
import { transformSourcePublication } from './source-bindings.mjs';
|
|
10
|
+
import { publicBindingInventory } from './public-bindings.mjs';
|
|
11
|
+
import { CONTENT_BINDINGS_FILE, contentDigest, readContentRegistry, readFrozenContent } from './content-registry.mjs';
|
|
12
|
+
import { inspectReviewedNativeOwnership } from './reviewed-native-ownership.mjs';
|
|
13
|
+
import { matchesNativeRoutes } from './native-routes.mjs';
|
|
14
|
+
import { inspectReviewedCommerceOwnership } from './reviewed-commerce-ownership.mjs';
|
|
15
|
+
import { setupReviewError } from './preflight-recovery.mjs';
|
|
16
|
+
const json = value => `${JSON.stringify(value, null, 2)}\n`;
|
|
17
|
+
const fail = message => { throw new SetupError(message); };
|
|
18
|
+
const relative = (from, to) => { const value = path.posix.relative(path.posix.dirname(from), to); return value.startsWith('.') ? value : `./${value}`; };
|
|
19
|
+
// Old init/setup installations may retain the additive delivery demo. Its exact
|
|
20
|
+
// generated catch-all is deliberately unavailable after full setup; it is not an
|
|
21
|
+
// unknown native route. Never grant that ownership from a route name alone.
|
|
22
|
+
export async function inspectOwnedDemo(root) {
|
|
23
|
+
const text = await readProjectFile(root, CONFIG_FILE);
|
|
24
|
+
if (text === null) return null;
|
|
25
|
+
let saved;
|
|
26
|
+
try { saved = validateConfig(JSON.parse(text)); }
|
|
27
|
+
catch { fail('The saved connector configuration is invalid. Restore it before running setup.'); }
|
|
28
|
+
if (saved.framework !== 'next') return null;
|
|
29
|
+
const mount = `${saved.appDirectory}${saved.mount}`, dynamic = `${mount}/[...slug]`;
|
|
30
|
+
const expected = (await createTemplates(saved)).filter(file => file.path.startsWith(`${mount}/`));
|
|
31
|
+
const observed = [{ path: CONFIG_FILE, content: text, state: 'unchanged' }];
|
|
32
|
+
for (const file of expected) {
|
|
33
|
+
const current = await readProjectFile(root, file.path);
|
|
34
|
+
if (current !== file.content) fail('A generated demo route contains different content. Review its ownership before full setup. Existing files were preserved.');
|
|
35
|
+
observed.push({ ...file, state: 'unchanged' });
|
|
36
|
+
}
|
|
37
|
+
for (const file of observed) if ((await lstat(path.join(root, file.path))).nlink !== 1) fail('A generated file is linked or changed. Existing files were preserved.');
|
|
38
|
+
for (const [directory, names] of [[mount, ['cms.server.js', '[...slug]']], [dynamic, ['page.jsx', 'error.jsx']]]) {
|
|
39
|
+
const entries = await readdir(path.join(root, directory), { withFileTypes: true });
|
|
40
|
+
if (entries.length !== names.length || entries.some(entry => !names.includes(entry.name) || entry.isSymbolicLink() || (entry.name === '[...slug]' ? !entry.isDirectory() : !entry.isFile()))) fail('The generated demo mount contains additional routes. Review its ownership before full setup.');
|
|
41
|
+
}
|
|
42
|
+
// The demo planner separately owns these files and may be updating their
|
|
43
|
+
// connection values in the same transaction. Do not duplicate its write set.
|
|
44
|
+
return { path: dynamic, files: observed };
|
|
45
|
+
}
|
|
46
|
+
export function nextContentPageTemplate(row, configurationFile = '.universalcms/publication.config.json') {
|
|
47
|
+
const native = relative(row.sourceFile, row.nativeFile).replace(/\.(?:[jt]sx?)$/, '');
|
|
48
|
+
const typed = row.sourceFile.endsWith('.tsx');
|
|
49
|
+
return `// Generated by @universalcms/connect. Published CMS document owns these bound slots.\nimport { cache } from 'react';\nimport { connection } from 'next/server';\nimport { notFound } from 'next/navigation';\nimport { createPublishedDocumentReader, publicPublishedEntry } from '@universalcms/connect/publication';\nimport { PublishedDocumentProvider } from '@universalcms/connect/react-publication';\nimport { publicBindingInventory } from '@universalcms/connect/public-bindings';\nimport configuration from ${JSON.stringify(relative(row.sourceFile, configurationFile))};\nimport inventory from ${JSON.stringify(relative(row.sourceFile, row.inventoryFile))};\nimport NativePage from ${JSON.stringify(native)};\n\nconst read = createPublishedDocumentReader({ configuration: configuration${typed ? " as Parameters<typeof createPublishedDocumentReader>[0]['configuration']" : ''} });\nconst load = cache(async () => {\n await connection();\n const result = await read(${JSON.stringify(row.path)});\n if (result.state === 'missing') notFound();\n if (result.state !== 'published') throw new Error('Published content is temporarily unavailable');\n return publicPublishedEntry(result.entry);\n});\nexport async function generateMetadata() {\n const entry = await load();\n return { title: entry.document.title, description: entry.document.excerpt };\n}\nexport default async function Page() {\n const entry = await load();\n const publicInventory = publicBindingInventory(inventory${typed ? " as import('@universalcms/connect/react-publication').Inventory" : ''});\n return <PublishedDocumentProvider initialEntry={entry} pathname=${JSON.stringify(row.path)} route={publicInventory}><NativePage /></PublishedDocumentProvider>;\n}\n`;
|
|
50
|
+
}
|
|
51
|
+
/** Configure request-time content/metadata only when the Next export is one
|
|
52
|
+
* literal object. Dynamic configuration is preserved and requires review. */
|
|
53
|
+
function rewriteNextContentConfiguration(source, filename) {
|
|
54
|
+
const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, filename.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS);
|
|
55
|
+
const unbox = value => { while (value && (ts.isAsExpression(value) || ts.isSatisfiesExpression(value) || ts.isParenthesizedExpression(value))) value = value.expression; return value; };
|
|
56
|
+
const exported = file.statements.find(statement => ts.isExportAssignment(statement));
|
|
57
|
+
let object = unbox(exported?.expression);
|
|
58
|
+
if (object && ts.isIdentifier(object)) {
|
|
59
|
+
const name = object.text;
|
|
60
|
+
const declarations = file.statements.filter(ts.isVariableStatement).flatMap(statement => [...statement.declarationList.declarations]);
|
|
61
|
+
object = unbox(declarations.find(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === name)?.initializer);
|
|
62
|
+
}
|
|
63
|
+
if (file.parseDiagnostics.length || !object || !ts.isObjectLiteralExpression(object) || object.properties.some(property => ts.isSpreadAssignment(property))) fail('Dynamic Next configuration needs a reviewed request-time content integration: disable Cache Components and serve published metadata in the initial HTML. Existing settings were preserved.');
|
|
64
|
+
const named = (properties, name) => properties.filter(property => property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === name);
|
|
65
|
+
const edits = [];
|
|
66
|
+
let disablesCacheComponents = false;
|
|
67
|
+
for (const name of ['cacheComponents', 'htmlLimitedBots']) {
|
|
68
|
+
const properties = named(object.properties, name);
|
|
69
|
+
if (properties.length > 1 || properties.some(property => !ts.isPropertyAssignment(property))) fail('Ambiguous Next content settings require manual review.');
|
|
70
|
+
const property = properties[0];
|
|
71
|
+
if (name === 'cacheComponents' && property) {
|
|
72
|
+
if (![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(property.initializer.kind)) fail('Dynamic Cache Components settings need manual review before binding public content.');
|
|
73
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
74
|
+
disablesCacheComponents = true;
|
|
75
|
+
edits.push({ start: property.initializer.getStart(file), end: property.initializer.end, text: 'false' });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (name === 'htmlLimitedBots') {
|
|
79
|
+
if (property && property.initializer.getText(file) !== '/.*/') edits.push({ start: property.initializer.getStart(file), end: property.initializer.end, text: '/.*/' });
|
|
80
|
+
else if (!property) edits.push({ start: object.getStart(file) + 1, end: object.getStart(file) + 1, text: '\n htmlLimitedBots: /.*/,\n' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const topPartials = named(object.properties, 'partialPrefetching');
|
|
84
|
+
if (topPartials.length > 1 || topPartials.some(property => !ts.isPropertyAssignment(property) || ![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(property.initializer.kind))) fail('Dynamic partial prefetch settings need manual review.');
|
|
85
|
+
if (topPartials[0]) {
|
|
86
|
+
const property = topPartials[0];
|
|
87
|
+
let start = property.getStart(file), end = property.end;
|
|
88
|
+
const after = source.slice(end, object.end - 1).match(/^\s*,/);
|
|
89
|
+
if (after) end += after[0].length;
|
|
90
|
+
else { const before = source.slice(object.getStart(file) + 1, start).match(/,\s*$/); if (before) start -= before[0].length; }
|
|
91
|
+
edits.push({start,end,text:''});
|
|
92
|
+
}
|
|
93
|
+
const experiments = named(object.properties, 'experimental');
|
|
94
|
+
if (experiments.length > 1) fail('Ambiguous Next experimental settings need review.');
|
|
95
|
+
const experimental = experiments[0];
|
|
96
|
+
if (experimental && ts.isPropertyAssignment(experimental) && ts.isObjectLiteralExpression(experimental.initializer)) {
|
|
97
|
+
const partials = named(experimental.initializer.properties, 'partialPrefetching');
|
|
98
|
+
if (partials.length > 1) fail('Ambiguous Next partial prefetch settings need review.');
|
|
99
|
+
if (partials[0]) {
|
|
100
|
+
const property = partials[0];
|
|
101
|
+
let start = property.getStart(file), end = property.end;
|
|
102
|
+
const after = source.slice(end, experimental.initializer.end - 1).match(/^\s*,/);
|
|
103
|
+
if (after) end += after[0].length;
|
|
104
|
+
else { const before = source.slice(experimental.initializer.getStart(file) + 1, start).match(/,\s*$/); if (before) start -= before[0].length; }
|
|
105
|
+
edits.push({ start, end, text: '' });
|
|
106
|
+
}
|
|
107
|
+
} else if (experimental && /partialPrefetching/.test(experimental.getText(file))) fail('Dynamic partial prefetch settings need manual review.');
|
|
108
|
+
let output = source;
|
|
109
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
110
|
+
return { source: output, disablesCacheComponents };
|
|
111
|
+
}
|
|
112
|
+
export function configureNextContent(source, filename) { return rewriteNextContentConfiguration(source, filename).source; }
|
|
113
|
+
async function assertNoActiveCacheComponents(root) {
|
|
114
|
+
let visited = 0, bytes = 0;
|
|
115
|
+
const excluded = new Set(['node_modules', '.git', '.next', '.universalcms', 'dist', 'build', 'coverage', 'public']);
|
|
116
|
+
async function visit(relative, depth) {
|
|
117
|
+
if (depth > 12) fail('Website cache-feature review exceeds its directory budget. Review Cache Components manually.');
|
|
118
|
+
const entries = await readdir(path.join(root, relative), { withFileTypes: true });
|
|
119
|
+
for (const item of entries) {
|
|
120
|
+
if (excluded.has(item.name) || item.name.startsWith('.')) continue;
|
|
121
|
+
if (++visited > 4000) fail('Website cache-feature review exceeds its file budget. Review Cache Components manually.');
|
|
122
|
+
const name = relative ? `${relative}/${item.name}` : item.name;
|
|
123
|
+
if (item.isSymbolicLink()) fail('Linked website sources require manual review before disabling Cache Components.');
|
|
124
|
+
if (item.isDirectory()) { await visit(name, depth + 1); continue; }
|
|
125
|
+
if (!item.isFile() || !/\.[cm]?[jt]sx?$/.test(item.name) || /\.d\.ts$/.test(item.name) || /^next\.config\./.test(item.name)) continue;
|
|
126
|
+
const source = await readProjectFile(root, name);
|
|
127
|
+
bytes += Buffer.byteLength(source ?? '');
|
|
128
|
+
if (bytes > 8 * 1024 * 1024) fail('Website cache-feature review exceeds its byte budget. Review Cache Components manually.');
|
|
129
|
+
if (/['"]use cache(?:: [a-z-]+)?['"]|\b(?:cacheLife|cacheTag|unstable_cacheLife|unstable_cacheTag)\b/.test(source ?? '')) fail('This website actively uses Cache Components APIs. Preserve its cache configuration and implement a reviewed request-time CMS boundary before enabling content binding.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
await visit('', 0);
|
|
133
|
+
}
|
|
134
|
+
// Publication mapping must report unresolved native ownership before trying to
|
|
135
|
+
// map reserved application paths such as /cart as CMS content. Both planners
|
|
136
|
+
// use the same validated native review and exact connector-owned demo exclusion.
|
|
137
|
+
export function assertNextUnresolvedRouteOwnership(inventory, nativeOwnership, demo) {
|
|
138
|
+
const unresolvedPaths = inventory.unresolvedPaths.filter(name => name !== demo?.path && !(nativeOwnership?.dynamicDirectories?.includes(name) || nativeOwnership?.profile === 'reviewed-icecream-commerce-2026-10-15' && name === 'src/app/menu/[id]'));
|
|
139
|
+
if (unresolvedPaths.length) throw setupReviewError('next-route-ownership', unresolvedPaths);
|
|
140
|
+
}
|
|
141
|
+
export async function planNextContent(directory, configuration) {
|
|
142
|
+
configuration = validatePublicationConfig(configuration);
|
|
143
|
+
const root = await projectRoot(directory), prior = await readContentRegistry(root), files = [];
|
|
144
|
+
const nativeOwnership = await inspectReviewedNativeOwnership(root) ?? await inspectReviewedCommerceOwnership(root);
|
|
145
|
+
if (nativeOwnership && configuration.routes.some(route => matchesNativeRoutes(nativeOwnership, route.path))) fail('Reviewed native commerce routes cannot also be CMS editorial pages. Refresh the public source inventory before enabling publication control.');
|
|
146
|
+
let foundConfiguration = false;
|
|
147
|
+
for (const extension of ['js','mjs','cjs','ts']) {
|
|
148
|
+
const filename = `next.config.${extension}`, source = await readProjectFile(root, filename);
|
|
149
|
+
if (source === null) continue;
|
|
150
|
+
if (foundConfiguration) fail('Multiple Next configuration files require manual review.');
|
|
151
|
+
foundConfiguration = true;
|
|
152
|
+
const result = rewriteNextContentConfiguration(source, filename), updated = result.source;
|
|
153
|
+
if (result.disablesCacheComponents) await assertNoActiveCacheComponents(root);
|
|
154
|
+
files.push({ path: filename, content: updated, state: updated === source ? 'unchanged' : 'update', ...(updated !== source ? { previous: source } : {}) });
|
|
155
|
+
}
|
|
156
|
+
if (!foundConfiguration) files.push({ path: 'next.config.mjs', content: 'export default { htmlLimitedBots: /.*/ };\n', state: 'create' });
|
|
157
|
+
const inventory = await inspectSourceRoutes(root), routes = [], demo = await inspectOwnedDemo(root);
|
|
158
|
+
assertNextUnresolvedRouteOwnership(inventory, nativeOwnership, demo);
|
|
159
|
+
const managedPaths = new Set(configuration.routes.map(route => route.path));
|
|
160
|
+
const unownedPaths = inventory.routes.map(route => `/${route}`).filter(pathname => !managedPaths.has(pathname) && !nativeOwnership?.nativePaths.includes(pathname) && !isExcludedPublicationPath(pathname));
|
|
161
|
+
if (unownedPaths.length) fail(`Every public website page needs explicit content ownership before full setup. Unmapped routes: ${unownedPaths.slice(0, 10).join(', ')}${unownedPaths.length > 10 ? ', …' : ''}. Add reviewed CMS mappings or a reviewed native adapter; existing routes were preserved.`);
|
|
162
|
+
if (prior?.routes.some(row => !configuration.routes.some(route => ['path','kind','slug','locale'].every(key => route[key] === row[key])))) fail('An existing content route cannot be removed or reassigned automatically.');
|
|
163
|
+
for (const route of configuration.routes) {
|
|
164
|
+
const existing = prior?.routes.find(row => row.path === route.path);
|
|
165
|
+
if (existing) {
|
|
166
|
+
const { texts } = await readFrozenContent(root, existing);
|
|
167
|
+
if (texts.source !== nextContentPageTemplate(existing)) fail('A customized Next content wrapper requires manual review.');
|
|
168
|
+
routes.push(existing);
|
|
169
|
+
for (const prefix of ['source','native','inventory','original']) files.push({ path: existing[`${prefix}File`], content: texts[prefix], state: 'unchanged' });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (route.kind !== 'page') fail('Automatic Next source binding requires static pages. Map custom post routes through the shared renderer manually.');
|
|
173
|
+
const sourceFile = inventory.files[route.path === '/' ? 'home' : route.path.slice(1)];
|
|
174
|
+
if (!sourceFile || !/\.[jt]sx$/.test(sourceFile)) fail('Automatic content bindings require a JSX/TSX source page for each route.');
|
|
175
|
+
const source = await readProjectFile(root, sourceFile);
|
|
176
|
+
const transformed = transformSourcePublication({ source, slug: route.slug, locale: route.locale, filename: sourceFile, publicSource: true });
|
|
177
|
+
// Projection is part of planning, so unsupported source structure cannot
|
|
178
|
+
// wait until a running page serializes its client provider properties.
|
|
179
|
+
try { publicBindingInventory(transformed.inventory); }
|
|
180
|
+
catch { fail('The source needs reviewed literal public bindings before private setup. Existing files were preserved.'); }
|
|
181
|
+
const suffix = contentDigest(`${route.path}:${route.kind}:${route.slug}:${route.locale}`).slice(0, 20);
|
|
182
|
+
const nativeFile = sourceFile.replace(/\/page\.([jt]sx)$/, '/page.universalcms-native.$1');
|
|
183
|
+
const inventoryFile = `.universalcms/content/${suffix}.json`, originalFile = `.universalcms/content/${suffix}.original.txt`;
|
|
184
|
+
for (const name of [nativeFile,inventoryFile,originalFile]) if (await readProjectFile(root, name) !== null) fail('A generated content adapter path already contains a file. It was preserved.');
|
|
185
|
+
const inventoryText = json(transformed.inventory);
|
|
186
|
+
const row = { ...route, sourceFile, nativeFile, inventoryFile, originalFile, sourceHash: '', nativeHash: contentDigest(transformed.source), inventoryHash: contentDigest(inventoryText), originalHash: contentDigest(source) };
|
|
187
|
+
const wrapper = nextContentPageTemplate(row); row.sourceHash = contentDigest(wrapper);
|
|
188
|
+
// Save the immutable original and all imported dependencies before replacing
|
|
189
|
+
// its route entrypoint. Partial I/O failure must not erase the source first.
|
|
190
|
+
files.push({ path: originalFile, content: source, state: 'create' }, { path: nativeFile, content: transformed.source, state: 'create' }, { path: inventoryFile, content: inventoryText, state: 'create' }, { path: sourceFile, content: wrapper, state: 'update', previous: source });
|
|
191
|
+
routes.push(row);
|
|
192
|
+
}
|
|
193
|
+
const ownershipFiles = nativeOwnership ? [...nativeOwnership.files, ...(nativeOwnership.manifest ? [nativeOwnership.manifest] : [])] : [];
|
|
194
|
+
if (ownershipFiles.some(observed => files.some(file => file.path === observed.path && file.state !== 'unchanged'))) fail('A native reviewed dependency also needs CMS content edits. Separate shared ownership before setup; existing files were preserved.');
|
|
195
|
+
files.push(...ownershipFiles);
|
|
196
|
+
const registry = json({ version: 1, routes, ...(nativeOwnership ? { profile:nativeOwnership.profile,nativePaths:nativeOwnership.nativePaths,...(nativeOwnership.nativePatterns ? {nativePatterns:nativeOwnership.nativePatterns} : {}),files:ownershipFiles.map(file => ({path:file.path,sha256:contentDigest(file.content)})) } : {}) }), previous = await readProjectFile(root, CONTENT_BINDINGS_FILE);
|
|
197
|
+
files.push({ path: CONTENT_BINDINGS_FILE, content: registry, state: previous === null ? 'create' : previous === registry ? 'unchanged' : 'update', ...(previous !== null && previous !== registry ? { previous } : {}) });
|
|
198
|
+
if (files.some(file => Buffer.byteLength(file.content) > 128 * 1024)) fail('This content binding exceeds the safe per-file setup budget. Split its public source or use a reviewed adapter.');
|
|
199
|
+
return { root, framework: 'next', files, routes };
|
|
200
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { REVIEWED_NEXT_NATIVE_PROFILE, validateNativeRoutes, isNativeReviewFilePath } from './native-routes.mjs';
|
|
2
|
+
/** Pure validation for generated native content ownership. */
|
|
3
|
+
const fail = message => { throw new Error(message); };
|
|
4
|
+
const ordinary = value => typeof value === 'string' && value.length < 512 && !value.startsWith('/') && !value.includes('\\') && value.split('/').every(part => !!part && part !== '.' && part !== '..');
|
|
5
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
6
|
+
export const REVIEWED_STOREFRONT_PROFILE = 'reviewed-storefront-native-2026-10-15';
|
|
7
|
+
export const REVIEWED_ICECREAM_PROFILE = 'reviewed-icecream-commerce-2026-10-15';
|
|
8
|
+
const icecreamIds = ['sicilian-pistachio','madagascar-bourbon-vanilla','belgian-midnight-fudge','strawberry-wild-basil','salted-caramel-pretzel-crackle','alphonso-mango-passion-vegan','espresso-affogato-truffle','wild-blueberry-lavender','caramelized-honey-pecan','mint-matcha-stracciatella','cookie-dough-avalanche','vegan-raspberry-hibiscus','hot-fudge-brownie-sundae-bowl','artisan-4-pint-tasting-box','diy-sundae-party-kit','ruby-berry-carnival-sundae'];
|
|
9
|
+
export function reviewedNativePaths(profile) {
|
|
10
|
+
if (profile === REVIEWED_STOREFRONT_PROFILE) return ['/stylist-consult','/track-order'];
|
|
11
|
+
if (profile === REVIEWED_ICECREAM_PROFILE) return ['/cart','/checkout','/order-success',...icecreamIds.map(id => '/menu/' + id)].sort();
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
export function validateContentRegistry(value) {
|
|
15
|
+
const shared = value?.profile === REVIEWED_STOREFRONT_PROFILE;
|
|
16
|
+
const native = value?.profile === REVIEWED_NEXT_NATIVE_PROFILE;
|
|
17
|
+
const reviewed = shared || value?.profile === REVIEWED_ICECREAM_PROFILE || native;
|
|
18
|
+
const keys = native ? 'files,nativePaths,nativePatterns,profile,routes,version' : reviewed ? 'files,nativePaths,profile,routes,version' : 'routes,version';
|
|
19
|
+
if (!value || value.version !== 1 || Object.keys(value).sort().join() !== keys || !Array.isArray(value.routes) || !value.routes.length || value.routes.length > 1000) fail('The content binding registry needs review.');
|
|
20
|
+
if (reviewed && (!Array.isArray(value.files) || !value.files.length || value.files.length > (native ? 201 : 100) || value.files.some(file => !file || Object.keys(file).sort().join() !== 'path,sha256' || !ordinary(file.path) || !hash(file.sha256)) || new Set(value.files.map(file => file.path)).size !== value.files.length)) fail('The reviewed shared content modules are invalid.');
|
|
21
|
+
if (native) {
|
|
22
|
+
validateNativeRoutes(value, value.routes);
|
|
23
|
+
if (!value.files.some(file => file.path === '.universalcms/native.routes.json') || value.files.some(file => file.path !== '.universalcms/native.routes.json' && !isNativeReviewFilePath(file.path))) fail('Native registry fingerprints must contain the explicit manifest and reviewed public source modules only.');
|
|
24
|
+
}
|
|
25
|
+
if (reviewed && !native && (!Array.isArray(value.nativePaths) || JSON.stringify([...value.nativePaths].sort()) !== JSON.stringify(reviewedNativePaths(value.profile)) || value.routes.some(row => value.nativePaths.includes(row?.path)))) fail('Native route ownership must match the exact reviewed profile without overlapping CMS routes.');
|
|
26
|
+
const paths = new Set(), identities = new Set(), files = new Map(), inventories = new Set();
|
|
27
|
+
for (const row of value.routes) {
|
|
28
|
+
if (!row || Object.keys(row).some(key => !['path', 'kind', 'slug', 'locale', 'sourceFile', 'nativeFile', 'inventoryFile', 'originalFile', 'sourceHash', 'nativeHash', 'inventoryHash', 'originalHash'].includes(key)) || typeof row.path !== 'string' || !/^\/(?:[a-z0-9]+(?:[-/][a-z0-9]+)*)?$/.test(row.path) || !['page', 'post'].includes(row.kind) || typeof row.slug !== 'string' || !/^[a-z0-9]+(?:[-/][a-z0-9]+)*$/.test(row.slug) || typeof row.locale !== 'string' || !['sourceFile','nativeFile','inventoryFile','originalFile'].every(key => ordinary(row[key])) || !['sourceHash','nativeHash','inventoryHash','originalHash'].every(key => hash(row[key]))) fail('A content binding route is invalid.');
|
|
29
|
+
let locale; try { locale = Intl.getCanonicalLocales(row.locale)[0]; } catch { fail('A content binding locale is invalid.'); }
|
|
30
|
+
const identity = `${row.kind}:${locale}:${row.slug}`;
|
|
31
|
+
if (paths.has(row.path) || identities.has(identity) || inventories.has(row.inventoryFile) || row.sourceFile === row.nativeFile) fail('Content binding routes must have unique paths, identities and inventories.');
|
|
32
|
+
for (const prefix of ['source', 'native', 'original']) {
|
|
33
|
+
const name = row[`${prefix}File`], digest = row[`${prefix}Hash`], previous = files.get(name);
|
|
34
|
+
if (previous && (!shared || previous !== `${prefix}:${digest}`)) fail('Shared content modules require an exact reviewed profile and consistent ownership.');
|
|
35
|
+
files.set(name, `${prefix}:${digest}`);
|
|
36
|
+
}
|
|
37
|
+
paths.add(row.path); identities.add(identity); inventories.add(row.inventoryFile);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readProjectFile, SetupError } from './project-files.mjs';
|
|
5
|
+
export const CONTENT_BINDINGS_FILE = '.universalcms/content.bindings.json';
|
|
6
|
+
export const contentDigest = text => createHash('sha256').update(text).digest('hex');
|
|
7
|
+
const fail = message => { throw new SetupError(message); };
|
|
8
|
+
import { REVIEWED_NEXT_NATIVE_PROFILE } from './native-routes.mjs';
|
|
9
|
+
import { inspectReviewedNativeOwnership } from './reviewed-native-ownership.mjs';
|
|
10
|
+
import { validateContentRegistry } from './content-registry-validation.mjs';
|
|
11
|
+
export { validateContentRegistry, REVIEWED_STOREFRONT_PROFILE, REVIEWED_ICECREAM_PROFILE, reviewedNativePaths } from './content-registry-validation.mjs';
|
|
12
|
+
export async function readContentRegistry(root) {
|
|
13
|
+
const text = await readProjectFile(root, CONTENT_BINDINGS_FILE);
|
|
14
|
+
if (text === null) return null;
|
|
15
|
+
if ((await lstat(path.join(root, CONTENT_BINDINGS_FILE))).nlink !== 1) fail('Content bindings cannot be linked files.');
|
|
16
|
+
let value;
|
|
17
|
+
try { value = JSON.parse(text); } catch { fail('The content binding registry is invalid. Preserve it and review the integration.'); }
|
|
18
|
+
validateContentRegistry(value);
|
|
19
|
+
for (const file of value.files ?? []) {
|
|
20
|
+
const current = await readProjectFile(root, file.path);
|
|
21
|
+
if (current === null || contentDigest(current) !== file.sha256 || (await lstat(path.join(root, file.path))).nlink !== 1) fail('A reviewed shared content module changed. Preserve its edits and review the native CMS bindings before setup or deployment.');
|
|
22
|
+
}
|
|
23
|
+
if (value.profile === REVIEWED_NEXT_NATIVE_PROFILE) {
|
|
24
|
+
const review = await inspectReviewedNativeOwnership(root);
|
|
25
|
+
const files = review && [...review.files, review.manifest].map(file => ({path:file.path,sha256:contentDigest(file.content)}));
|
|
26
|
+
if (!review || JSON.stringify(review.nativePaths) !== JSON.stringify(value.nativePaths) || JSON.stringify(review.nativePatterns) !== JSON.stringify(value.nativePatterns) || JSON.stringify(files) !== JSON.stringify(value.files)) fail('Native route ownership changed. Review the explicit manifest and content bindings before continuing.');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export async function readFrozenContent(root, row) {
|
|
31
|
+
// Shared imported components are as authoritative as their route entrypoint.
|
|
32
|
+
// Never validate only a frozen JSON baseline while ignoring edited modules.
|
|
33
|
+
await readContentRegistry(root);
|
|
34
|
+
const texts = {};
|
|
35
|
+
for (const prefix of ['source','native','inventory','original']) {
|
|
36
|
+
const file = row[`${prefix}File`], text = await readProjectFile(root, file);
|
|
37
|
+
if (text === null || contentDigest(text) !== row[`${prefix}Hash`] || (await lstat(path.join(root, file))).nlink !== 1) fail('A bound content source or baseline changed. Preserve your edits and review its stable CMS bindings before setup or deployment.');
|
|
38
|
+
texts[prefix] = text;
|
|
39
|
+
}
|
|
40
|
+
let inventory; try { inventory = JSON.parse(texts.inventory); } catch { fail('The frozen content inventory is invalid.'); }
|
|
41
|
+
if (!inventory || !inventory.identity || ['kind','slug','locale'].some(key => inventory.identity[key] !== row[key]) || inventory.baseline?.schemaVersion !== 1 || !Array.isArray(inventory.baseline?.blocks) || !inventory.bindings || typeof inventory.bindings !== 'object') fail('The frozen content inventory does not match its registered route.');
|
|
42
|
+
const entry = { kind: row.kind, slug: row.slug, locale: row.locale, document: inventory.baseline };
|
|
43
|
+
return { inventory, entry, texts };
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Published-content setup is separate from source import and availability. */
|
|
2
|
+
import { planAstroContent } from './astro-source.mjs';
|
|
3
|
+
import { planNextContent } from './content-next-setup.mjs';
|
|
4
|
+
import { planViteContent } from './content-vite-setup.mjs';
|
|
5
|
+
import { isNativeFramework, planNativeContent } from './native-adapters.mjs';
|
|
6
|
+
|
|
7
|
+
export function planContentSetup(root, publication, sourcePlan) {
|
|
8
|
+
if (isNativeFramework(publication.framework)) return planNativeContent(root, publication.framework, publication.config);
|
|
9
|
+
if (publication.framework === 'astro') return planAstroContent(root, publication.config);
|
|
10
|
+
return publication.framework === 'vite'
|
|
11
|
+
? planViteContent(root, publication.config, sourcePlan)
|
|
12
|
+
: planNextContent(root, publication.config);
|
|
13
|
+
}
|