@universalcms/connect 0.1.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +694 -37
  2. package/dist/angular-runtime.d.mts +4 -0
  3. package/dist/angular-runtime.mjs +7 -0
  4. package/dist/angular-setup.mjs +124 -0
  5. package/dist/astro-runtime.d.mts +6 -0
  6. package/dist/astro-runtime.mjs +255 -0
  7. package/dist/astro-setup.mjs +215 -0
  8. package/dist/astro-source.mjs +451 -0
  9. package/dist/astro-website.d.mts +5 -0
  10. package/dist/astro-website.mjs +42 -0
  11. package/dist/cli.mjs +418 -15
  12. package/dist/connector-runtime.mjs +130 -0
  13. package/dist/content-next-setup.mjs +200 -0
  14. package/dist/content-registry-validation.mjs +40 -0
  15. package/dist/content-registry.mjs +44 -0
  16. package/dist/content-setup.mjs +13 -0
  17. package/dist/content-storefront-setup.mjs +155 -0
  18. package/dist/content-vite-setup.mjs +107 -0
  19. package/dist/core.mjs +32 -51
  20. package/dist/framework-ownership.mjs +68 -0
  21. package/dist/framework.mjs +54 -0
  22. package/dist/guided.mjs +335 -0
  23. package/dist/native-adapters.mjs +31 -0
  24. package/dist/native-chrome-model.d.mts +3 -0
  25. package/dist/native-chrome-model.mjs +14 -0
  26. package/dist/native-chrome-review.mjs +49 -0
  27. package/dist/native-chrome-source.mjs +69 -0
  28. package/dist/native-route-review.mjs +152 -0
  29. package/dist/native-routes.mjs +27 -0
  30. package/dist/native-website-model.d.mts +18 -0
  31. package/dist/native-website-model.mjs +67 -0
  32. package/dist/native-website-server.d.mts +1 -0
  33. package/dist/native-website-server.mjs +35222 -0
  34. package/dist/native-website-server.mjs.LEGAL.txt +56 -0
  35. package/dist/native-website.d.mts +3 -0
  36. package/dist/native-website.mjs +120 -0
  37. package/dist/native-website.mjs.LEGAL.txt +56 -0
  38. package/dist/next-publication.d.mts +12 -0
  39. package/dist/next-publication.mjs +107 -0
  40. package/dist/next-settings-layout.mjs +233 -0
  41. package/dist/next-source.d.mts +8 -0
  42. package/dist/next-source.mjs +93 -0
  43. package/dist/page-discovery.mjs +236 -0
  44. package/dist/page-mapping-review.mjs +160 -0
  45. package/dist/preflight-recovery.mjs +122 -0
  46. package/dist/preflight.mjs +138 -0
  47. package/dist/project-files.mjs +56 -0
  48. package/dist/public-bindings.d.mts +3 -0
  49. package/dist/public-bindings.mjs +28 -0
  50. package/dist/publication-browser.d.mts +5 -0
  51. package/dist/publication-browser.mjs +258 -0
  52. package/dist/publication-check.mjs +141 -0
  53. package/dist/publication-document.d.mts +2 -0
  54. package/dist/publication-document.mjs +59 -0
  55. package/dist/publication-next-setup.mjs +134 -0
  56. package/dist/publication-setup.mjs +138 -0
  57. package/dist/publication-verification.mjs +85 -0
  58. package/dist/publication.css +6 -0
  59. package/dist/publication.d.mts +31 -0
  60. package/dist/publication.mjs +320 -0
  61. package/dist/react-publication.d.mts +22 -0
  62. package/dist/react-publication.mjs +301 -0
  63. package/dist/react-website.d.mts +15 -0
  64. package/dist/react-website.mjs +735 -0
  65. package/dist/reviewed-commerce-ownership.mjs +26 -0
  66. package/dist/reviewed-commerce-settings-setup.mjs +243 -0
  67. package/dist/reviewed-native-ownership.mjs +118 -0
  68. package/dist/reviewed-storefront-source.mjs +227 -0
  69. package/dist/runtime-dependencies.mjs +59 -0
  70. package/dist/setup-environment.mjs +70 -0
  71. package/dist/source-bindings.mjs +77 -0
  72. package/dist/source-check.mjs +149 -0
  73. package/dist/source-collect.mjs +80 -0
  74. package/dist/source-config.mjs +20 -0
  75. package/dist/source-extract.mjs +514 -0
  76. package/dist/source-finalize.mjs +52 -0
  77. package/dist/source-routes.mjs +65 -0
  78. package/dist/source-setup.mjs +490 -0
  79. package/dist/staged-setup.mjs +287 -0
  80. package/dist/static-runtime.mjs +67 -0
  81. package/dist/static-setup.mjs +136 -0
  82. package/dist/svelte-runtime.d.mts +27 -0
  83. package/dist/svelte-runtime.mjs +40 -0
  84. package/dist/svelte-setup.mjs +135 -0
  85. package/dist/svelte-template.mjs +48 -0
  86. package/dist/templates.mjs +167 -3
  87. package/dist/vendor/astro-compiler/LICENSE +53 -0
  88. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  89. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  90. package/dist/vendor/astro-compiler/node/index.js +1 -0
  91. package/dist/vendor/project-analysis.mjs +56 -4
  92. package/dist/vendor/react-LICENSE.txt +21 -0
  93. package/dist/vendor/react-document.mjs +124 -0
  94. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  95. package/dist/vendor/react-form.mjs +63 -0
  96. package/dist/vendor/react-safety.mjs +47 -0
  97. package/dist/vendor/source-manifest-provider.mjs +104 -0
  98. package/dist/vendor/typescript-LICENSE.txt +55 -0
  99. package/dist/vendor/typescript.cjs +200276 -0
  100. package/dist/vite-publication-server.d.mts +3 -0
  101. package/dist/vite-publication-server.mjs +82 -0
  102. package/dist/vite-publication.d.mts +15 -0
  103. package/dist/vite-publication.mjs +241 -0
  104. package/dist/vite-source-discovery.mjs +315 -0
  105. package/dist/vite-source.d.mts +30 -0
  106. package/dist/vite-source.mjs +141 -0
  107. package/dist/vue-runtime.d.mts +10 -0
  108. package/dist/vue-runtime.mjs +50 -0
  109. package/dist/vue-server.d.mts +2 -0
  110. package/dist/vue-server.mjs +58 -0
  111. package/dist/vue-setup.mjs +127 -0
  112. package/dist/vue-source.mjs +168 -0
  113. package/dist/website-check.mjs +41 -0
  114. package/dist/website-http.mjs +149 -0
  115. package/dist/website-model.d.mts +20 -0
  116. package/dist/website-model.mjs +155 -0
  117. package/dist/website-next.d.mts +2 -0
  118. package/dist/website-next.mjs +29 -0
  119. package/dist/website-node.d.mts +3 -0
  120. package/dist/website-node.mjs +50 -0
  121. package/dist/website-runtime.d.mts +13 -0
  122. package/dist/website-runtime.mjs +396 -0
  123. package/dist/website-schema.mjs +22175 -0
  124. package/dist/website-setup.mjs +256 -0
  125. package/dist/website.css +49 -0
  126. package/dist/wizard.mjs +237 -38
  127. package/package.json +43 -7
