@universalcms/connect 0.0.0-stage → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +779 -2
  2. package/dist/angular-runtime.d.mts +4 -0
  3. package/dist/angular-runtime.mjs +7 -0
  4. package/dist/angular-setup.mjs +124 -0
  5. package/dist/astro-runtime.d.mts +6 -0
  6. package/dist/astro-runtime.mjs +255 -0
  7. package/dist/astro-setup.mjs +215 -0
  8. package/dist/astro-source.mjs +451 -0
  9. package/dist/astro-website.d.mts +5 -0
  10. package/dist/astro-website.mjs +42 -0
  11. package/dist/cli.mjs +552 -0
  12. package/dist/connector-runtime.mjs +130 -0
  13. package/dist/content-next-setup.mjs +200 -0
  14. package/dist/content-registry-validation.mjs +40 -0
  15. package/dist/content-registry.mjs +44 -0
  16. package/dist/content-setup.mjs +13 -0
  17. package/dist/content-storefront-setup.mjs +155 -0
  18. package/dist/content-vite-setup.mjs +107 -0
  19. package/dist/core.mjs +205 -0
  20. package/dist/framework-ownership.mjs +68 -0
  21. package/dist/framework.mjs +54 -0
  22. package/dist/guided.mjs +335 -0
  23. package/dist/native-adapters.mjs +31 -0
  24. package/dist/native-chrome-model.d.mts +3 -0
  25. package/dist/native-chrome-model.mjs +14 -0
  26. package/dist/native-chrome-review.mjs +49 -0
  27. package/dist/native-chrome-source.mjs +69 -0
  28. package/dist/native-route-review.mjs +152 -0
  29. package/dist/native-routes.mjs +27 -0
  30. package/dist/native-website-model.d.mts +18 -0
  31. package/dist/native-website-model.mjs +67 -0
  32. package/dist/native-website-server.d.mts +1 -0
  33. package/dist/native-website-server.mjs +35222 -0
  34. package/dist/native-website-server.mjs.LEGAL.txt +56 -0
  35. package/dist/native-website.d.mts +3 -0
  36. package/dist/native-website.mjs +120 -0
  37. package/dist/native-website.mjs.LEGAL.txt +56 -0
  38. package/dist/next-publication.d.mts +12 -0
  39. package/dist/next-publication.mjs +107 -0
  40. package/dist/next-settings-layout.mjs +233 -0
  41. package/dist/next-source.d.mts +8 -0
  42. package/dist/next-source.mjs +93 -0
  43. package/dist/page-discovery.mjs +236 -0
  44. package/dist/page-mapping-review.mjs +160 -0
  45. package/dist/preflight-recovery.mjs +122 -0
  46. package/dist/preflight.mjs +138 -0
  47. package/dist/project-files.mjs +56 -0
  48. package/dist/public-bindings.d.mts +3 -0
  49. package/dist/public-bindings.mjs +28 -0
  50. package/dist/publication-browser.d.mts +5 -0
  51. package/dist/publication-browser.mjs +258 -0
  52. package/dist/publication-check.mjs +141 -0
  53. package/dist/publication-document.d.mts +2 -0
  54. package/dist/publication-document.mjs +59 -0
  55. package/dist/publication-next-setup.mjs +134 -0
  56. package/dist/publication-setup.mjs +138 -0
  57. package/dist/publication-verification.mjs +85 -0
  58. package/dist/publication.css +6 -0
  59. package/dist/publication.d.mts +31 -0
  60. package/dist/publication.mjs +320 -0
  61. package/dist/react-publication.d.mts +22 -0
  62. package/dist/react-publication.mjs +301 -0
  63. package/dist/react-website.d.mts +15 -0
  64. package/dist/react-website.mjs +735 -0
  65. package/dist/reviewed-commerce-ownership.mjs +26 -0
  66. package/dist/reviewed-commerce-settings-setup.mjs +243 -0
  67. package/dist/reviewed-native-ownership.mjs +118 -0
  68. package/dist/reviewed-storefront-source.mjs +227 -0
  69. package/dist/runtime-dependencies.mjs +59 -0
  70. package/dist/setup-environment.mjs +307 -0
  71. package/dist/source-bindings.mjs +77 -0
  72. package/dist/source-check.mjs +149 -0
  73. package/dist/source-collect.mjs +80 -0
  74. package/dist/source-config.mjs +20 -0
  75. package/dist/source-extract.mjs +514 -0
  76. package/dist/source-finalize.mjs +52 -0
  77. package/dist/source-routes.mjs +65 -0
  78. package/dist/source-setup.mjs +490 -0
  79. package/dist/staged-setup.mjs +287 -0
  80. package/dist/static-runtime.mjs +67 -0
  81. package/dist/static-setup.mjs +136 -0
  82. package/dist/svelte-runtime.d.mts +27 -0
  83. package/dist/svelte-runtime.mjs +40 -0
  84. package/dist/svelte-setup.mjs +135 -0
  85. package/dist/svelte-template.mjs +48 -0
  86. package/dist/templates.mjs +248 -0
  87. package/dist/vendor/analyze-project.mjs +82 -0
  88. package/dist/vendor/astro-compiler/LICENSE +53 -0
  89. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  90. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  91. package/dist/vendor/astro-compiler/node/index.js +1 -0
  92. package/dist/vendor/check-delivery.mjs +158 -0
  93. package/dist/vendor/dependency-diagnostics.mjs +72 -0
  94. package/dist/vendor/inspect-installed-dependencies.mjs +125 -0
  95. package/dist/vendor/project-analysis.mjs +498 -0
  96. package/dist/vendor/react-LICENSE.txt +21 -0
  97. package/dist/vendor/react-document.mjs +124 -0
  98. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  99. package/dist/vendor/react-form.mjs +63 -0
  100. package/dist/vendor/react-safety.mjs +47 -0
  101. package/dist/vendor/source-manifest-provider.mjs +104 -0
  102. package/dist/vendor/typescript-LICENSE.txt +55 -0
  103. package/dist/vendor/typescript.cjs +200276 -0
  104. package/dist/vite-handler.mjs +54 -0
  105. package/dist/vite-publication-server.d.mts +3 -0
  106. package/dist/vite-publication-server.mjs +82 -0
  107. package/dist/vite-publication.d.mts +15 -0
  108. package/dist/vite-publication.mjs +241 -0
  109. package/dist/vite-source-discovery.mjs +315 -0
  110. package/dist/vite-source.d.mts +30 -0
  111. package/dist/vite-source.mjs +141 -0
  112. package/dist/vue-runtime.d.mts +10 -0
  113. package/dist/vue-runtime.mjs +50 -0
  114. package/dist/vue-server.d.mts +2 -0
  115. package/dist/vue-server.mjs +58 -0
  116. package/dist/vue-setup.mjs +127 -0
  117. package/dist/vue-source.mjs +168 -0
  118. package/dist/website-check.mjs +41 -0
  119. package/dist/website-http.mjs +149 -0
  120. package/dist/website-model.d.mts +20 -0
  121. package/dist/website-model.mjs +155 -0
  122. package/dist/website-next.d.mts +2 -0
  123. package/dist/website-next.mjs +29 -0
  124. package/dist/website-node.d.mts +3 -0
  125. package/dist/website-node.mjs +50 -0
  126. package/dist/website-runtime.d.mts +13 -0
  127. package/dist/website-runtime.mjs +396 -0
  128. package/dist/website-schema.mjs +22175 -0
  129. package/dist/website-setup.mjs +256 -0
  130. package/dist/website.css +49 -0
  131. package/dist/wizard.mjs +298 -0
  132. package/package.json +60 -4