@@ -0,0 +1,80 @@
1
+ import { projectRoot, readProjectFile } from './project-files.mjs';
2
+ import { inspectSourceRoutes } from './source-routes.mjs';
3
+ import { extractStaticPage } from './source-extract.mjs';
4
+ import { nextPublicationProxyTemplate } from './publication-next-setup.mjs';
5
+ import { readContentRegistry, readFrozenContent } from './content-registry.mjs';
6
+ import { inspectReviewedNativeOwnership } from './reviewed-native-ownership.mjs';
7
+ import { REVIEWED_NEXT_NATIVE_PROFILE } from './native-routes.mjs';
8
+ import { inspectReviewedCommerceOwnership } from './reviewed-commerce-ownership.mjs';
9
+
10
+ const protectedSegment = /^(?:admin|account|dashboard|profile|auth|login|register|checkout|orders|settings|billing|portal|patient|patients|vault|records)(?:-|$)|(?:^|-)vault$|^(?:medical|health)-records(?:-|$)/;
11
+ const accessLogic = /\b(?:cookies|headers|useSession|getServerSession|auth|requireAuth|currentUser|redirect)\s*\(/;
12
+
13
+ /** Parse public static source only. No page execution, HTML fetching or CMS writes. */
14
+ export async function collectSource({ root: directory, locale = 'en', skipSlugs = [], excludedRoutes = [] }) {
15
+ const root = await projectRoot(directory), inventory = await inspectSourceRoutes(root);
16
+ if (inventory.routes.includes('') && inventory.routes.includes('home')) throw Error('Ambiguous home identity');
17
+ const entries = [], warnings = [];
18
+ const content = await readContentRegistry(root);
19
+ let websiteSettings = null;
20
+ if (await readProjectFile(root, '.universalcms/website.integration.json') !== null) {
21
+ // Website runtimes (for example a bundled Next source route) must not bundle
22
+ // the setup toolchain and its framework compilers. Node resolves it on demand.
23
+ // A non-literal specifier keeps bundlers from copying the module as an asset.
24
+ const setup = new URL(['.', 'website-setup.mjs'].join('/'), import.meta.url).href;
25
+ const { planWebsiteSettings } = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ setup);
26
+ websiteSettings = await planWebsiteSettings(root);
27
+ }
28
+ const nativeOwnership = await inspectReviewedNativeOwnership(root) ?? await inspectReviewedCommerceOwnership(root);
29
+ // The reviewed product detail module accounts for exactly one dynamic route.
30
+ // Any other unresolved route remains in the diagnostic count for review.
31
+ const reviewedDynamic = name => nativeOwnership?.dynamicDirectories?.includes(name) || nativeOwnership?.profile === 'reviewed-icecream-commerce-2026-10-15' && name === 'src/app/menu/[id]' || websiteSettings?.files.some(file => ['js','jsx','ts','tsx'].some(ext => file.path === `${name}/page.${ext}`));
32
+ let skippedPages = 0, unsupportedRoutes = inventory.unresolvedPaths.filter(name => !reviewedDynamic(name)).length, bytes = 0;
33
+ const hasMiddleware = (await Promise.all(['middleware', 'src/middleware', 'proxy', 'src/proxy'].flatMap(name => ['js', 'ts'].map(async extension => {
34
+ const content = await readProjectFile(root, `${name}.${extension}`);
35
+ if (!content) return false;
36
+ if (websiteSettings?.files.some(file => file.path === `${name}.${extension}` && file.content === content)) return false;
37
+ // Our exact generated proxy only filters publications; it adds no private
38
+ // application content. Every other middleware still requires access review.
39
+ return !(extension === 'ts' && (name === 'proxy' && [nextPublicationProxyTemplate('app'), nextPublicationProxyTemplate('app', true)].includes(content) || name === 'src/proxy' && [nextPublicationProxyTemplate('src/app'), nextPublicationProxyTemplate('src/app', true)].includes(content)));
40
+ })))).some(Boolean);
41
+ const excludes = pathname => excludedRoutes.some(route => route === '/' || pathname === route || pathname.startsWith(`${route}/`));
42
+ const started = Date.now();
43
+ for (const route of [...inventory.routes].sort()) {
44
+ if (Date.now() - started > 8000) throw Error('Source inspection deadline');
45
+ const slug = route || 'home', filename = inventory.files[slug];
46
+ if (nativeOwnership?.nativePaths.includes(`/${route}`)) { skippedPages++; continue; }
47
+ if (skipSlugs.includes(slug) || excludes(`/${route}`)) { skippedPages++; continue; }
48
+ // Conservative guards, not a replacement for choosing a public-content root.
49
+ if (hasMiddleware || route.split('/').some(segment => protectedSegment.test(segment))) {
50
+ unsupportedRoutes++; warnings.push({ slug, codes: ['access-review-required'] }); continue;
51
+ }
52
+ const bound = content?.routes.find(row => row.sourceFile === filename && row.path === `/${route}` && row.kind === 'page' && row.slug === slug && row.locale === locale);
53
+ const source = await readProjectFile(root, filename);
54
+ if (source === null) throw Error('Source changed during inspection');
55
+ bytes += Buffer.byteLength(source);
56
+ if (bytes > 8 * 1024 * 1024) throw Error('Source inventory byte budget');
57
+ let guarded = accessLogic.test(source);
58
+ const parts = filename.split('/'); parts.pop();
59
+ while (parts.length) {
60
+ for (const ext of ['js', 'jsx', 'ts', 'tsx']) {
61
+ const layout = await readProjectFile(root, `${parts.join('/')}/layout.${ext}`);
62
+ if (layout && accessLogic.test(layout)) guarded = true;
63
+ }
64
+ if (parts.at(-1) === 'app') break;
65
+ parts.pop();
66
+ }
67
+ if (guarded) { unsupportedRoutes++; warnings.push({ slug, codes: ['access-review-required'] }); continue; }
68
+ // A frozen baseline does not freeze authorization. Ancestor layouts remain
69
+ // current application policy and must be checked before exporting its text.
70
+ if (bound) { entries.push((await readFrozenContent(root, bound)).entry); continue; }
71
+ const result = extractStaticPage({ source, slug, locale, filename });
72
+ if (Date.now() - started > 8000) throw Error('Source inspection deadline');
73
+ if (result.entry) entries.push(result.entry); else unsupportedRoutes++;
74
+ const codes = [...(result.warnings ?? []), ...(result.entry && content ? ['content-binding-required'] : [])];
75
+ if (codes.length) warnings.push({ slug, codes });
76
+ }
77
+ const snapshot = { version: 1, entries, diagnostics: { discoveredPages: inventory.routes.length, automaticPages: entries.length, skippedPages, unsupportedRoutes, warnings, ...(nativeOwnership && nativeOwnership.profile !== REVIEWED_NEXT_NATIVE_PROFILE ? { nativeOwnership: { profile:nativeOwnership.profile,paths:nativeOwnership.nativePaths } } : {}) } };
78
+ if (Buffer.byteLength(JSON.stringify(snapshot)) > 20 * 1024 * 1024) throw Error('Source response byte budget');
79
+ return snapshot;
80
+ }
@@ -0,0 +1,20 @@
1
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+ const slug = /^[a-z0-9]+(?:[-/][a-z0-9]+)*$/;
3
+ const fail = () => { throw Error('Invalid automatic source configuration'); };
4
+ function origin(value) {
5
+ if (typeof value !== 'string' || value.length > 2048 || /[\s\\]|cms_server\.|sk-[\w-]{12,}/i.test(value)) return fail();
6
+ const url = new URL(value);
7
+ if (url.username || url.password || url.search || url.hash || url.pathname !== '/' ||
8
+ !(url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) return fail();
9
+ return url.origin;
10
+ }
11
+ export function validateSourceConfiguration(value) {
12
+ const names = ['version', 'siteId', 'environmentId', 'websiteOrigin', 'adminOrigin', 'locale', 'skipSlugs', 'excludedRoutes', 'publicStaticPages', 'exportClosed'];
13
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !names.includes(key)) || (value.exportClosed !== undefined && value.exportClosed !== true) || value.version !== 1 || value.publicStaticPages !== true || typeof value.siteId !== 'string' || typeof value.environmentId !== 'string' || !uuid.test(value.siteId) || !uuid.test(value.environmentId)) return fail();
14
+ const locale = value.locale ?? 'en';
15
+ if (typeof locale !== 'string' || locale.length > 35 || Intl.getCanonicalLocales(locale).length !== 1) return fail();
16
+ const skipSlugs = value.skipSlugs ?? [], excludedRoutes = value.excludedRoutes ?? [];
17
+ if (!Array.isArray(skipSlugs) || skipSlugs.length > 1000 || skipSlugs.some(item => typeof item !== 'string' || item.length > 160 || !slug.test(item)) || new Set(skipSlugs).size !== skipSlugs.length) return fail();
18
+ if (!Array.isArray(excludedRoutes) || excludedRoutes.length > 1000 || excludedRoutes.some(item => typeof item !== 'string' || item.length > 161 || item !== '/' && (!item.startsWith('/') || !slug.test(item.slice(1)))) || new Set(excludedRoutes).size !== excludedRoutes.length) return fail();
19
+ return { version: 1, siteId: value.siteId.toLowerCase(), environmentId: value.environmentId.toLowerCase(), websiteOrigin: origin(value.websiteOrigin), adminOrigin: origin(value.adminOrigin), locale: Intl.getCanonicalLocales(locale)[0], skipSlugs: [...skipSlugs], excludedRoutes: [...excludedRoutes], publicStaticPages: true, ...(value.exportClosed ? { exportClosed: true } : {}) };
20
+ }
@@ -0,0 +1,514 @@
1
+ /**
2
+ * Bounded, inert TSX-to-text projection. This is an AST interpreter for a small
3
+ * data/JSX subset, never an evaluator of customer JavaScript. The result is a
4
+ * draft source projection, not proof that an arbitrary React page is complete.
5
+ */
6
+ import ts from './vendor/typescript.cjs';
7
+ import { createHash } from 'node:crypto';
8
+
9
+ export const SOURCE_EXTRACT_LIMITS = Object.freeze({ bytes: 512 * 1024, nodes: 30_000, depth: 100, steps: 200_000, blocks: 200, text: 30_000, array: 1000 });
10
+ const UNKNOWN = Symbol('unknown'), FN = Symbol('inert-function'), TRACE = Symbol('source-trace');
11
+ const privateKeys = new Set(['__proto__', 'prototype', 'constructor']);
12
+ const excludedTags = new Set(['form', 'input', 'textarea', 'select', 'option', 'script', 'style', 'iframe', 'object', 'embed', 'noscript', 'svg', 'canvas']);
13
+ const inlineTags = new Set(['span', 'strong', 'em', 'a', 'small', 'b', 'i', 'u', 's', 'del', 'sup', 'sub', 'code', 'mark', 'label']);
14
+ const partialCodes = new Set(['component-unresolved', 'expression-unresolved', 'collection-unresolved', 'conditional-unresolved', 'table-unresolved', 'source-mutation', 'component-flow-unresolved', 'presentation-filter-initial-state']);
15
+ const primitive = value => value === null || value === undefined || ['string', 'number', 'boolean'].includes(typeof value);
16
+ const literalText = value => typeof value === 'string' || typeof value === 'number' ? String(value) : '';
17
+ const normalize = value => value.replace(/\s+/g, ' ').trim();
18
+ const jsxText = value => value.replace(/&(?:amp|lt|gt|quot|apos|nbsp|#\d{1,7}|#x[\da-fA-F]{1,6});/g, entity => {
19
+ const named = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&nbsp;': ' ' };
20
+ if (Object.hasOwn(named, entity)) return named[entity];
21
+ const code = entity.startsWith('&#x') ? Number.parseInt(entity.slice(3, -1), 16) : Number(entity.slice(2, -1));
22
+ return code > 0 && code <= 0x10ffff && (code < 0xd800 || code > 0xdfff) ? String.fromCodePoint(code) : entity;
23
+ });
24
+ const unbox = node => { while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node))) node = node.expression; return node; };
25
+ const own = (object, key) => !privateKeys.has(String(key)) && Object.hasOwn(object, key);
26
+ const uuid = key => { const value = createHash('sha256').update(key).digest('hex'); return `${value.slice(0, 8)}-${value.slice(8, 12)}-5${value.slice(13, 16)}-a${value.slice(17, 20)}-${value.slice(20, 32)}`; };
27
+
28
+ /** No filesystem, network, dynamic import, eval, or customer function execution. */
29
+ export function extractStaticPage({ source, slug, locale = 'en', filename = 'page.tsx', includeBindings = false } = {}) {
30
+ const warnings = new Set();
31
+ let filteredRowsOmitted = false;
32
+ const failure = code => ({ entry: null, warnings: [...warnings, code].filter((value, index, all) => all.indexOf(value) === index).sort(), complete: false });
33
+ if (typeof source !== 'string' || Buffer.byteLength(source) > SOURCE_EXTRACT_LIMITS.bytes || typeof slug !== 'string' || slug.length > 160 || !/^[a-z0-9]+(?:[-/][a-z0-9]+)*$/.test(slug) || typeof locale !== 'string' || !/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(locale)) return failure('invalid-input');
34
+ let steps = 0;
35
+ const step = depth => { if (++steps > SOURCE_EXTRACT_LIMITS.steps || depth > SOURCE_EXTRACT_LIMITS.depth) throw Error('budget'); };
36
+ try {
37
+ // filename is a parser hint only; it is never opened or emitted.
38
+ const file = ts.createSourceFile(typeof filename === 'string' ? filename.slice(-200) : 'page.tsx', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
39
+ if (file.parseDiagnostics.length) return failure('syntax-error');
40
+ let nodes = 0;
41
+ const count = (node, depth = 0) => { if (++nodes > SOURCE_EXTRACT_LIMITS.nodes || depth > SOURCE_EXTRACT_LIMITS.depth) throw Error('budget'); ts.forEachChild(node, child => count(child, depth + 1)); };
42
+ count(file);
43
+ const moduleScope = new Map(), hooks = new Map(), setters = new Map(), selectors = new Set(), iconNames = new Set(), mutated = new Set(), stateImports = new Set(), reactImports = new Set();
44
+ let component;
45
+ const addDeclarations = (statements, scope) => {
46
+ for (const statement of statements) if (ts.isVariableStatement(statement) && statement.declarationList.flags & ts.NodeFlags.Const) {
47
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) scope.set(declaration.name.text, { expression: declaration.initializer, scope });
48
+ }
49
+ };
50
+ addDeclarations(file.statements, moduleScope);
51
+ for (const statement of file.statements) {
52
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === 'react') {
53
+ if (statement.importClause?.name) reactImports.add(statement.importClause.name.text);
54
+ const names = statement.importClause?.namedBindings;
55
+ if (names && ts.isNamespaceImport(names)) reactImports.add(names.name.text);
56
+ if (names && ts.isNamedImports(names)) for (const element of names.elements) if ((element.propertyName?.text ?? element.name.text) === 'useState') stateImports.add(element.name.text);
57
+ }
58
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === 'lucide-react') {
59
+ const names = statement.importClause?.namedBindings;
60
+ if (names && ts.isNamedImports(names)) for (const element of names.elements) iconNames.add(element.name.text);
61
+ }
62
+ if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) component = statement;
63
+ if (ts.isExportAssignment(statement)) {
64
+ const expression = unbox(statement.expression);
65
+ if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) component = expression;
66
+ else if (ts.isIdentifier(expression)) {
67
+ const declaration = file.statements.find(candidate => ts.isFunctionDeclaration(candidate) && candidate.name?.text === expression.text);
68
+ component = declaration ?? unbox(moduleScope.get(expression.text)?.expression);
69
+ }
70
+ }
71
+ }
72
+ if (!component || !component.body || !(ts.isFunctionDeclaration(component) || ts.isFunctionExpression(component) || ts.isArrowFunction(component))) return failure('component-unresolved');
73
+ // A zero-prop local component under React's Suspense boundary is still the
74
+ // same public page. Follow only this exact transparent wrapper shape; never
75
+ // infer custom HOCs, access guards, prop-dependent pages or imported code.
76
+ const suspenseNames = new Set();
77
+ for (const statement of file.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === 'react' && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) {
78
+ for (const item of statement.importClause.namedBindings.elements) if ((item.propertyName?.text ?? item.name.text) === 'Suspense') suspenseNames.add(item.name.text);
79
+ }
80
+ const wrappedReturn = candidate => {
81
+ if (candidate.parameters.length !== 0 || !ts.isBlock(candidate.body) || candidate.body.statements.length !== 1 || !ts.isReturnStatement(candidate.body.statements[0])) return null;
82
+ const body = unbox(candidate.body.statements[0].expression);
83
+ if (!body || !ts.isJsxElement(body) || !suspenseNames.has(body.openingElement.tagName.getText(file))) return null;
84
+ if (body.openingElement.attributes.properties.some(attribute => !ts.isJsxAttribute(attribute) || attribute.name.getText(file) !== 'fallback')) return null;
85
+ const children = body.children.filter(child => !(ts.isJsxText(child) && !child.text.trim()) && !(ts.isJsxExpression(child) && !child.expression));
86
+ const child = children.length === 1 ? children[0] : null;
87
+ if (!child || !ts.isJsxSelfClosingElement(child) || !ts.isIdentifier(child.tagName) || child.attributes.properties.length) return null;
88
+ const target = file.statements.find(statement => ts.isFunctionDeclaration(statement) && statement.name?.text === child.tagName.text);
89
+ return target?.body && target.parameters.length === 0 ? target : null;
90
+ };
91
+ const wrapped = wrappedReturn(component);
92
+ if (wrapped) component = wrapped;
93
+ const componentScope = new Map(moduleScope);
94
+ if (ts.isBlock(component.body)) {
95
+ addDeclarations(component.body.statements, componentScope);
96
+ for (const statement of component.body.statements) if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
97
+ const initializer = unbox(declaration.initializer);
98
+ if (!ts.isArrayBindingPattern(declaration.name) || !initializer || !ts.isCallExpression(initializer) || initializer.arguments.length !== 1) continue;
99
+ const callee = initializer.expression;
100
+ if (!(ts.isIdentifier(callee) && stateImports.has(callee.text) || ts.isPropertyAccessExpression(callee) && callee.name.text === 'useState' && ts.isIdentifier(callee.expression) && reactImports.has(callee.expression.text))) continue;
101
+ const [state, setter] = declaration.name.elements;
102
+ if (state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) {
103
+ hooks.set(state.name.text, initializer.arguments[0]); setters.set(setter.name.text, state.name.text);
104
+ componentScope.delete(state.name.text); componentScope.delete(setter.name.text);
105
+ }
106
+ }
107
+ }
108
+ // Mutated module objects are not immutable public data, even when declared
109
+ // const. Detect direct writes and unrecognized calls on their member paths.
110
+ const rootName = node => { node = unbox(node); while (node && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))) node = unbox(node.expression); return node && ts.isIdentifier(node) ? node.text : undefined; };
111
+ const aliases = new Map();
112
+ const aliasScan = node => {
113
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
114
+ // Objects/arrays can wrap references as well as directly alias them.
115
+ // Conservatively track every initializer dependency: an opaque call
116
+ // receiving { items: ITEMS } may sanitize or mutate ITEMS in place.
117
+ const values = aliases.get(node.name.text) ?? new Set();
118
+ const dependencies = item => {
119
+ step(0);
120
+ if (ts.isIdentifier(item) && item.text !== node.name.text && !(ts.isPropertyAccessExpression(item.parent) && item.parent.name === item) && !(ts.isPropertyAssignment(item.parent) && item.parent.name === item)) values.add(item.text);
121
+ ts.forEachChild(item, dependencies);
122
+ };
123
+ dependencies(node.initializer); aliases.set(node.name.text, values);
124
+ }
125
+ ts.forEachChild(node, aliasScan);
126
+ };
127
+ aliasScan(file);
128
+ for (const [state, initial] of hooks) { const from = rootName(initial); if (from) aliases.set(state, new Set([from])); }
129
+ const taint = (name, seen = new Set()) => {
130
+ step(0);
131
+ if (!name || seen.has(name)) return;
132
+ seen.add(name); if (moduleScope.has(name)) mutated.add(name);
133
+ for (const from of aliases.get(name) ?? []) taint(from, seen);
134
+ };
135
+ const taintReferences = node => {
136
+ if (ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) && !(ts.isPropertyAssignment(node.parent) && node.parent.name === node)) taint(node.text);
137
+ ts.forEachChild(node, taintReferences);
138
+ };
139
+ const taintAll = () => { for (const name of moduleScope.keys()) mutated.add(name); };
140
+ // Unsupported executable module flow (loops, conditional sanitization,
141
+ // destructuring mutations, etc.) invalidates the assumption that literal
142
+ // module constants still describe public data when the page renders.
143
+ for (const statement of file.statements) if (!(ts.isImportDeclaration(statement) || ts.isImportEqualsDeclaration(statement) || ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) || ts.isVariableStatement(statement) || ts.isFunctionDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEmptyStatement(statement) || ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression))) taintAll();
144
+ const eagerlyCalled = node => {
145
+ for (let parent = node.parent; parent; parent = parent.parent) {
146
+ if (parent === component) return true;
147
+ if (ts.isFunctionLike(parent)) return false;
148
+ if (ts.isSourceFile(parent)) return true;
149
+ }
150
+ return false;
151
+ };
152
+ const mutationScan = node => {
153
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) || ts.isPostfixUnaryExpression(node)) {
154
+ taint(rootName(ts.isBinaryExpression(node) ? node.left : node.operand));
155
+ }
156
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && !['map', 'filter', 'slice', 'includes', 'toLowerCase', 'toUpperCase', 'trim', 'join', 'toFixed'].includes(node.expression.name.text)) {
157
+ taint(rootName(node.expression.expression));
158
+ }
159
+ if (ts.isCallExpression(node)) {
160
+ const callee = node.expression;
161
+ const stateCall = ts.isIdentifier(callee) && stateImports.has(callee.text) || ts.isPropertyAccessExpression(callee) && callee.name.text === 'useState' && ts.isIdentifier(callee.expression) && reactImports.has(callee.expression.text);
162
+ const readCall = ts.isPropertyAccessExpression(callee) && ['map', 'filter', 'slice', 'includes', 'toLowerCase', 'toUpperCase', 'trim', 'join', 'toFixed'].includes(callee.name.text);
163
+ // Passing a constant (or an alias) to arbitrary project code can mutate
164
+ // or sanitize it. Do not project an earlier literal as current content.
165
+ if (!stateCall && !readCall) {
166
+ for (const argument of node.arguments) taintReferences(argument);
167
+ // An opaque call can reach a constant via closures, destructuring,
168
+ // getters or indirect aliases that this small interpreter does not
169
+ // model. At execution scope, fail closed for all module data.
170
+ if (eagerlyCalled(node)) taintAll();
171
+ }
172
+ }
173
+ ts.forEachChild(node, mutationScan);
174
+ };
175
+ mutationScan(file);
176
+ if (mutated.size) warnings.add('source-mutation');
177
+ // Only controls outside forms establish a presentation selector. Hook
178
+ // initializers still never become ordinary content-interpolation values.
179
+ const controlScan = (node, scope, excluded = false, depth = 0) => {
180
+ step(depth);
181
+ // A control hidden behind an unresolved guard is not evidence that the
182
+ // associated state is a public presentation filter.
183
+ if (ts.isConditionalExpression(node) || ts.isBinaryExpression(node) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken].includes(node.operatorToken.kind)) return;
184
+ if (ts.isCallExpression(node)) {
185
+ const rows = mapRows(node, scope, depth);
186
+ if (rows) for (const row of rows) if (row.node) controlScan(row.node, row.scope, excluded, depth + 1);
187
+ return;
188
+ }
189
+ const opening = ts.isJsxElement(node) ? node.openingElement : ts.isJsxSelfClosingElement(node) ? node : null;
190
+ if (opening) {
191
+ const tag = opening.tagName.getText(file);
192
+ excluded ||= tag === 'form' || /^[A-Z]/.test(tag) || opening.attributes.properties.some(attribute => ts.isJsxSpreadAttribute(attribute) || ts.isJsxAttribute(attribute) && ['data-cms-exclude', 'data-cms-private', 'hidden'].includes(attribute.name.getText(file)));
193
+ if (!excluded && ['button', 'input', 'select'].includes(tag)) for (const attribute of opening.attributes.properties) {
194
+ if (!ts.isJsxAttribute(attribute) || !['onClick', 'onChange'].includes(attribute.name.getText(file)) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer)) continue;
195
+ const handler = unbox(attribute.initializer.expression);
196
+ if (!handler || !(ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) || handler.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || handler.parameters.length > 1 || handler.parameters.some(parameter => !ts.isIdentifier(parameter.name))) continue;
197
+ const body = ts.isBlock(handler.body) && handler.body.statements.length === 1 && ts.isExpressionStatement(handler.body.statements[0]) ? handler.body.statements[0].expression : handler.body;
198
+ const call = unbox(body);
199
+ if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || !setters.has(call.expression.text) || call.arguments.length !== 1) continue;
200
+ const state = setters.get(call.expression.text), initial = evaluate(hooks.get(state), moduleScope, false, depth + 1);
201
+ // Boolean state frequently represents access or verification. It is
202
+ // never evidence for expanding a conditional public-content branch.
203
+ if (!(initial === null || typeof initial === 'string' || typeof initial === 'number' && Number.isFinite(initial))) continue;
204
+ const argument = unbox(call.arguments[0]), parameter = handler.parameters[0]?.name;
205
+ const inputValue = attribute.name.getText(file) === 'onChange' && ['input', 'select'].includes(tag) && parameter && ts.isIdentifier(parameter) && ts.isPropertyAccessExpression(argument) && argument.name.text === 'value' && ts.isPropertyAccessExpression(argument.expression) && ['target', 'currentTarget'].includes(argument.expression.name.text) && ts.isIdentifier(argument.expression.expression) && argument.expression.expression.text === parameter.text;
206
+ const already = selectors.has(state);
207
+ selectors.add(state);
208
+ const value = inputValue ? '' : evaluate(argument, scope, true, depth + 1);
209
+ if (!(value === null || typeof value === 'string' || typeof value === 'number' && Number.isFinite(value)) && !already) selectors.delete(state);
210
+ }
211
+ }
212
+ ts.forEachChild(node, child => controlScan(child, scope, excluded, depth + 1));
213
+ };
214
+ let returned;
215
+ if (ts.isBlock(component.body)) {
216
+ const returns = component.body.statements.filter(statement => ts.isReturnStatement(statement));
217
+ if (returns.length !== 1 || component.body.statements.some(statement => !ts.isVariableStatement(statement) && !ts.isReturnStatement(statement) && !ts.isFunctionDeclaration(statement) && !ts.isEmptyStatement(statement))) return failure('component-flow-unresolved');
218
+ returned = returns[0].expression;
219
+ } else returned = component.body;
220
+ const resolving = new Set();
221
+ const readBinding = (name, scope, mode, depth) => {
222
+ if (mutated.has(name)) return UNKNOWN;
223
+ if (hooks.has(name)) {
224
+ if (!mode || !selectors.has(name)) return UNKNOWN;
225
+ const initial = evaluate(hooks.get(name), moduleScope, false, depth + 1);
226
+ warnings.add('presentation-filter-initial-state');
227
+ return initial === null || typeof initial === 'string' || typeof initial === 'number' && Number.isFinite(initial) ? initial : UNKNOWN;
228
+ }
229
+ const binding = scope.get(name);
230
+ if (!binding) return name === 'undefined' ? undefined : UNKNOWN;
231
+ if (Object.hasOwn(binding, 'value')) return binding.value;
232
+ if (resolving.has(binding)) return UNKNOWN;
233
+ resolving.add(binding);
234
+ try { return evaluate(binding.expression, binding.scope, mode, depth + 1); }
235
+ finally { resolving.delete(binding); }
236
+ };
237
+ const callback = (node, parentScope, args, mode, depth) => {
238
+ step(depth); node = unbox(node);
239
+ if (!node || !(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) || node.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer)) return UNKNOWN;
240
+ const scope = new Map(parentScope);
241
+ node.parameters.forEach((parameter, index) => scope.set(parameter.name.text, { value: args[index] }));
242
+ if (!ts.isBlock(node.body)) return evaluate(node.body, scope, mode, depth + 1);
243
+ addDeclarations(node.body.statements, scope);
244
+ for (const statement of node.body.statements) {
245
+ if (ts.isReturnStatement(statement)) return statement.expression ? evaluate(statement.expression, scope, mode, depth + 1) : undefined;
246
+ if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) return UNKNOWN;
247
+ }
248
+ return UNKNOWN;
249
+ };
250
+ function evaluate(node, scope, mode = false, depth = 0) {
251
+ const value = evaluateValue(node, scope, mode, depth);
252
+ if (typeof value === 'string' && value.length > SOURCE_EXTRACT_LIMITS.text) throw Error('budget');
253
+ return typeof value === 'number' && !Number.isFinite(value) ? UNKNOWN : value;
254
+ }
255
+ function evaluateValue(node, scope, mode = false, depth = 0) {
256
+ step(depth); node = unbox(node); if (!node) return UNKNOWN;
257
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
258
+ if (ts.isNumericLiteral(node)) return Number(node.text);
259
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
260
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
261
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
262
+ if (ts.isIdentifier(node)) return readBinding(node.text, scope, mode, depth);
263
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return { [FN]: node, scope };
264
+ if (ts.isArrayLiteralExpression(node)) {
265
+ if (node.elements.length > SOURCE_EXTRACT_LIMITS.array) throw Error('budget');
266
+ const values = node.elements.map(element => evaluate(element, scope, mode, depth + 1));
267
+ return values.includes(UNKNOWN) ? UNKNOWN : values;
268
+ }
269
+ if (ts.isObjectLiteralExpression(node)) {
270
+ const value = Object.create(null);
271
+ for (const property of node.properties) {
272
+ if (!(ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) || !property.name || ts.isComputedPropertyName(property.name)) return UNKNOWN;
273
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) || ts.isNumericLiteral(property.name) ? property.name.text : null;
274
+ if (key === null || privateKeys.has(key)) return UNKNOWN;
275
+ // Unknown presentation properties (e.g. icon components) do not make
276
+ // independent literal text properties unavailable.
277
+ value[key] = evaluate(ts.isPropertyAssignment(property) ? property.initializer : property.name, scope, mode, depth + 1);
278
+ }
279
+ return value;
280
+ }
281
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
282
+ const value = evaluate(node.expression, scope, mode, depth + 1);
283
+ const key = ts.isPropertyAccessExpression(node) ? node.name.text : evaluate(node.argumentExpression, scope, mode, depth + 1);
284
+ if (value === UNKNOWN || key === UNKNOWN || value == null || privateKeys.has(String(key))) return UNKNOWN;
285
+ return (typeof value === 'object' || typeof value === 'string') && own(value, key) ? value[key] : undefined;
286
+ }
287
+ if (ts.isConditionalExpression(node)) {
288
+ const condition = evaluate(node.condition, scope, mode, depth + 1);
289
+ return condition === UNKNOWN ? UNKNOWN : evaluate(condition ? node.whenTrue : node.whenFalse, scope, mode, depth + 1);
290
+ }
291
+ if (ts.isPrefixUnaryExpression(node)) {
292
+ const value = evaluate(node.operand, scope, mode, depth + 1); if (value === UNKNOWN || !primitive(value)) return UNKNOWN;
293
+ if (node.operator === ts.SyntaxKind.ExclamationToken) return !value;
294
+ if (node.operator === ts.SyntaxKind.MinusToken && typeof value === 'number') return -value;
295
+ if (node.operator === ts.SyntaxKind.PlusToken && typeof value === 'number') return value;
296
+ return UNKNOWN;
297
+ }
298
+ if (ts.isBinaryExpression(node)) {
299
+ const left = evaluate(node.left, scope, mode, depth + 1), operator = node.operatorToken.kind;
300
+ if (left === UNKNOWN) return UNKNOWN;
301
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken) return left ? evaluate(node.right, scope, mode, depth + 1) : left;
302
+ if (operator === ts.SyntaxKind.BarBarToken) return left ? left : evaluate(node.right, scope, mode, depth + 1);
303
+ if (operator === ts.SyntaxKind.QuestionQuestionToken) return left == null ? evaluate(node.right, scope, mode, depth + 1) : left;
304
+ const right = evaluate(node.right, scope, mode, depth + 1);
305
+ if (right === UNKNOWN || !primitive(left) || !primitive(right)) return UNKNOWN;
306
+ if (operator === ts.SyntaxKind.EqualsEqualsEqualsToken) return left === right;
307
+ if (operator === ts.SyntaxKind.ExclamationEqualsEqualsToken) return left !== right;
308
+ if (operator === ts.SyntaxKind.PlusToken && ['string', 'number'].includes(typeof left) && ['string', 'number'].includes(typeof right)) return left + right;
309
+ if (typeof left === 'number' && typeof right === 'number') {
310
+ if (operator === ts.SyntaxKind.MinusToken) return left - right;
311
+ if (operator === ts.SyntaxKind.AsteriskToken) return left * right;
312
+ if (operator === ts.SyntaxKind.SlashToken && right !== 0) return left / right;
313
+ if (operator === ts.SyntaxKind.LessThanToken) return left < right;
314
+ if (operator === ts.SyntaxKind.GreaterThanToken) return left > right;
315
+ if (operator === ts.SyntaxKind.LessThanEqualsToken) return left <= right;
316
+ if (operator === ts.SyntaxKind.GreaterThanEqualsToken) return left >= right;
317
+ }
318
+ return UNKNOWN;
319
+ }
320
+ if (ts.isTemplateExpression(node)) {
321
+ let value = node.head.text;
322
+ for (const span of node.templateSpans) { const item = evaluate(span.expression, scope, mode, depth + 1); if (item === UNKNOWN || !primitive(item)) return UNKNOWN; value += String(item) + span.literal.text; if (value.length > SOURCE_EXTRACT_LIMITS.text) throw Error('budget'); }
323
+ return value;
324
+ }
325
+ if (ts.isCallExpression(node)) {
326
+ if (ts.isPropertyAccessExpression(node.expression)) {
327
+ const method = node.expression.name.text, value = evaluate(node.expression.expression, scope, mode, depth + 1);
328
+ if (value === UNKNOWN) return UNKNOWN;
329
+ if (method === 'filter' && Array.isArray(value) && node.arguments.length === 1) {
330
+ const filtered = [];
331
+ for (let index = 0; index < value.length; index++) { const keep = callback(node.arguments[0], scope, [value[index], index], mode, depth + 1); if (keep === UNKNOWN || typeof keep !== 'boolean') return UNKNOWN; if (keep) filtered.push(value[index]); }
332
+ if (mode && filtered.length !== value.length) filteredRowsOmitted = true;
333
+ return filtered;
334
+ }
335
+ const args = node.arguments.map(argument => evaluate(argument, scope, mode, depth + 1));
336
+ if (args.includes(UNKNOWN)) return UNKNOWN;
337
+ if (typeof value === 'string') {
338
+ if (!args.length && method === 'toLowerCase') return value.toLowerCase();
339
+ if (!args.length && method === 'toUpperCase') return value.toUpperCase();
340
+ if (!args.length && method === 'trim') return value.trim();
341
+ if (args.length === 1 && typeof args[0] === 'string' && method === 'includes') return value.includes(args[0]);
342
+ }
343
+ if (typeof value === 'number' && Number.isFinite(value) && method === 'toFixed' && args.length === 1 && Number.isInteger(args[0]) && args[0] >= 0 && args[0] <= 6) return value.toFixed(args[0]);
344
+ if (Array.isArray(value) && method === 'join' && value.every(primitive) && args.length <= 1 && (args[0] === undefined || typeof args[0] === 'string')) {
345
+ if (value.reduce((total, item) => total + (item == null ? 0 : String(item).length), 0) + Math.max(0, value.length - 1) * (args[0]?.length ?? 1) > SOURCE_EXTRACT_LIMITS.text) throw Error('budget');
346
+ return value.join(args[0]);
347
+ }
348
+ return UNKNOWN;
349
+ }
350
+ const fn = evaluate(node.expression, scope, mode, depth + 1);
351
+ if (!fn || typeof fn !== 'object' || !fn[FN]) return UNKNOWN;
352
+ const args = node.arguments.map(argument => evaluate(argument, scope, mode, depth + 1));
353
+ return args.includes(UNKNOWN) ? UNKNOWN : callback(fn[FN], fn.scope, args, mode, depth + 1);
354
+ }
355
+ return UNKNOWN;
356
+ }
357
+ const controlled = (node, scope, seen = new Set(), depth = 0) => {
358
+ step(depth); node = unbox(node); if (!node) return false;
359
+ if (ts.isIdentifier(node)) {
360
+ if (hooks.has(node.text)) return selectors.has(node.text) && evaluate(node, scope, true) !== UNKNOWN;
361
+ if (seen.has(node.text)) return false;
362
+ const binding = scope.get(node.text);
363
+ if (!binding) return false;
364
+ if (Object.hasOwn(binding, 'value')) return true;
365
+ return controlled(binding.expression, binding.scope, new Set([...seen, node.text]), depth + 1);
366
+ }
367
+ if (evaluate(node, scope, false, depth + 1) !== UNKNOWN) return true;
368
+ if (ts.isBinaryExpression(node)) return controlled(node.left, scope, seen, depth + 1) && controlled(node.right, scope, seen, depth + 1);
369
+ if (ts.isPrefixUnaryExpression(node)) return node.operator === ts.SyntaxKind.ExclamationToken && controlled(node.operand, scope, seen, depth + 1);
370
+ return false;
371
+ };
372
+ const pieces = [], bindings = [];
373
+ const describe = (node, scope, kind = 'element', end) => ({ start: node.getStart(file), end: end ?? node.end, kind, trace: scope.get(TRACE) ?? [] });
374
+ let firstHeading = '', firstH1 = '';
375
+ const emit = (type, raw, level, binding) => {
376
+ const text = normalize(raw); if (!text) return;
377
+ if (text.length > SOURCE_EXTRACT_LIMITS.text || pieces.length >= SOURCE_EXTRACT_LIMITS.blocks) throw Error('budget');
378
+ if (type === 'heading') { firstHeading ||= text; if (level === 1) firstH1 ||= text; }
379
+ bindings.push(binding);
380
+ pieces.push({ type, text, ...(type === 'heading' && level > 1 ? { data: { level: Math.min(level, 4) } } : {}) });
381
+ };
382
+ const opening = node => ts.isJsxElement(node) ? node.openingElement : ts.isJsxSelfClosingElement(node) ? node : null;
383
+ const omit = node => {
384
+ const tag = opening(node); if (!tag) return false;
385
+ const name = tag.tagName.getText(file);
386
+ if (excludedTags.has(name)) { if (['form', 'input', 'textarea', 'select'].includes(name)) warnings.add('interactive-content-omitted'); return true; }
387
+ if (tag.attributes.properties.some(attribute => ts.isJsxSpreadAttribute(attribute) || ts.isJsxAttribute(attribute) && ['data-cms-exclude', 'data-cms-private', 'hidden'].includes(attribute.name.getText(file)))) { warnings.add('excluded-content'); return true; }
388
+ if (/^[A-Z]/.test(name)) { if (!iconNames.has(name)) warnings.add('component-unresolved'); return true; }
389
+ return false;
390
+ };
391
+ const mapRows = (node, scope, depth) => {
392
+ node = unbox(node);
393
+ if (!node || !ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== 'map' || node.arguments.length !== 1) return null;
394
+ const values = evaluate(node.expression.expression, scope, true, depth + 1), fn = unbox(node.arguments[0]);
395
+ if (!Array.isArray(values) || values.length > SOURCE_EXTRACT_LIMITS.array || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer)) { warnings.add('collection-unresolved'); return []; }
396
+ return values.map((value, index) => {
397
+ const childScope = new Map(scope); fn.parameters.forEach((parameter, number) => childScope.set(parameter.name.text, { value: [value, index][number] }));
398
+ const name = fn.parameters[0]?.name.text;
399
+ const property = value && typeof value === 'object' ? ['id', 'slug', 'title', 'name', 'question', 'q', 'label'].find(key => typeof value[key] === 'string' || typeof value[key] === 'number') : undefined;
400
+ const selector = name && (property ? `${name}.${property}` : primitive(value) ? name : undefined);
401
+ const selected = property ? value[property] : primitive(value) ? value : undefined;
402
+ childScope.set(TRACE, [...(scope.get(TRACE) ?? []), { selector, value: selected }]);
403
+ if (!ts.isBlock(fn.body)) return { node: fn.body, scope: childScope };
404
+ addDeclarations(fn.body.statements, childScope);
405
+ const result = fn.body.statements.find(statement => ts.isReturnStatement(statement));
406
+ if (!result || fn.body.statements.some(statement => !ts.isVariableStatement(statement) && !ts.isReturnStatement(statement))) { warnings.add('collection-unresolved'); return null; }
407
+ return { node: result.expression, scope: childScope };
408
+ }).filter(Boolean);
409
+ };
410
+ const branches = (node, scope, depth) => {
411
+ if (ts.isConditionalExpression(node)) {
412
+ const condition = evaluate(node.condition, scope, false, depth + 1);
413
+ if (condition !== UNKNOWN) return [condition ? node.whenTrue : node.whenFalse];
414
+ if (controlled(node.condition, scope)) { warnings.add('interactive-content-omitted'); return [node.whenTrue, node.whenFalse]; }
415
+ warnings.add('conditional-unresolved'); return [];
416
+ }
417
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
418
+ const condition = evaluate(node.left, scope, false, depth + 1);
419
+ if (condition !== UNKNOWN) return condition ? [node.right] : [];
420
+ if (controlled(node.left, scope)) { warnings.add('interactive-content-omitted'); return [node.right]; }
421
+ warnings.add('conditional-unresolved'); return [];
422
+ }
423
+ return null;
424
+ };
425
+ function inline(node, scope, depth = 0) {
426
+ step(depth); node = unbox(node); if (!node) return '';
427
+ if (ts.isJsxText(node)) return jsxText(node.text);
428
+ if (ts.isJsxExpression(node)) return node.expression ? inline(node.expression, scope, depth + 1) : '';
429
+ if (ts.isJsxFragment(node)) return node.children.map(child => inline(child, scope, depth + 1)).join('');
430
+ if (opening(node)) {
431
+ if (omit(node)) return '';
432
+ if (opening(node).tagName.getText(file) === 'br') return ' ';
433
+ return ts.isJsxElement(node) ? node.children.map(child => inline(child, scope, depth + 1)).join('') : '';
434
+ }
435
+ const rows = mapRows(node, scope, depth); if (rows) return rows.map(row => inline(row.node, row.scope, depth + 1)).join(' ');
436
+ const selected = branches(node, scope, depth); if (selected) return selected.map(child => inline(child, scope, depth + 1)).join(' ');
437
+ const value = evaluate(node, scope, false, depth + 1);
438
+ if (value === UNKNOWN) { warnings.add('expression-unresolved'); return ''; }
439
+ return literalText(value);
440
+ }
441
+ function walk(node, scope, depth = 0) {
442
+ step(depth); node = unbox(node); if (!node) return;
443
+ if (ts.isJsxExpression(node)) { if (node.expression) walk(node.expression, scope, depth + 1); return; }
444
+ if (ts.isJsxFragment(node)) { flow(node.children, scope, depth + 1); return; }
445
+ if (opening(node)) {
446
+ if (omit(node)) return;
447
+ const tag = opening(node).tagName.getText(file);
448
+ if (tag === 'table') {
449
+ const rows = [];
450
+ let invalid = false, header = false;
451
+ const tableWalk = (item, itemScope, at) => {
452
+ step(at); item = unbox(item); if (!item) return;
453
+ if (ts.isJsxText(item)) return;
454
+ if (ts.isJsxExpression(item)) { if (item.expression) tableWalk(item.expression, itemScope, at + 1); return; }
455
+ if (ts.isJsxFragment(item)) { for (const child of item.children) tableWalk(child, itemScope, at + 1); return; }
456
+ const tagNode = opening(item);
457
+ if (tagNode) {
458
+ if (omit(item)) return;
459
+ const name = tagNode.tagName.getText(file);
460
+ if (name === 'tr' && ts.isJsxElement(item)) {
461
+ const cells = item.children.filter(child => opening(child) && ['td', 'th'].includes(opening(child).tagName.getText(file)));
462
+ if (item.children.some(child => !ts.isJsxText(child) && !cells.includes(child))) { invalid = true; return; }
463
+ const values = cells.map(child => normalize(inline(child, itemScope, at + 1)));
464
+ if (!values.length || values.length > 12 || values.some(value => value.length > 2000) || rows.length >= 100) { invalid = true; return; }
465
+ if (!rows.length) header = cells.every(child => opening(child).tagName.getText(file) === 'th');
466
+ rows.push(values); return;
467
+ }
468
+ if (!['table', 'thead', 'tbody', 'tfoot'].includes(name)) { invalid = true; return; }
469
+ if (ts.isJsxElement(item)) for (const child of item.children) tableWalk(child, itemScope, at + 1);
470
+ return;
471
+ }
472
+ const mapped = mapRows(item, itemScope, at); if (mapped) { for (const row of mapped) tableWalk(row.node, row.scope, at + 1); return; }
473
+ const selected = branches(item, itemScope, at); if (selected) { for (const child of selected) tableWalk(child, itemScope, at + 1); return; }
474
+ invalid = true;
475
+ };
476
+ tableWalk(node, scope, depth + 1);
477
+ if (invalid || !rows.length || rows.some(row => row.length !== rows[0].length)) warnings.add('table-unresolved');
478
+ else { if (pieces.length >= SOURCE_EXTRACT_LIMITS.blocks) throw Error('budget'); bindings.push(describe(node, scope)); pieces.push({ type: 'table', text: '', data: { header, rows } }); }
479
+ return;
480
+ }
481
+ if (/^h[1-6]$/.test(tag)) { emit('heading', inline(node, scope, depth + 1), Number(tag[1]), describe(node, scope)); return; }
482
+ if (['p', 'blockquote', 'li', 'pre'].includes(tag)) { emit(tag === 'li' ? 'list' : tag === 'blockquote' ? 'quote' : tag === 'pre' ? 'code' : 'paragraph', inline(node, scope, depth + 1), undefined, describe(node, scope)); return; }
483
+ if (ts.isJsxElement(node)) flow(node.children, scope, depth + 1);
484
+ return;
485
+ }
486
+ const rows = mapRows(node, scope, depth); if (rows) { for (const row of rows) walk(row.node, row.scope, depth + 1); return; }
487
+ const selected = branches(node, scope, depth); if (selected) { for (const child of selected) walk(child, scope, depth + 1); return; }
488
+ emit('paragraph', inline(node, scope, depth + 1), undefined, describe(node, scope, 'expression'));
489
+ }
490
+ function flow(children, scope, depth) {
491
+ let buffer = '', buffered = [];
492
+ const flush = () => { emit('paragraph', buffer, undefined, buffered.length ? describe(buffered[0], scope, 'inline', buffered.at(-1).end) : undefined); buffer = ''; buffered = []; };
493
+ for (const child of children) {
494
+ const tag = opening(child)?.tagName.getText(file);
495
+ if (ts.isJsxText(child) || tag && inlineTags.has(tag)) { buffer += inline(child, scope, depth + 1); buffered.push(child); continue; }
496
+ if (ts.isJsxExpression(child) && (!child.expression || !opening(unbox(child.expression)) && !ts.isJsxFragment(unbox(child.expression)))) {
497
+ const expression = unbox(child.expression);
498
+ if (!expression) continue;
499
+ const simple = evaluate(expression, scope, false, depth + 1);
500
+ if (simple !== UNKNOWN && primitive(simple)) { buffer += literalText(simple); buffered.push(child); continue; }
501
+ }
502
+ flush(); walk(child, scope, depth + 1);
503
+ }
504
+ flush();
505
+ }
506
+ if (returned) controlScan(returned, componentScope);
507
+ walk(returned, componentScope);
508
+ const title = firstH1 || firstHeading;
509
+ if (!title || title.length > 200) return failure('missing-title');
510
+ if (!pieces.length) return failure('empty-content');
511
+ const blocks = pieces.map((piece, index) => ({ id: uuid(`universalcms-static-v1:${locale}:${slug}:${index}:${piece.type}:${piece.text}`), version: 1, ...piece }));
512
+ return { ...(includeBindings ? { bindings, hasUnboundPresentationRows: filteredRowsOmitted && warnings.has('presentation-filter-initial-state'), returnRange: { start: unbox(returned).getStart(file), end: unbox(returned).end }, parameterCount: component.parameters.length } : {}), entry: { kind: 'page', slug, locale, document: { schemaVersion: 1, title, excerpt: (pieces.find(piece => piece.type === 'paragraph')?.text ?? '').slice(0, 1000), fields: {}, taxonomy: { categories: [], tags: [] }, blocks } }, warnings: [...warnings].sort(), complete: ![...warnings].some(code => partialCodes.has(code)) };
513
+ } catch { return failure('extraction-budget-or-unsupported'); }
514
+ }