@@ -0,0 +1,215 @@
1
+ /** Bounded Astro integration planning. Public source only; never loads project code. */
2
+ import { lstat, opendir, realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import ts from './vendor/typescript.cjs';
5
+ import { projectRoot, readProjectFile, SetupError } from './core.mjs';
6
+ import { contentDigest, readContentRegistry, readFrozenContent, CONTENT_BINDINGS_FILE } from './content-registry.mjs';
7
+ import { WEBSITE_SECTIONS } from './website-http.mjs';
8
+ import { inspectAstroSource } from './astro-source.mjs';
9
+ import { isExcludedPublicationPath } from './publication.mjs';
10
+
11
+ export const ASTRO_CONFIG = '.universalcms/astro.config.mjs';
12
+ export const ASTRO_MANIFEST = '.universalcms/astro.integration.json';
13
+ const MIDDLEWARE = '.universalcms/astro.middleware.mjs', PAGE = '.universalcms/astro-page.astro', SERVE = '.universalcms/astro.serve.mjs';
14
+ const SETTINGS = '.universalcms/website.integration.json';
15
+ const json = value => `${JSON.stringify(value, null, 2)}\n`;
16
+ const fail = message => { throw new SetupError(message); };
17
+ const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
18
+ const nameOf = node => ts.isIdentifier(node) || ts.isStringLiteral(node) ? node.text : null;
19
+ export function supportedAstroVersion(value) {
20
+ const m = typeof value === 'string' && /^(?:\^|~)?(\d+)\.(\d+)\.(\d+)$/.exec(value);
21
+ return !!m && +m[1] === 7 && (+m[2] > 2 || +m[2] === 2 && +m[3] >= 1);
22
+ }
23
+ const version = (value, major, minor = 0) => typeof value === 'string' && new RegExp(`^(?:\\^|~)?${major}\\.(\\d+)\\.\\d+$`).test(value) && +value.replace(/^[~^]/, '').split('.')[1] >= minor;
24
+ async function ordinary(root, name) { const content = await readProjectFile(root, name); if (content !== null && (await lstat(path.join(root, name))).nlink !== 1) fail('Astro integration files must be ordinary files without hard links.'); return content; }
25
+ async function absent(root, name) { try { await lstat(path.join(root, name)); } catch (error) { if (error.code === 'ENOENT') return; throw error; } fail('An Astro middleware, endpoint or public asset conflicts with the managed integration. Preserve it and request a reviewed adapter.'); }
26
+
27
+ // The standalone Node adapter serves public assets before Astro middleware.
28
+ // Static pages or private/connector paths must never shadow publication checks.
29
+ // Inspect metadata only: no public asset or potential private file is opened.
30
+ export async function inspectAstroPublicAssets(root) {
31
+ const publicRoot = path.join(root,'public');
32
+ try { await lstat(publicRoot); } catch(error) { if(error.code==='ENOENT')return; throw error; }
33
+ const extensions = new Set(['.avif','.bmp','.css','.csv','.eot','.gif','.ico','.jpeg','.jpg','.js','.json','.map','.mjs','.mp3','.mp4','.ogg','.otf','.pdf','.png','.svg','.ttf','.txt','.wasm','.webm','.webmanifest','.webp','.woff','.woff2','.xml']);
34
+ const reserved = new Set(['robots.txt','sitemap.xml','.well-known/universalcms-publication-client.js']);
35
+ let files=0,directories=0; const started=Date.now();
36
+ const refused = () => fail('Astro public assets need review: static pages, private paths, reserved endpoints, linked files or excessive inventories can bypass publication middleware. Existing files were preserved.');
37
+ const walk = async (directory,relative='',depth=0) => {
38
+ if(++directories>256||depth>12||Date.now()-started>8000)refused();
39
+ const info=await lstat(directory);
40
+ if(!info.isDirectory()||info.isSymbolicLink()||await realpath(directory)!==directory)refused();
41
+ for await(const item of await opendir(directory)) {
42
+ if(++files>5000||Date.now()-started>8000)refused();
43
+ const filename=path.join(directory,item.name), name=relative+item.name, lower=item.name.toLowerCase();
44
+ if(item.isSymbolicLink()||/[\\%?#:\u0000-\u001f\u007f]/.test(item.name)||lower.startsWith('.')&&lower!=='.well-known'||reserved.has(name.toLowerCase()))refused();
45
+ const stem=item.isDirectory()?lower:lower.slice(0,lower.length-path.extname(lower).length);
46
+ if(!['assets','static','.well-known'].includes(stem)&&/^[a-z0-9_.@-]+$/.test(stem)&&isExcludedPublicationPath('/'+stem)||['api','_astro'].includes(lower))refused();
47
+ if(item.isDirectory()) { await walk(filename,name+'/',depth+1); continue; }
48
+ const stat=await lstat(filename);
49
+ if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1||await realpath(filename)!==filename||!extensions.has(path.extname(lower)))refused();
50
+ }
51
+ };
52
+ await walk(publicRoot);
53
+ }
54
+
55
+ function reviewConfig(source, filename) {
56
+ if (source === null) return { react: false, node: false };
57
+ const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
58
+ if (file.parseDiagnostics.length) fail('The Astro configuration cannot be inspected safely.');
59
+ const imports = new Map(); let config;
60
+ for (const statement of file.statements) {
61
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
62
+ const specifier = statement.moduleSpecifier.text, clause = statement.importClause;
63
+ if (statement.attributes || !clause || clause.isTypeOnly) fail('Custom Astro configuration imports need review.');
64
+ if (specifier === 'astro/config' && !clause.name && clause.namedBindings && ts.isNamedImports(clause.namedBindings) && clause.namedBindings.elements.length === 1 && (clause.namedBindings.elements[0].propertyName?.text ?? clause.namedBindings.elements[0].name.text) === 'defineConfig') imports.set(clause.namedBindings.elements[0].name.text, 'defineConfig');
65
+ else if (['@astrojs/node', '@astrojs/react'].includes(specifier) && clause.name && !clause.namedBindings) imports.set(clause.name.text, specifier);
66
+ else fail('Only the ordinary Astro configuration and official Node/React integration imports are supported automatically.');
67
+ } else if (ts.isExportAssignment(statement) && !statement.isExportEquals && !config) {
68
+ const expression = statement.expression;
69
+ config = ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && imports.get(expression.expression.text) === 'defineConfig' && expression.arguments.length === 1 ? expression.arguments[0] : expression;
70
+ } else if (!ts.isEmptyStatement(statement)) fail('Dynamic Astro configuration requires a reviewed integration. Existing configuration was preserved.');
71
+ }
72
+ if (!config || !ts.isObjectLiteralExpression(config)) fail('Use an inspectable literal Astro configuration before automatic setup.');
73
+ const seen = new Set(), result = { react: false, node: false };
74
+ const factory = (node, specifier) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && imports.get(node.expression.text) === specifier;
75
+ for (const member of config.properties) {
76
+ const name = ts.isPropertyAssignment(member) ? nameOf(member.name) : null;
77
+ if (!name || seen.has(name)) fail('Spread, computed or duplicate Astro configuration needs manual review.');
78
+ seen.add(name); const value = member.initializer;
79
+ if (name === 'integrations') {
80
+ if (!ts.isArrayLiteralExpression(value) || value.elements.length > 1 || value.elements.some(item => !factory(item, '@astrojs/react') || item.arguments.length !== 0)) fail('Custom Astro integrations require explicit routing and build review.');
81
+ result.react = value.elements.length === 1;
82
+ } else if (name === 'adapter') {
83
+ if (!factory(value, '@astrojs/node') || value.arguments.length !== 1 || !ts.isObjectLiteralExpression(value.arguments[0]) || value.arguments[0].properties.length !== 1) fail('An existing Astro adapter requires explicit deployment review.');
84
+ const mode = value.arguments[0].properties[0];
85
+ if (!ts.isPropertyAssignment(mode) || nameOf(mode.name) !== 'mode' || !ts.isStringLiteral(mode.initializer) || mode.initializer.text !== 'standalone') fail('Automatic Astro setup requires the official Node standalone adapter.');
86
+ result.node = true;
87
+ } else if (name === 'output') { if (!ts.isStringLiteral(value) || !['static', 'server'].includes(value.text)) fail('The Astro output needs a reviewed Node server integration.'); }
88
+ else if (name === 'site') { if (!ts.isStringLiteral(value) || !/^https?:\/\/[^\s]+$/.test(value.text)) fail('The Astro site setting must be a public literal URL.'); }
89
+ else if (name === 'trailingSlash') { if (!ts.isStringLiteral(value) || !['ignore', 'never'].includes(value.text)) fail('Trailing-slash Astro routing needs explicit review.'); }
90
+ else if (name === 'compressHTML') { if (![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(value.kind)) fail('Astro configuration values must be literal.'); }
91
+ else if (name === 'scopedStyleStrategy') { if (!ts.isStringLiteral(value) || !['where', 'class', 'attribute'].includes(value.text)) fail('The Astro style strategy needs review.'); }
92
+ else fail('Custom Astro roots, routing, environment, Vite hooks or build configuration require a reviewed adapter.');
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function scriptsFor(manifest) {
98
+ if (!plain(manifest.scripts)) fail('Astro setup requires ordinary astro dev and astro build scripts.');
99
+ const scripts = { ...manifest.scripts }, suffix = ` --config ${ASTRO_CONFIG}`;
100
+ for (const operation of ['dev', 'build', 'preview']) {
101
+ if (operation === 'preview' && scripts.preview === undefined) continue;
102
+ let command = scripts[operation];
103
+ if (typeof command !== 'string') fail('Astro setup requires ordinary astro dev and astro build scripts.');
104
+ if (command.endsWith(suffix)) command = command.slice(0, -suffix.length);
105
+ const expected = `astro ${operation}`;
106
+ if (!command.startsWith(expected) || !(operation === 'build' ? /^$/ : /^(?: --(?:host(?: (?:localhost|127\.0\.0\.1|0\.0\.0\.0))?|port [0-9]{1,5}|open))*$/).test(command.slice(expected.length))) fail('Custom Astro scripts, roots, ports or config flags need a reviewed deployment integration.');
107
+ for (const [, port] of command.matchAll(/--port (\d+)/g)) if (+port < 1 || +port > 65535) fail('Astro startup needs a valid port.');
108
+ scripts[operation] = command + suffix;
109
+ }
110
+ const start = `node ${SERVE}`;
111
+ if (scripts.start !== undefined && scripts.start !== start && scripts.start !== 'astro dev') fail('An existing Astro production command needs explicit review; it was preserved.');
112
+ scripts.start = start;
113
+ // Revalidate source/layout/public ownership before the supported commands.
114
+ // A custom hook could create static shadow pages after our check; preserve it
115
+ // and require explicit review instead of guessing its execution order.
116
+ const validation = 'universalcms-connect sync-source';
117
+ for(const operation of ['dev','build','start',...(scripts.preview===undefined?[]:['preview'])]) {
118
+ const hook='pre'+operation;
119
+ if(scripts[hook]!==undefined&&scripts[hook]!==validation)fail('Existing Astro lifecycle hooks require review before automatic setup. Preserve the hook and integrate source validation explicitly.');
120
+ scripts[hook]=validation;
121
+ }
122
+ return { ...manifest, scripts };
123
+ }
124
+
125
+ /** Returns only public observations, with no package imports or environment reads. */
126
+ export async function inspectAstroProject(directory, suppliedManifest) {
127
+ const root = await projectRoot(directory), packageText = await ordinary(root, 'package.json');
128
+ let manifest; try { manifest = suppliedManifest ?? JSON.parse(packageText); } catch { fail('Astro setup needs a valid package manifest.'); }
129
+ const deps = { ...manifest.devDependencies, ...manifest.dependencies };
130
+ if (manifest.workspaces || manifest.type !== 'module' || !supportedAstroVersion(deps.astro)) fail('Automatic setup supports one ESM Astro 7.2.1–7.x website. Older or different Astro versions need a deliberate upgrade or reviewed adapter.');
131
+ if (!version(deps['@astrojs/node'], 11) || !version(deps['@astrojs/react'], 7) || !version(deps.react, 19, 2) || !version(deps['react-dom'], 19, 2)) fail('Install the supported official Astro Node 11 and React 7 integrations, plus React and React DOM 19.2+, before setup.');
132
+ const currentNode = process.versions.node.split('.').map(Number);
133
+ if (currentNode[0] < 22 || currentNode[0] === 22 && currentNode[1] < 22) fail('Astro 7 requires Node 22.22 or newer. Upgrade the Node runtime deliberately before setup.');
134
+ let configFile = null, configText = null;
135
+ for (const extension of ['mjs', 'js', 'ts', 'cjs', 'mts', 'cts']) {
136
+ const name = `astro.config.${extension}`, text = await ordinary(root, name);
137
+ if (text !== null) { if (configFile || !['mjs', 'js', 'ts'].includes(extension)) fail('Multiple or CommonJS Astro configurations need manual review.'); configFile = name; configText = text; }
138
+ }
139
+ const config = reviewConfig(configText, configFile ?? 'astro.config.mjs');
140
+ for (const extension of ['js', 'ts', 'mjs', 'mts']) await absent(root, `src/middleware.${extension}`);
141
+ await absent(root, 'src/middleware');
142
+ await inspectAstroPublicAssets(root);
143
+ return { root, manifest, packageText, configFile, configText, ...config, nextPackage: scriptsFor(manifest) };
144
+ }
145
+
146
+ function configTemplate(project, publication) {
147
+ return `// Generated by @universalcms/connect. The original configuration remains owned by the website.\nimport sourceConfiguration from './source.config.json' with { type: 'json' };\nimport { defineConfig } from 'astro/config';\nimport node from '@astrojs/node';\nimport react from '@astrojs/react';\nimport { fileURLToPath } from 'node:url';\n${project.configFile ? `import existing from '../${project.configFile}';` : 'const existing = {};'}\nconst sourceRoot = new URL('../', import.meta.url);\nconst website = new URL(sourceConfiguration.websiteOrigin);\nconst integration = { name: 'universalcms-reviewed-astro', hooks: {\n 'astro:config:setup': ({ addMiddleware, injectRoute }) => {\n addMiddleware({ entrypoint: fileURLToPath(new URL('./astro.middleware.mjs', import.meta.url)), order: 'pre' });\n${publication ? " injectRoute({ pattern: '/[...cmsPath]', entrypoint: fileURLToPath(new URL('./astro-page.astro', import.meta.url)), prerender: false });\n" : ''} },\n 'astro:route:setup': ({ route }) => { route.prerender = false; },\n} };\nexport default defineConfig({ ...existing, root: fileURLToPath(sourceRoot), output: 'server', security: { allowedDomains: [{ hostname: website.hostname, protocol: website.protocol.slice(0, -1), port: website.port }] }, server: { port: Number(new URL(sourceConfiguration.websiteOrigin).port || (sourceConfiguration.websiteOrigin.startsWith('https:') ? 443 : 80)) }, vite: { server: { strictPort: true }, preview: { strictPort: true } }, adapter: ${project.node ? 'existing.adapter' : "node({ mode: 'standalone' })"}, integrations: [...(existing.integrations ?? []), ${project.react ? '' : 'react(), '}integration] });\n`;
148
+ }
149
+ function middlewareTemplate(publication) {
150
+ return `// Generated by @universalcms/connect. Server-only imports; never expose delivery credentials.\nimport { fileURLToPath } from 'node:url';\nimport { createAstroRuntime } from '@universalcms/connect/astro-runtime';\nimport sourceConfiguration from './source.config.json' with { type: 'json' };\nimport snapshot from './source.generated.json' with { type: 'json' };\n${publication ? "import configuration from './publication.config.json' with { type: 'json' };\nimport contentBindings from './content.bindings.json' with { type: 'json' };\n" : ''}const runtime = createAstroRuntime({ ${publication ? 'configuration, contentBindings, ' : ''}sourceConfiguration, snapshot, sourceRoot: process.cwd() });\nexport const onRequest = runtime.onRequest;\n`;
151
+ }
152
+ function catchallTemplate() { return `---\nimport { AstroWebsite } from '@universalcms/connect/astro-website';\nimport '@universalcms/connect/publication.css';\nimport '@universalcms/connect/website.css';\nexport const prerender = false;\nconst cms = Astro.locals.universalcms;\n// Excluded native/private paths can reach this fallback without CMS locals.\nif (!cms) return new Response(null, { status: 404, headers: { 'Cache-Control': 'private, no-store', 'X-Content-Type-Options': 'nosniff' } });\n---\n<html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head><body><AstroWebsite pathname={cms.pathname} scope={cms.scope} initialResult={cms.result} initialSettings={cms.settings} client:load /></body></html>\n`; }
153
+ function serveTemplate() { return `// Generated by @universalcms/connect. Run from the website root after astro build.\nimport { fileURLToPath } from 'node:url';\nprocess.chdir(fileURLToPath(new URL('../', import.meta.url)));\nimport configuration from './source.config.json' with { type: 'json' };\nconst port = Number(process.env.PORT ?? (new URL(configuration.websiteOrigin).port || (configuration.websiteOrigin.startsWith('https:') ? 443 : 80)));\nif (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Configure a valid website PORT.');\nprocess.env.PORT = String(port);\nprocess.env.HOST ??= '0.0.0.0';\nawait import('../dist/server/entry.mjs');\n`; }
154
+ function generated(project, publication) { return [[ASTRO_CONFIG, configTemplate(project, publication)], [MIDDLEWARE, middlewareTemplate(publication)], [SERVE, serveTemplate()], ...(publication ? [[PAGE, catchallTemplate()]] : [])]; }
155
+
156
+ async function previousManifest(root, project) {
157
+ const text = await ordinary(root, ASTRO_MANIFEST);
158
+ if (text === null) return null;
159
+ let value; try { value = JSON.parse(text); } catch { fail('The Astro integration manifest needs review.'); }
160
+ if (!plain(value) || Object.keys(value).sort().join() !== 'configFile,configHash,files,publication,version' || value.version !== 1 || typeof value.publication !== 'boolean' || value.configFile !== project.configFile || value.configHash !== (project.configText === null ? null : contentDigest(project.configText)) || !Array.isArray(value.files)) fail('The original Astro configuration changed after setup. Preserve it and review the integration.');
161
+ const expected = generated(project, value.publication).map(([name, content]) => ({ path: name, sha256: contentDigest(content) }));
162
+ if (json(value.files) !== json(expected)) fail('The Astro integration ownership manifest was changed.');
163
+ for (const row of value.files) if (contentDigest(await ordinary(root, row.path) ?? '') !== row.sha256) fail('A generated Astro runtime adapter was changed or is missing. Preserve custom edits and review the integration.');
164
+ return { text, value };
165
+ }
166
+
167
+ /** Source owns generated runtime files and scripts; publication/content never rewrite them. */
168
+ export async function planAstroIntegration(root, { publication = false, sourceConfiguration } = {}) {
169
+ const project = await inspectAstroProject(root), prior = await previousManifest(root, project);
170
+ if (prior?.value.publication && !publication) publication = true;
171
+ if (!sourceConfiguration) { const text = await ordinary(root, '.universalcms/source.config.json'); if (text !== null) sourceConfiguration = JSON.parse(text); }
172
+ if (sourceConfiguration) {
173
+ const expectedPort = Number(new URL(sourceConfiguration.websiteOrigin).port || (sourceConfiguration.websiteOrigin.startsWith('https:') ? 443 : 80));
174
+ for (const name of ['dev','preview']) for (const [, port] of (project.manifest.scripts?.[name] ?? '').matchAll(/--port (\d+)/g)) if (+port !== expectedPort) fail('The Astro startup port differs from the saved website origin. Review the public origin or command before setup.');
175
+ }
176
+ const desired = generated(project, publication), files = [];
177
+ for (const [name, content] of desired) {
178
+ const current = await ordinary(root, name), previous = prior && generated(project, prior.value.publication).find(([file]) => file === name)?.[1];
179
+ if (current !== null && current !== content && current !== previous || current !== null && !prior) fail('An existing Astro adapter needs explicit review; setup preserved it.');
180
+ files.push({ path: name, content, state: current === content ? 'unchanged' : current === null ? 'create' : 'update', ...(current !== null && current !== content ? { previous: current } : {}) });
181
+ }
182
+ const content = json({ version: 1, publication, configFile: project.configFile, configHash: project.configText === null ? null : contentDigest(project.configText), files: desired.map(([name, body]) => ({ path: name, sha256: contentDigest(body) })) });
183
+ files.push({ path: ASTRO_MANIFEST, content, state: prior?.text === content ? 'unchanged' : prior ? 'update' : 'create', ...(prior && prior.text !== content ? { previous: prior.text } : {}) });
184
+ const packageContent = JSON.stringify(project.nextPackage) === JSON.stringify(project.manifest) ? project.packageText : json(project.nextPackage);
185
+ files.push({ path: 'package.json', content: packageContent, state: packageContent === project.packageText ? 'unchanged' : 'update', ...(packageContent !== project.packageText ? { previous: project.packageText } : {}) });
186
+ if (project.configFile) files.push({ path: project.configFile, content: project.configText, state: 'unchanged' });
187
+ return { root, framework: 'astro', files };
188
+ }
189
+
190
+ export async function validateInstalledAstro(root, config) {
191
+ const plan = await planAstroIntegration(root, { publication: true });
192
+ if (plan.files.some(file => file.state !== 'unchanged')) fail('The Astro runtime, scripts or middleware are incomplete. Finish the reviewed setup before saving a server key.');
193
+ const inspection = await inspectAstroSource(root, { locale: config.routes[0]?.locale ?? 'en', websiteOrigin: config.websiteOrigin });
194
+ if (inspection.routes.length !== config.routes.length || config.routes.some(route => !inspection.routes.some(row => ['path','kind','slug','locale'].every(name => row[name] === route[name])))) fail('Astro routes changed after publication setup. Review route ownership before continuing.');
195
+ if (await ordinary(root, '.universalcms/source.generated.json') !== json(inspection.snapshot)) fail('The Astro source snapshot changed or is missing. Restore the reviewed frozen inventory.');
196
+ const registry = await readContentRegistry(root);
197
+ if (!registry || registry.routes.some(row => !row.sourceFile.endsWith('.astro')) || registry.routes.length !== config.routes.length) fail('Astro published-content bindings are incomplete.');
198
+ for (const row of registry.routes) {
199
+ const frozen = await readFrozenContent(root, row);
200
+ for (const [key, content] of Object.entries(frozen.texts)) plan.files.push({ path: row[`${key}File`], content, state: 'unchanged' });
201
+ }
202
+ plan.files.push({ path: CONTENT_BINDINGS_FILE, content: await ordinary(root, CONTENT_BINDINGS_FILE), state: 'unchanged' });
203
+ for (const row of inspection.sourceFiles) if (!plan.files.some(file => file.path === row.path)) plan.files.push({ path: row.path, content: await ordinary(root, row.path), state: 'unchanged' });
204
+ return { ...plan, config };
205
+ }
206
+
207
+ export async function planAstroSettings(root, { config, source }) {
208
+ const plan = await validateInstalledAstro(root, config);
209
+ const content = json({ version: 1, framework: 'astro', siteId: config.siteId, environmentId: config.environmentId, apiOrigin: config.apiOrigin, websiteOrigin: config.websiteOrigin, adminOrigin: source.adminOrigin, sections: WEBSITE_SECTIONS, files: plan.files.filter(file => file.path !== 'package.json').map(file => ({ path: file.path, sha256: contentDigest(file.content) })) });
210
+ const previous = await ordinary(root, SETTINGS);
211
+ if (previous !== null && previous !== content) fail('The Astro website settings ownership or scope changed. Review it before continuing.');
212
+ plan.files.push({ path: SETTINGS, content, state: previous === null ? 'create' : 'unchanged' });
213
+ for (const name of ['.universalcms/publication.config.json', '.universalcms/source.config.json', '.universalcms/source.generated.json']) plan.files.push({ path: name, content: await ordinary(root, name), state: 'unchanged' });
214
+ return { ...plan, sections: [...WEBSITE_SECTIONS] };
215
+ }