@olonjs/cli 3.0.165 → 3.0.166
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/assets/src_tenant_alpha.sh +62 -13
- package/assets/templates/alpha/src_tenant.sh +62 -13
- package/assets/templates/next/src_tenant.sh +1087 -473
- package/package.json +1 -1
|
@@ -19,11 +19,10 @@ import {
|
|
|
19
19
|
import { loadVisitorPage } from '@olonjs/next/server';
|
|
20
20
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
21
21
|
import { WebMcpVisitorRuntime } from '@/components/webmcp/WebMcpVisitorRuntime';
|
|
22
|
-
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
23
22
|
import { buildVisitorWebPageJsonLd } from '@/lib/buildVisitorWebPageJsonLd';
|
|
24
|
-
import { getFileCollections } from '@/lib/loaders/getFileCollections';
|
|
25
23
|
import { getFilePages } from '@/lib/loaders/getFilePages';
|
|
26
24
|
import { getFileSiteBundle } from '@/lib/loaders/getFileSiteConfig';
|
|
25
|
+
import { loadVisitorPageBundle } from '@/lib/loaders/loadVisitorPageBundle';
|
|
27
26
|
import { resolveVisitorShell } from '@/lib/resolveVisitorShell';
|
|
28
27
|
import { VisitorSection } from '@/lib/VisitorSection';
|
|
29
28
|
|
|
@@ -89,7 +88,10 @@ export default async function VisitorCatchAllPage({ params, searchParams }: Page
|
|
|
89
88
|
notFound();
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
const collections =
|
|
91
|
+
const { collections, collectionSchemas } = await loadVisitorPageBundle({
|
|
92
|
+
page: result.page,
|
|
93
|
+
params: result.params,
|
|
94
|
+
});
|
|
93
95
|
const { siteConfig, menuConfig, themeConfig } = getFileSiteBundle();
|
|
94
96
|
const collectionContext = resolveCollectionContext(result.page, result.params, collections);
|
|
95
97
|
|
|
@@ -99,7 +101,7 @@ export default async function VisitorCatchAllPage({ params, searchParams }: Page
|
|
|
99
101
|
themeConfig,
|
|
100
102
|
menuConfig,
|
|
101
103
|
collections,
|
|
102
|
-
collectionSchemas:
|
|
104
|
+
collectionSchemas: collectionSchemas as never,
|
|
103
105
|
collectionContext,
|
|
104
106
|
refDocuments: {
|
|
105
107
|
'menu.json': menuConfig,
|
|
@@ -213,27 +215,50 @@ mkdir -p "app/api/public-collection/[source]"
|
|
|
213
215
|
mkdir -p "app/api/public-collection/[source]/[file]"
|
|
214
216
|
echo "Creating app/api/public-collection/[source]/[file]/route.ts..."
|
|
215
217
|
cat << 'END_OF_FILE_CONTENT' > "app/api/public-collection/[source]/[file]/route.ts"
|
|
216
|
-
import { NextResponse } from 'next/server';
|
|
217
|
-
import {
|
|
218
|
-
import {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
218
|
+
import { NextResponse } from 'next/server';
|
|
219
|
+
import { resolveCollectionSources } from '@olonjs/sources';
|
|
220
|
+
import { resolvePublicCollectionDocument } from '@/lib/loaders/publishedContent';
|
|
221
|
+
import { collectionSources } from '@/sources';
|
|
222
|
+
import { loadRuntimeSurfaceForSite } from '@/lib/webmcp/runtime/loadRuntimeSurface';
|
|
223
|
+
|
|
224
|
+
export const dynamic = 'force-dynamic';
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* GET /collections/{source}/{source}.json (rewrite) — collection document, runtime.
|
|
228
|
+
*
|
|
229
|
+
* File-backed collections are read from the content bundle; EXTERNAL sources
|
|
230
|
+
* (ADR-0018) are projected on demand so the contract's `dataHref` resolves.
|
|
231
|
+
* Note: resolution here has no route params — parameterized sources should be
|
|
232
|
+
* resolved at the page level (see loadPublicPageBundleForRequest).
|
|
233
|
+
*/
|
|
234
|
+
export async function GET(_request: Request, context: { params: Promise<{ source: string; file: string }> }) {
|
|
235
|
+
try {
|
|
236
|
+
const { source, file } = await context.params;
|
|
237
|
+
const sourceName = decodeURIComponent(source);
|
|
238
|
+
|
|
239
|
+
if (sourceName in collectionSources) {
|
|
240
|
+
const resolved = await resolveCollectionSources({
|
|
241
|
+
sources: collectionSources as never,
|
|
242
|
+
needed: [sourceName],
|
|
243
|
+
});
|
|
244
|
+
const projected = resolved.collections[sourceName];
|
|
245
|
+
if (!projected) {
|
|
246
|
+
return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
|
|
247
|
+
}
|
|
248
|
+
return NextResponse.json(projected);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const { bundle } = loadRuntimeSurfaceForSite();
|
|
252
|
+
const doc = resolvePublicCollectionDocument(bundle, sourceName, decodeURIComponent(file));
|
|
253
|
+
if (!doc) return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
|
|
254
|
+
return NextResponse.json(doc);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return NextResponse.json(
|
|
257
|
+
{ error: error instanceof Error ? error.message : 'Collection JSON failed' },
|
|
258
|
+
{ status: 500 },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
237
262
|
|
|
238
263
|
END_OF_FILE_CONTENT
|
|
239
264
|
mkdir -p "app/api/public-config"
|
|
@@ -761,10 +786,10 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
761
786
|
"dist:dna": "npm run dist"
|
|
762
787
|
},
|
|
763
788
|
"dependencies": {
|
|
764
|
-
"@olonjs/core": "^2.0.
|
|
765
|
-
"@olonjs/next": "^0.0.
|
|
766
|
-
"@olonjs/react": "^0.2.
|
|
767
|
-
"@olonjs/studio": "^0.2.
|
|
789
|
+
"@olonjs/core": "^2.0.2",
|
|
790
|
+
"@olonjs/next": "^0.0.14",
|
|
791
|
+
"@olonjs/react": "^0.2.2",
|
|
792
|
+
"@olonjs/studio": "^0.2.2",
|
|
768
793
|
"clsx": "^2.1.1",
|
|
769
794
|
"lucide-react": "^0.474.0",
|
|
770
795
|
"next": "^15.5.0",
|
|
@@ -772,7 +797,8 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
772
797
|
"react-dom": "^19.0.0",
|
|
773
798
|
"react-router-dom": "^6.29.0",
|
|
774
799
|
"tailwind-merge": "^3.0.1",
|
|
775
|
-
"zod": "^4.6.0"
|
|
800
|
+
"zod": "^4.6.0",
|
|
801
|
+
"@olonjs/sources": "^0.1.0"
|
|
776
802
|
},
|
|
777
803
|
"devDependencies": {
|
|
778
804
|
"@tailwindcss/postcss": "^4.0.0",
|
|
@@ -1011,10 +1037,16 @@ const SCRIPT = path.join(__dirname, 'webmcp-feature-check.mjs');
|
|
|
1011
1037
|
const PKG = path.join(ROOT, 'package.json');
|
|
1012
1038
|
|
|
1013
1039
|
describe('next verify:webmcp script', () => {
|
|
1014
|
-
it('probes document.
|
|
1040
|
+
it('probes the native document.modelContext surface with polyfill fallback', () => {
|
|
1015
1041
|
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1016
1042
|
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1043
|
+
// Native Chrome surface: document.modelContext.getTools() / executeTool(toolObject, argsObject).
|
|
1044
|
+
assert.match(src, /document\.modelContext/);
|
|
1045
|
+
assert.match(src, /await native\.getTools\(\)/);
|
|
1046
|
+
assert.match(src, /native\.executeTool\(tool,/);
|
|
1047
|
+
// Polyfill fallback (browsers without native WebMCP): listTools / executeTool(name, json).
|
|
1017
1048
|
assert.match(src, /document\.modelContextProtocol/);
|
|
1049
|
+
assert.match(src, /protocol\.executeTool\(/);
|
|
1018
1050
|
// modelContextTesting is never assigned by the runtime — probing it is a false negative.
|
|
1019
1051
|
assert.doesNotMatch(src, /modelContextTesting/);
|
|
1020
1052
|
assert.doesNotMatch(src, /navigator\.modelContext/);
|
|
@@ -1031,302 +1063,351 @@ describe('next verify:webmcp script', () => {
|
|
|
1031
1063
|
END_OF_FILE_CONTENT
|
|
1032
1064
|
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
1033
1065
|
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
1034
|
-
import fs from 'fs/promises';
|
|
1035
|
-
import path from 'path';
|
|
1036
|
-
import { fileURLToPath } from 'url';
|
|
1037
|
-
import { createRequire } from 'module';
|
|
1038
|
-
|
|
1039
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1040
|
-
const rootDir = path.resolve(__dirname, '..');
|
|
1041
|
-
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:3000';
|
|
1042
|
-
const UPDATE_TOOL_NAME = 'update-section';
|
|
1043
|
-
const SAVE_TOOL_NAME = 'save';
|
|
1044
|
-
|
|
1045
|
-
function pageFilePathFromSlug(slug) {
|
|
1046
|
-
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
function adminUrlFromSlug(slug) {
|
|
1050
|
-
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
function isStringSchema(schema) {
|
|
1054
|
-
if (!schema || typeof schema !== 'object') return false;
|
|
1055
|
-
if (schema.type === 'string') return true;
|
|
1056
|
-
if (Array.isArray(schema.anyOf)) {
|
|
1057
|
-
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
1058
|
-
}
|
|
1059
|
-
return false;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function findTopLevelStringField(sectionSchema) {
|
|
1063
|
-
const properties = sectionSchema?.properties;
|
|
1064
|
-
if (!properties || typeof properties !== 'object') return null;
|
|
1065
|
-
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
1066
|
-
for (const key of preferred) {
|
|
1067
|
-
if (isStringSchema(properties[key])) return key;
|
|
1068
|
-
}
|
|
1069
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
1070
|
-
if (isStringSchema(value)) return key;
|
|
1071
|
-
}
|
|
1072
|
-
return null;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
async function loadPlaywright() {
|
|
1076
|
-
const require = createRequire(import.meta.url);
|
|
1077
|
-
try {
|
|
1078
|
-
return require('playwright');
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1081
|
-
throw new Error(
|
|
1082
|
-
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
async function readPageJson(slug) {
|
|
1088
|
-
const pageFilePath = pageFilePathFromSlug(slug);
|
|
1089
|
-
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
1090
|
-
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
async function waitFor(predicate, timeoutMs, label) {
|
|
1094
|
-
const startedAt = Date.now();
|
|
1095
|
-
for (;;) {
|
|
1096
|
-
const result = await predicate();
|
|
1097
|
-
if (result) return result;
|
|
1098
|
-
if (Date.now() - startedAt > timeoutMs) {
|
|
1099
|
-
throw new Error(`Timed out while waiting for ${label}.`);
|
|
1100
|
-
}
|
|
1101
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
1106
|
-
await waitFor(async () => {
|
|
1107
|
-
const { json } = await readPageJson(slug);
|
|
1108
|
-
const section = Array.isArray(json.sections)
|
|
1109
|
-
? json.sections.find((item) => item?.id === sectionId)
|
|
1110
|
-
: null;
|
|
1111
|
-
return section?.data?.[fieldKey] === expectedValue;
|
|
1112
|
-
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
async function ensureResponseOk(response, label) {
|
|
1116
|
-
if (!response.ok) {
|
|
1117
|
-
const text = await response.text();
|
|
1118
|
-
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
1119
|
-
}
|
|
1120
|
-
return response;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
async function fetchJson(relativePath, label) {
|
|
1124
|
-
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
1125
|
-
return response.json();
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
async function selectTarget() {
|
|
1129
|
-
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
1130
|
-
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1131
|
-
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1132
|
-
: null;
|
|
1133
|
-
|
|
1134
|
-
const candidatePages = requestedSlug
|
|
1135
|
-
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
1136
|
-
: (siteIndex.pages ?? []);
|
|
1137
|
-
|
|
1138
|
-
for (const pageEntry of candidatePages) {
|
|
1139
|
-
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
1140
|
-
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
1141
|
-
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
1142
|
-
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
1143
|
-
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
1144
|
-
: [];
|
|
1145
|
-
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
1146
|
-
// Manifest tools are page-level (`update-section`, `save`); targets come from the
|
|
1147
|
-
// contract's local section instances + their schemas.
|
|
1148
|
-
const updateTool = tools.find((tool) => tool?.name === UPDATE_TOOL_NAME);
|
|
1149
|
-
const saveTool = tools.find((tool) => tool?.name === SAVE_TOOL_NAME);
|
|
1150
|
-
if (!updateTool || !saveTool) continue;
|
|
1151
|
-
|
|
1152
|
-
for (const targetInstance of localInstances) {
|
|
1153
|
-
const sectionType = targetInstance?.type;
|
|
1154
|
-
if (!targetInstance?.id || typeof sectionType !== 'string') continue;
|
|
1155
|
-
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
1156
|
-
if (!targetFieldKey) continue;
|
|
1157
|
-
const pageState = await readPageJson(pageEntry.slug);
|
|
1158
|
-
const section = Array.isArray(pageState.json.sections)
|
|
1159
|
-
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
1160
|
-
: null;
|
|
1161
|
-
const originalValue = section?.data?.[targetFieldKey];
|
|
1162
|
-
if (typeof originalValue !== 'string') continue;
|
|
1163
|
-
|
|
1164
|
-
return {
|
|
1165
|
-
slug: pageEntry.slug,
|
|
1166
|
-
manifestHref: pageEntry.manifestHref,
|
|
1167
|
-
contractHref: pageEntry.contractHref,
|
|
1168
|
-
toolName: UPDATE_TOOL_NAME,
|
|
1169
|
-
saveToolName: SAVE_TOOL_NAME,
|
|
1170
|
-
sectionId: targetInstance.id,
|
|
1171
|
-
sectionType,
|
|
1172
|
-
fieldKey: targetFieldKey,
|
|
1173
|
-
originalValue,
|
|
1174
|
-
originalState: pageState,
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
throw new Error(
|
|
1180
|
-
requestedSlug
|
|
1181
|
-
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
1182
|
-
: 'No valid WebMCP verification target found in manifest index.'
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
async function main() {
|
|
1187
|
-
const { chromium } = await loadPlaywright();
|
|
1188
|
-
const target = await selectTarget();
|
|
1189
|
-
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
1190
|
-
const browser = await chromium.launch({ headless: true });
|
|
1191
|
-
const page = await browser.newPage();
|
|
1192
|
-
const consoleEvents = [];
|
|
1193
|
-
let mutationApplied = false;
|
|
1194
|
-
|
|
1195
|
-
page.on('console', (message) => {
|
|
1196
|
-
if (message.type() === 'error' || message.type() === 'warning') {
|
|
1197
|
-
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
1198
|
-
}
|
|
1199
|
-
});
|
|
1200
|
-
page.on('pageerror', (error) => {
|
|
1201
|
-
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
/** update-section mutates the Studio draft only; save persists. Returns raw update result. */
|
|
1205
|
-
const runUpdateThenSave = async (value) => {
|
|
1206
|
-
const rawUpdate = await page.evaluate(
|
|
1207
|
-
async ({ toolName, slug, sectionId, sectionType, fieldKey, value: nextValue }) => {
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
await page.
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1066
|
+
import fs from 'fs/promises';
|
|
1067
|
+
import path from 'path';
|
|
1068
|
+
import { fileURLToPath } from 'url';
|
|
1069
|
+
import { createRequire } from 'module';
|
|
1070
|
+
|
|
1071
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1072
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1073
|
+
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:3000';
|
|
1074
|
+
const UPDATE_TOOL_NAME = 'update-section';
|
|
1075
|
+
const SAVE_TOOL_NAME = 'save';
|
|
1076
|
+
|
|
1077
|
+
function pageFilePathFromSlug(slug) {
|
|
1078
|
+
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function adminUrlFromSlug(slug) {
|
|
1082
|
+
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function isStringSchema(schema) {
|
|
1086
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
1087
|
+
if (schema.type === 'string') return true;
|
|
1088
|
+
if (Array.isArray(schema.anyOf)) {
|
|
1089
|
+
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
1090
|
+
}
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function findTopLevelStringField(sectionSchema) {
|
|
1095
|
+
const properties = sectionSchema?.properties;
|
|
1096
|
+
if (!properties || typeof properties !== 'object') return null;
|
|
1097
|
+
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
1098
|
+
for (const key of preferred) {
|
|
1099
|
+
if (isStringSchema(properties[key])) return key;
|
|
1100
|
+
}
|
|
1101
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
1102
|
+
if (isStringSchema(value)) return key;
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
async function loadPlaywright() {
|
|
1108
|
+
const require = createRequire(import.meta.url);
|
|
1109
|
+
try {
|
|
1110
|
+
return require('playwright');
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1113
|
+
throw new Error(
|
|
1114
|
+
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async function readPageJson(slug) {
|
|
1120
|
+
const pageFilePath = pageFilePathFromSlug(slug);
|
|
1121
|
+
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
1122
|
+
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function waitFor(predicate, timeoutMs, label) {
|
|
1126
|
+
const startedAt = Date.now();
|
|
1127
|
+
for (;;) {
|
|
1128
|
+
const result = await predicate();
|
|
1129
|
+
if (result) return result;
|
|
1130
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
1131
|
+
throw new Error(`Timed out while waiting for ${label}.`);
|
|
1132
|
+
}
|
|
1133
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
1138
|
+
await waitFor(async () => {
|
|
1139
|
+
const { json } = await readPageJson(slug);
|
|
1140
|
+
const section = Array.isArray(json.sections)
|
|
1141
|
+
? json.sections.find((item) => item?.id === sectionId)
|
|
1142
|
+
: null;
|
|
1143
|
+
return section?.data?.[fieldKey] === expectedValue;
|
|
1144
|
+
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async function ensureResponseOk(response, label) {
|
|
1148
|
+
if (!response.ok) {
|
|
1149
|
+
const text = await response.text();
|
|
1150
|
+
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
1151
|
+
}
|
|
1152
|
+
return response;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function fetchJson(relativePath, label) {
|
|
1156
|
+
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
1157
|
+
return response.json();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function selectTarget() {
|
|
1161
|
+
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
1162
|
+
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1163
|
+
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1164
|
+
: null;
|
|
1165
|
+
|
|
1166
|
+
const candidatePages = requestedSlug
|
|
1167
|
+
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
1168
|
+
: (siteIndex.pages ?? []);
|
|
1169
|
+
|
|
1170
|
+
for (const pageEntry of candidatePages) {
|
|
1171
|
+
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
1172
|
+
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
1173
|
+
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
1174
|
+
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
1175
|
+
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
1176
|
+
: [];
|
|
1177
|
+
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
1178
|
+
// Manifest tools are page-level (`update-section`, `save`); targets come from the
|
|
1179
|
+
// contract's local section instances + their schemas.
|
|
1180
|
+
const updateTool = tools.find((tool) => tool?.name === UPDATE_TOOL_NAME);
|
|
1181
|
+
const saveTool = tools.find((tool) => tool?.name === SAVE_TOOL_NAME);
|
|
1182
|
+
if (!updateTool || !saveTool) continue;
|
|
1183
|
+
|
|
1184
|
+
for (const targetInstance of localInstances) {
|
|
1185
|
+
const sectionType = targetInstance?.type;
|
|
1186
|
+
if (!targetInstance?.id || typeof sectionType !== 'string') continue;
|
|
1187
|
+
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
1188
|
+
if (!targetFieldKey) continue;
|
|
1189
|
+
const pageState = await readPageJson(pageEntry.slug);
|
|
1190
|
+
const section = Array.isArray(pageState.json.sections)
|
|
1191
|
+
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
1192
|
+
: null;
|
|
1193
|
+
const originalValue = section?.data?.[targetFieldKey];
|
|
1194
|
+
if (typeof originalValue !== 'string') continue;
|
|
1195
|
+
|
|
1196
|
+
return {
|
|
1197
|
+
slug: pageEntry.slug,
|
|
1198
|
+
manifestHref: pageEntry.manifestHref,
|
|
1199
|
+
contractHref: pageEntry.contractHref,
|
|
1200
|
+
toolName: UPDATE_TOOL_NAME,
|
|
1201
|
+
saveToolName: SAVE_TOOL_NAME,
|
|
1202
|
+
sectionId: targetInstance.id,
|
|
1203
|
+
sectionType,
|
|
1204
|
+
fieldKey: targetFieldKey,
|
|
1205
|
+
originalValue,
|
|
1206
|
+
originalState: pageState,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
requestedSlug
|
|
1213
|
+
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
1214
|
+
: 'No valid WebMCP verification target found in manifest index.'
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function main() {
|
|
1219
|
+
const { chromium } = await loadPlaywright();
|
|
1220
|
+
const target = await selectTarget();
|
|
1221
|
+
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
1222
|
+
const browser = await chromium.launch({ headless: true });
|
|
1223
|
+
const page = await browser.newPage();
|
|
1224
|
+
const consoleEvents = [];
|
|
1225
|
+
let mutationApplied = false;
|
|
1226
|
+
|
|
1227
|
+
page.on('console', (message) => {
|
|
1228
|
+
if (message.type() === 'error' || message.type() === 'warning') {
|
|
1229
|
+
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
page.on('pageerror', (error) => {
|
|
1233
|
+
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
/** update-section mutates the Studio draft only; save persists. Returns raw update result. */
|
|
1237
|
+
const runUpdateThenSave = async (value) => {
|
|
1238
|
+
const rawUpdate = await page.evaluate(
|
|
1239
|
+
async ({ toolName, slug, sectionId, sectionType, fieldKey, value: nextValue }) => {
|
|
1240
|
+
const args = { slug, sectionId, sectionType, fieldKey, value: nextValue };
|
|
1241
|
+
const native = document.modelContext;
|
|
1242
|
+
const protocol = document.modelContextProtocol;
|
|
1243
|
+
// Native Chrome WebMCP: executeTool(toolObject, args) — the tool object
|
|
1244
|
+
// comes from getTools(). Args format: plain object since Chrome 155;
|
|
1245
|
+
// before that the native expects a JSON string (verified on Chrome 153).
|
|
1246
|
+
if (native && typeof native.executeTool === 'function') {
|
|
1247
|
+
const tools = await native.getTools();
|
|
1248
|
+
const tool = tools.find((entry) => entry.name === toolName);
|
|
1249
|
+
if (!tool) {
|
|
1250
|
+
throw new Error(`Tool "${toolName}" not found in native getTools().`);
|
|
1251
|
+
}
|
|
1252
|
+
const chromeMajor = Number((navigator.userAgent.match(/Chrome\/(\d+)/) ?? [])[1] ?? 0);
|
|
1253
|
+
const nativeArgs = chromeMajor >= 155 ? args : JSON.stringify(args);
|
|
1254
|
+
const result = await native.executeTool(tool, nativeArgs);
|
|
1255
|
+
if (result === null) {
|
|
1256
|
+
throw new Error(`Tool "${toolName}" returned null (navigation).`);
|
|
1257
|
+
}
|
|
1258
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
1259
|
+
}
|
|
1260
|
+
if (protocol && typeof protocol.executeTool === 'function') {
|
|
1261
|
+
return protocol.executeTool(toolName, JSON.stringify(args));
|
|
1262
|
+
}
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
'WebMCP runtime unavailable: no document.modelContext (native) and no document.modelContextProtocol (polyfill).',
|
|
1265
|
+
);
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
toolName: target.toolName,
|
|
1269
|
+
slug: target.slug,
|
|
1270
|
+
sectionId: target.sectionId,
|
|
1271
|
+
sectionType: target.sectionType,
|
|
1272
|
+
fieldKey: target.fieldKey,
|
|
1273
|
+
value,
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
const parsedUpdate = JSON.parse(rawUpdate);
|
|
1277
|
+
if (parsedUpdate?.isError) {
|
|
1278
|
+
throw new Error(`WebMCP ${target.toolName} returned an error: ${rawUpdate}`);
|
|
1279
|
+
}
|
|
1280
|
+
const rawSave = await page.evaluate(
|
|
1281
|
+
async ({ saveToolName }) => {
|
|
1282
|
+
const native = document.modelContext;
|
|
1283
|
+
const protocol = document.modelContextProtocol;
|
|
1284
|
+
if (native && typeof native.executeTool === 'function') {
|
|
1285
|
+
const tools = await native.getTools();
|
|
1286
|
+
const tool = tools.find((entry) => entry.name === saveToolName);
|
|
1287
|
+
if (!tool) {
|
|
1288
|
+
throw new Error(`Tool "${saveToolName}" not found in native getTools().`);
|
|
1289
|
+
}
|
|
1290
|
+
const chromeMajor = Number((navigator.userAgent.match(/Chrome\/(\d+)/) ?? [])[1] ?? 0);
|
|
1291
|
+
const nativeArgs = chromeMajor >= 155 ? {} : '{}';
|
|
1292
|
+
const result = await native.executeTool(tool, nativeArgs);
|
|
1293
|
+
if (result === null) {
|
|
1294
|
+
throw new Error(`Tool "${saveToolName}" returned null (navigation).`);
|
|
1295
|
+
}
|
|
1296
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
1297
|
+
}
|
|
1298
|
+
if (protocol && typeof protocol.executeTool === 'function') {
|
|
1299
|
+
return protocol.executeTool(saveToolName, '{}');
|
|
1300
|
+
}
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
'WebMCP runtime unavailable: no document.modelContext (native) and no document.modelContextProtocol (polyfill).',
|
|
1303
|
+
);
|
|
1304
|
+
},
|
|
1305
|
+
{ saveToolName: target.saveToolName }
|
|
1306
|
+
);
|
|
1307
|
+
const parsedSave = JSON.parse(rawSave);
|
|
1308
|
+
if (parsedSave?.isError) {
|
|
1309
|
+
throw new Error(`WebMCP ${target.saveToolName} returned an error: ${rawSave}`);
|
|
1310
|
+
}
|
|
1311
|
+
return rawUpdate;
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
const restoreOriginal = async () => {
|
|
1315
|
+
try {
|
|
1316
|
+
await runUpdateThenSave(target.originalValue);
|
|
1317
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
|
|
1318
|
+
} catch {
|
|
1319
|
+
await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
|
|
1325
|
+
if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
|
|
1326
|
+
throw new Error(`Manifest does not expose ${target.toolName}.`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const pageContract = await fetchJson(target.contractHref, `Page contract request for ${target.slug}`);
|
|
1330
|
+
if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
|
|
1331
|
+
throw new Error(`Page contract does not expose ${target.toolName}.`);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
await page.waitForFunction(
|
|
1338
|
+
({ manifestHref, contractHref }) => {
|
|
1339
|
+
const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
|
|
1340
|
+
const contractLink = document.head.querySelector('link[rel="olon-contract"]');
|
|
1341
|
+
return manifestLink?.getAttribute('href') === manifestHref
|
|
1342
|
+
&& contractLink?.getAttribute('href') === contractHref;
|
|
1343
|
+
},
|
|
1344
|
+
{ manifestHref: target.manifestHref, contractHref: target.contractHref },
|
|
1345
|
+
{ timeout: 10_000 }
|
|
1346
|
+
);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
const diagnostics = await page.evaluate(() => ({
|
|
1349
|
+
head: document.head.innerHTML,
|
|
1350
|
+
bodyText: document.body.innerText,
|
|
1351
|
+
}));
|
|
1352
|
+
throw new Error(
|
|
1353
|
+
[
|
|
1354
|
+
error instanceof Error ? error.message : String(error),
|
|
1355
|
+
`head=${diagnostics.head}`,
|
|
1356
|
+
`body=${diagnostics.bodyText}`,
|
|
1357
|
+
...consoleEvents,
|
|
1358
|
+
].join('\n')
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const toolNames = await page.evaluate(async () => {
|
|
1363
|
+
const native = document.modelContext;
|
|
1364
|
+
const protocol = document.modelContextProtocol;
|
|
1365
|
+
if (native && typeof native.getTools === 'function') {
|
|
1366
|
+
const tools = await native.getTools();
|
|
1367
|
+
return tools.map((tool) => tool.name);
|
|
1368
|
+
}
|
|
1369
|
+
if (protocol && typeof protocol.listTools === 'function') {
|
|
1370
|
+
return protocol.listTools().map((tool) => tool.name);
|
|
1371
|
+
}
|
|
1372
|
+
return [];
|
|
1373
|
+
});
|
|
1374
|
+
for (const required of [target.toolName, target.saveToolName]) {
|
|
1375
|
+
if (!toolNames.includes(required)) {
|
|
1376
|
+
throw new Error(`Runtime did not register ${required}. Found: ${toolNames.join(', ')}`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
mutationApplied = true;
|
|
1381
|
+
await runUpdateThenSave(nextValue);
|
|
1382
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
|
|
1383
|
+
await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
|
|
1384
|
+
|
|
1385
|
+
console.log(
|
|
1386
|
+
JSON.stringify({
|
|
1387
|
+
ok: true,
|
|
1388
|
+
slug: target.slug,
|
|
1389
|
+
manifestHref: target.manifestHref,
|
|
1390
|
+
contractHref: target.contractHref,
|
|
1391
|
+
toolName: target.toolName,
|
|
1392
|
+
saveToolName: target.saveToolName,
|
|
1393
|
+
sectionId: target.sectionId,
|
|
1394
|
+
sectionType: target.sectionType,
|
|
1395
|
+
fieldKey: target.fieldKey,
|
|
1396
|
+
toolNames,
|
|
1397
|
+
})
|
|
1398
|
+
);
|
|
1399
|
+
} finally {
|
|
1400
|
+
if (mutationApplied) {
|
|
1401
|
+
await restoreOriginal();
|
|
1402
|
+
}
|
|
1403
|
+
await browser.close();
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
main().catch((error) => {
|
|
1408
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1409
|
+
process.exit(1);
|
|
1410
|
+
});
|
|
1330
1411
|
|
|
1331
1412
|
END_OF_FILE_CONTENT
|
|
1332
1413
|
mkdir -p "src"
|
|
@@ -1396,6 +1477,43 @@ import { LibroSchema, LibriCollectionSchema } from './schema';
|
|
|
1396
1477
|
export type Libro = z.infer<typeof LibroSchema>;
|
|
1397
1478
|
export type LibriCollection = z.infer<typeof LibriCollectionSchema>;
|
|
1398
1479
|
|
|
1480
|
+
END_OF_FILE_CONTENT
|
|
1481
|
+
mkdir -p "src/collections/prodotti"
|
|
1482
|
+
echo "Creating src/collections/prodotti/index.ts..."
|
|
1483
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/prodotti/index.ts"
|
|
1484
|
+
export { ProdottoSchema, ProdottiCollectionSchema } from './schema';
|
|
1485
|
+
export type { Prodotto, ProdottiCollection } from './types';
|
|
1486
|
+
|
|
1487
|
+
END_OF_FILE_CONTENT
|
|
1488
|
+
echo "Creating src/collections/prodotti/schema.ts..."
|
|
1489
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/prodotti/schema.ts"
|
|
1490
|
+
import { z } from 'zod';
|
|
1491
|
+
import { BaseCollectionItem } from '@olonjs/core';
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Demo collection whose source of truth is EXTERNAL (see src/sources/catalogo.ts).
|
|
1495
|
+
* Same Zod item shape feeds the human list rendering (ui:* descriptors) and the
|
|
1496
|
+
* agent contracts (JSON Schema emitted from this schema).
|
|
1497
|
+
*/
|
|
1498
|
+
export const ProdottoSchema = BaseCollectionItem.extend({
|
|
1499
|
+
title: z.string().describe('ui:text'),
|
|
1500
|
+
price: z.number().describe('ui:number'),
|
|
1501
|
+
currency: z.string().default('EUR').describe('ui:text'),
|
|
1502
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
1503
|
+
image: z.string().optional().describe('ui:text'),
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
export const ProdottiCollectionSchema = z.record(z.string(), ProdottoSchema);
|
|
1507
|
+
|
|
1508
|
+
END_OF_FILE_CONTENT
|
|
1509
|
+
echo "Creating src/collections/prodotti/types.ts..."
|
|
1510
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/prodotti/types.ts"
|
|
1511
|
+
import { z } from 'zod';
|
|
1512
|
+
import { ProdottoSchema, ProdottiCollectionSchema } from './schema';
|
|
1513
|
+
|
|
1514
|
+
export type Prodotto = z.infer<typeof ProdottoSchema>;
|
|
1515
|
+
export type ProdottiCollection = z.infer<typeof ProdottiCollectionSchema>;
|
|
1516
|
+
|
|
1399
1517
|
END_OF_FILE_CONTENT
|
|
1400
1518
|
mkdir -p "src/components"
|
|
1401
1519
|
mkdir -p "src/components/admin"
|
|
@@ -2661,6 +2779,183 @@ import { HeaderSchema, HeaderSettingsSchema } from './schema';
|
|
|
2661
2779
|
export type HeaderData = z.infer<typeof HeaderSchema>;
|
|
2662
2780
|
export type HeaderSettings = z.infer<typeof HeaderSettingsSchema>;
|
|
2663
2781
|
|
|
2782
|
+
END_OF_FILE_CONTENT
|
|
2783
|
+
mkdir -p "src/components/prodotti-list"
|
|
2784
|
+
echo "Creating src/components/prodotti-list/View.tsx..."
|
|
2785
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotti-list/View.tsx"
|
|
2786
|
+
import type { Prodotto } from '@/collections/prodotti';
|
|
2787
|
+
import type { ProdottiListData, ProdottiListSettings } from './types';
|
|
2788
|
+
|
|
2789
|
+
type ProdottiListViewProps = {
|
|
2790
|
+
data: ProdottiListData;
|
|
2791
|
+
settings?: ProdottiListSettings;
|
|
2792
|
+
};
|
|
2793
|
+
|
|
2794
|
+
function formatPrice(prodotto: Prodotto): string {
|
|
2795
|
+
const amount = (prodotto.price / 100).toFixed(2);
|
|
2796
|
+
return `${amount} ${prodotto.currency ?? 'EUR'}`;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function toProdotti(items: ProdottiListData['items']): Prodotto[] {
|
|
2800
|
+
return Object.values(items ?? {}).sort((a, b) => a.title.localeCompare(b.title));
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
/**
|
|
2804
|
+
* RSC-safe catalog list — data comes from a PROJECTED external source
|
|
2805
|
+
* (src/sources/catalogo.ts). The View itself never fetches (CIP).
|
|
2806
|
+
*/
|
|
2807
|
+
export function ProdottiListView({ data }: ProdottiListViewProps) {
|
|
2808
|
+
const prodotti = toProdotti(data.items);
|
|
2809
|
+
|
|
2810
|
+
return (
|
|
2811
|
+
<section className="bg-background px-6 py-16 text-foreground">
|
|
2812
|
+
<div className="mx-auto w-full max-w-5xl">
|
|
2813
|
+
{data.eyebrow ? (
|
|
2814
|
+
<p className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
2815
|
+
{data.eyebrow}
|
|
2816
|
+
</p>
|
|
2817
|
+
) : null}
|
|
2818
|
+
<h1 data-jp-field="title" className="mt-2 text-3xl font-semibold">
|
|
2819
|
+
{data.title}
|
|
2820
|
+
</h1>
|
|
2821
|
+
{data.description ? (
|
|
2822
|
+
<p data-jp-field="description" className="mt-3 max-w-2xl text-muted-foreground">
|
|
2823
|
+
{data.description}
|
|
2824
|
+
</p>
|
|
2825
|
+
) : null}
|
|
2826
|
+
|
|
2827
|
+
<ul className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
2828
|
+
{prodotti.map((prodotto) => (
|
|
2829
|
+
<li
|
|
2830
|
+
key={prodotto.id}
|
|
2831
|
+
data-jp-item-id={prodotto.id}
|
|
2832
|
+
data-jp-item-field="items"
|
|
2833
|
+
className="rounded-2xl border border-border bg-card p-6 shadow-sm"
|
|
2834
|
+
>
|
|
2835
|
+
<a
|
|
2836
|
+
href={`/prodotti/${prodotto.id}`}
|
|
2837
|
+
className="text-lg font-medium hover:underline"
|
|
2838
|
+
>
|
|
2839
|
+
{prodotto.title}
|
|
2840
|
+
</a>
|
|
2841
|
+
<p className="mt-2 text-sm text-muted-foreground">{formatPrice(prodotto)}</p>
|
|
2842
|
+
{prodotto.description ? (
|
|
2843
|
+
<p className="mt-3 text-sm text-muted-foreground">{prodotto.description}</p>
|
|
2844
|
+
) : null}
|
|
2845
|
+
</li>
|
|
2846
|
+
))}
|
|
2847
|
+
</ul>
|
|
2848
|
+
|
|
2849
|
+
{prodotti.length === 0 ? (
|
|
2850
|
+
<p className="mt-10 text-muted-foreground">No products available.</p>
|
|
2851
|
+
) : null}
|
|
2852
|
+
</div>
|
|
2853
|
+
</section>
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
END_OF_FILE_CONTENT
|
|
2858
|
+
echo "Creating src/components/prodotti-list/index.ts..."
|
|
2859
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotti-list/index.ts"
|
|
2860
|
+
export { ProdottiListView } from './View';
|
|
2861
|
+
export { ProdottiListSchema, ProdottiListSettingsSchema } from './schema';
|
|
2862
|
+
export type { ProdottiListData, ProdottiListSettings } from './types';
|
|
2863
|
+
|
|
2864
|
+
END_OF_FILE_CONTENT
|
|
2865
|
+
echo "Creating src/components/prodotti-list/schema.ts..."
|
|
2866
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotti-list/schema.ts"
|
|
2867
|
+
import { z } from 'zod';
|
|
2868
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
2869
|
+
import { ProdottoSchema } from '@/collections/prodotti';
|
|
2870
|
+
|
|
2871
|
+
export const ProdottiListSchema = BaseSectionData.extend({
|
|
2872
|
+
eyebrow: z.string().optional().describe('ui:text'),
|
|
2873
|
+
title: z.string().describe('ui:text'),
|
|
2874
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
2875
|
+
items: z.record(z.string(), ProdottoSchema).describe('ui:collection-ref:catalogo'),
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
export const ProdottiListSettingsSchema = z.object({});
|
|
2879
|
+
|
|
2880
|
+
END_OF_FILE_CONTENT
|
|
2881
|
+
echo "Creating src/components/prodotti-list/types.ts..."
|
|
2882
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotti-list/types.ts"
|
|
2883
|
+
import type { z } from 'zod';
|
|
2884
|
+
import type { ProdottiListSchema, ProdottiListSettingsSchema } from './schema';
|
|
2885
|
+
|
|
2886
|
+
export type ProdottiListData = z.infer<typeof ProdottiListSchema>;
|
|
2887
|
+
export type ProdottiListSettings = z.infer<typeof ProdottiListSettingsSchema>;
|
|
2888
|
+
|
|
2889
|
+
END_OF_FILE_CONTENT
|
|
2890
|
+
mkdir -p "src/components/prodotto-detail"
|
|
2891
|
+
echo "Creating src/components/prodotto-detail/View.tsx..."
|
|
2892
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotto-detail/View.tsx"
|
|
2893
|
+
import type { ProdottoDetailData, ProdottoDetailSettings } from './types';
|
|
2894
|
+
|
|
2895
|
+
type ProdottoDetailViewProps = {
|
|
2896
|
+
data: ProdottoDetailData;
|
|
2897
|
+
settings?: ProdottoDetailSettings;
|
|
2898
|
+
};
|
|
2899
|
+
|
|
2900
|
+
function formatPrice(price: number, currency?: string): string {
|
|
2901
|
+
return `${(price / 100).toFixed(2)} ${currency ?? 'EUR'}`;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/** RSC-safe product detail — resolved from `collection:current` (projected source). */
|
|
2905
|
+
export function ProdottoDetailView({ data }: ProdottoDetailViewProps) {
|
|
2906
|
+
const prodotto = data.item;
|
|
2907
|
+
|
|
2908
|
+
return (
|
|
2909
|
+
<main className="min-h-screen bg-background px-6 py-16 text-foreground">
|
|
2910
|
+
<article
|
|
2911
|
+
data-jp-field="item"
|
|
2912
|
+
data-jp-item-id={prodotto.id}
|
|
2913
|
+
data-jp-item-field="item"
|
|
2914
|
+
className="mx-auto w-full max-w-3xl rounded-2xl border border-border bg-card p-8 shadow-sm"
|
|
2915
|
+
>
|
|
2916
|
+
<a href="/prodotti" className="text-sm font-medium text-muted-foreground hover:text-foreground">
|
|
2917
|
+
{data.backLabel}
|
|
2918
|
+
</a>
|
|
2919
|
+
<h1 className="mt-10 text-3xl font-semibold">{prodotto.title}</h1>
|
|
2920
|
+
<p className="mt-3 text-lg">{formatPrice(prodotto.price, prodotto.currency)}</p>
|
|
2921
|
+
{prodotto.description ? (
|
|
2922
|
+
<p className="mt-6 text-muted-foreground">{prodotto.description}</p>
|
|
2923
|
+
) : null}
|
|
2924
|
+
</article>
|
|
2925
|
+
</main>
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
END_OF_FILE_CONTENT
|
|
2930
|
+
echo "Creating src/components/prodotto-detail/index.ts..."
|
|
2931
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotto-detail/index.ts"
|
|
2932
|
+
export { ProdottoDetailView } from './View';
|
|
2933
|
+
export { ProdottoDetailSchema, ProdottoDetailSettingsSchema } from './schema';
|
|
2934
|
+
export type { ProdottoDetailData, ProdottoDetailSettings } from './types';
|
|
2935
|
+
|
|
2936
|
+
END_OF_FILE_CONTENT
|
|
2937
|
+
echo "Creating src/components/prodotto-detail/schema.ts..."
|
|
2938
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotto-detail/schema.ts"
|
|
2939
|
+
import { z } from 'zod';
|
|
2940
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
2941
|
+
import { ProdottoSchema } from '@/collections/prodotti';
|
|
2942
|
+
|
|
2943
|
+
export const ProdottoDetailSchema = BaseSectionData.extend({
|
|
2944
|
+
item: ProdottoSchema.describe('ui:collection-ref'),
|
|
2945
|
+
backLabel: z.string().default('Torna al catalogo').describe('ui:text'),
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
export const ProdottoDetailSettingsSchema = z.object({});
|
|
2949
|
+
|
|
2950
|
+
END_OF_FILE_CONTENT
|
|
2951
|
+
echo "Creating src/components/prodotto-detail/types.ts..."
|
|
2952
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/prodotto-detail/types.ts"
|
|
2953
|
+
import type { z } from 'zod';
|
|
2954
|
+
import type { ProdottoDetailSchema, ProdottoDetailSettingsSchema } from './schema';
|
|
2955
|
+
|
|
2956
|
+
export type ProdottoDetailData = z.infer<typeof ProdottoDetailSchema>;
|
|
2957
|
+
export type ProdottoDetailSettings = z.infer<typeof ProdottoDetailSettingsSchema>;
|
|
2958
|
+
|
|
2664
2959
|
END_OF_FILE_CONTENT
|
|
2665
2960
|
mkdir -p "src/components/webmcp"
|
|
2666
2961
|
echo "Creating src/components/webmcp/WebMcpVisitorRuntime.tsx..."
|
|
@@ -3062,8 +3357,8 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
|
|
|
3062
3357
|
"summary": "Fuga, destino e sogno si intrecciano in un romanzo sospeso tra reale e mitico."
|
|
3063
3358
|
},
|
|
3064
3359
|
"americanah": {
|
|
3065
|
-
"id": "
|
|
3066
|
-
"title": "
|
|
3360
|
+
"id": "americana",
|
|
3361
|
+
"title": "Americana",
|
|
3067
3362
|
"author": {
|
|
3068
3363
|
"$ref": "../autori/autori.json#/chimamanda-ngozi-adichie"
|
|
3069
3364
|
},
|
|
@@ -3134,6 +3429,10 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
3134
3429
|
"label": "Get started",
|
|
3135
3430
|
"href": "#Getstarted"
|
|
3136
3431
|
},
|
|
3432
|
+
{
|
|
3433
|
+
"label": "Prodotti",
|
|
3434
|
+
"href": "/prodotti"
|
|
3435
|
+
},
|
|
3137
3436
|
{
|
|
3138
3437
|
"label": "GitHub",
|
|
3139
3438
|
"href": "https://github.com/olonjs/core"
|
|
@@ -3388,31 +3687,30 @@ mkdir -p "src/data/pages"
|
|
|
3388
3687
|
mkdir -p "src/data/pages/authors"
|
|
3389
3688
|
echo "Creating src/data/pages/authors.json..."
|
|
3390
3689
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/authors.json"
|
|
3391
|
-
{
|
|
3392
|
-
"id": "authors-page",
|
|
3393
|
-
"slug": "authors",
|
|
3394
|
-
"meta": {
|
|
3395
|
-
"title": "Authors",
|
|
3396
|
-
"description": "Author directory powered by the autori collection."
|
|
3397
|
-
},
|
|
3398
|
-
"sections": [
|
|
3399
|
-
{
|
|
3400
|
-
"id": "authors-list-1",
|
|
3401
|
-
"type": "authors-list",
|
|
3402
|
-
"data": {
|
|
3403
|
-
"anchorId": "authors",
|
|
3404
|
-
"eyebrow": "Collection demo",
|
|
3405
|
-
"title": "Authors",
|
|
3406
|
-
"description": "A collection-backed directory of authors referenced by books.",
|
|
3407
|
-
"items": {
|
|
3408
|
-
"$ref": "../collections/autori/autori.json"
|
|
3409
|
-
}
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
],
|
|
3413
|
-
"global-header": false
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3690
|
+
{
|
|
3691
|
+
"id": "authors-page",
|
|
3692
|
+
"slug": "authors",
|
|
3693
|
+
"meta": {
|
|
3694
|
+
"title": "Authors",
|
|
3695
|
+
"description": "Author directory powered by the autori collection."
|
|
3696
|
+
},
|
|
3697
|
+
"sections": [
|
|
3698
|
+
{
|
|
3699
|
+
"id": "authors-list-1",
|
|
3700
|
+
"type": "authors-list",
|
|
3701
|
+
"data": {
|
|
3702
|
+
"anchorId": "authors",
|
|
3703
|
+
"eyebrow": "Collection demo",
|
|
3704
|
+
"title": "Authors",
|
|
3705
|
+
"description": "A collection-backed directory of authors referenced by books.",
|
|
3706
|
+
"items": {
|
|
3707
|
+
"$ref": "../collections/autori/autori.json"
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
],
|
|
3712
|
+
"global-header": false
|
|
3713
|
+
}
|
|
3416
3714
|
END_OF_FILE_CONTENT
|
|
3417
3715
|
mkdir -p "src/data/pages/authors/[authorId]"
|
|
3418
3716
|
echo "Creating src/data/pages/authors/[authorId]/libri.json..."
|
|
@@ -3559,46 +3857,111 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri/[slug].json"
|
|
|
3559
3857
|
"global-header": false
|
|
3560
3858
|
}
|
|
3561
3859
|
|
|
3860
|
+
END_OF_FILE_CONTENT
|
|
3861
|
+
mkdir -p "src/data/pages/prodotti"
|
|
3862
|
+
echo "Creating src/data/pages/prodotti.json..."
|
|
3863
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/prodotti.json"
|
|
3864
|
+
{
|
|
3865
|
+
"id": "prodotti-page",
|
|
3866
|
+
"slug": "prodotti",
|
|
3867
|
+
"meta": {
|
|
3868
|
+
"title": "Prodotti",
|
|
3869
|
+
"description": "Catalogo proiettato da una sorgente esterna (demo @olonjs/sources)."
|
|
3870
|
+
},
|
|
3871
|
+
"sections": [
|
|
3872
|
+
{
|
|
3873
|
+
"id": "prodotti-list-1",
|
|
3874
|
+
"type": "prodotti-list",
|
|
3875
|
+
"data": {
|
|
3876
|
+
"anchorId": "catalogo-prodotti",
|
|
3877
|
+
"eyebrow": "Projected source demo",
|
|
3878
|
+
"title": "Prodotti",
|
|
3879
|
+
"description": "Catalogo letto da una sorgente esterna, validato con la sua Zod shape e proiettato nel contratto OlonJS.",
|
|
3880
|
+
"items": {
|
|
3881
|
+
"$ref": "../collections/catalogo/catalogo.json"
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
],
|
|
3886
|
+
"global-header": false
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
END_OF_FILE_CONTENT
|
|
3890
|
+
echo "Creating src/data/pages/prodotti/[slug].json..."
|
|
3891
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/prodotti/[slug].json"
|
|
3892
|
+
{
|
|
3893
|
+
"id": "prodotto-detail-page",
|
|
3894
|
+
"slug": "prodotti/[slug]",
|
|
3895
|
+
"meta": {
|
|
3896
|
+
"title": "Dettaglio prodotto",
|
|
3897
|
+
"description": "Pagina dinamica alimentata dalla sorgente esterna catalogo."
|
|
3898
|
+
},
|
|
3899
|
+
"sections": [
|
|
3900
|
+
{
|
|
3901
|
+
"id": "prodotto-detail-1",
|
|
3902
|
+
"type": "prodotto-detail",
|
|
3903
|
+
"data": {
|
|
3904
|
+
"anchorId": "dettaglio-prodotto",
|
|
3905
|
+
"item": {
|
|
3906
|
+
"$ref": "collection:current"
|
|
3907
|
+
},
|
|
3908
|
+
"backLabel": "← Torna al catalogo"
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
],
|
|
3912
|
+
"collection": {
|
|
3913
|
+
"source": "catalogo",
|
|
3914
|
+
"paramKey": "slug"
|
|
3915
|
+
},
|
|
3916
|
+
"global-header": false
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3562
3919
|
END_OF_FILE_CONTENT
|
|
3563
3920
|
# SKIP: src/index.css is binary and cannot be embedded as text.
|
|
3564
3921
|
mkdir -p "src/lib"
|
|
3565
3922
|
echo "Creating src/lib/CollectionRegistry.ts..."
|
|
3566
3923
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/CollectionRegistry.ts"
|
|
3567
|
-
import { AutoriCollectionSchema } from '@/collections/autori';
|
|
3568
|
-
import { LibriCollectionSchema } from '@/collections/libri';
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3924
|
+
import { AutoriCollectionSchema } from '@/collections/autori';
|
|
3925
|
+
import { LibriCollectionSchema } from '@/collections/libri';
|
|
3926
|
+
import { ProdottiCollectionSchema } from '@/collections/prodotti';
|
|
3927
|
+
|
|
3928
|
+
export const CollectionRegistry = {
|
|
3929
|
+
autori: AutoriCollectionSchema,
|
|
3930
|
+
libri: LibriCollectionSchema,
|
|
3931
|
+
catalogo: ProdottiCollectionSchema,
|
|
3932
|
+
} as const;
|
|
3933
|
+
|
|
3934
|
+
export type CollectionType = keyof typeof CollectionRegistry;
|
|
3576
3935
|
|
|
3577
3936
|
END_OF_FILE_CONTENT
|
|
3578
3937
|
echo "Creating src/lib/ComponentRegistry.tsx..."
|
|
3579
3938
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
3580
|
-
import type React from 'react';
|
|
3581
|
-
import type { SectionType } from '@/types';
|
|
3582
|
-
import type { SectionComponentPropsMap } from '@/types';
|
|
3583
|
-
import { AuthorsListView } from '@/components/authors-list';
|
|
3584
|
-
import { BookDetailView } from '@/components/book-detail';
|
|
3585
|
-
import { BooksListView } from '@/components/books-list';
|
|
3586
|
-
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
3587
|
-
import { FooterView } from '@/components/footer';
|
|
3588
|
-
import { FormDemoView } from '@/components/form-demo';
|
|
3589
|
-
import { HeaderView } from '@/components/header';
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
'
|
|
3597
|
-
'
|
|
3598
|
-
|
|
3599
|
-
'
|
|
3600
|
-
|
|
3601
|
-
|
|
3939
|
+
import type React from 'react';
|
|
3940
|
+
import type { SectionType } from '@/types';
|
|
3941
|
+
import type { SectionComponentPropsMap } from '@/types';
|
|
3942
|
+
import { AuthorsListView } from '@/components/authors-list';
|
|
3943
|
+
import { BookDetailView } from '@/components/book-detail';
|
|
3944
|
+
import { BooksListView } from '@/components/books-list';
|
|
3945
|
+
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
3946
|
+
import { FooterView } from '@/components/footer';
|
|
3947
|
+
import { FormDemoView } from '@/components/form-demo';
|
|
3948
|
+
import { HeaderView } from '@/components/header';
|
|
3949
|
+
import { ProdottoDetailView } from '@/components/prodotto-detail';
|
|
3950
|
+
import { ProdottiListView } from '@/components/prodotti-list';
|
|
3951
|
+
|
|
3952
|
+
export const ComponentRegistry: {
|
|
3953
|
+
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
3954
|
+
} = {
|
|
3955
|
+
'authors-list': AuthorsListView as React.FC<SectionComponentPropsMap['authors-list']>,
|
|
3956
|
+
'book-detail': BookDetailView as React.FC<SectionComponentPropsMap['book-detail']>,
|
|
3957
|
+
'books-list': BooksListView as React.FC<SectionComponentPropsMap['books-list']>,
|
|
3958
|
+
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
3959
|
+
footer: FooterView as React.FC<SectionComponentPropsMap['footer']>,
|
|
3960
|
+
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
3961
|
+
header: HeaderView as React.FC<SectionComponentPropsMap['header']>,
|
|
3962
|
+
'prodotti-list': ProdottiListView as React.FC<SectionComponentPropsMap['prodotti-list']>,
|
|
3963
|
+
'prodotto-detail': ProdottoDetailView as React.FC<SectionComponentPropsMap['prodotto-detail']>,
|
|
3964
|
+
};
|
|
3602
3965
|
|
|
3603
3966
|
END_OF_FILE_CONTENT
|
|
3604
3967
|
echo "Creating src/lib/IconResolver.tsx..."
|
|
@@ -5152,13 +5515,57 @@ END_OF_FILE_CONTENT
|
|
|
5152
5515
|
echo "Creating src/lib/loaders/loadPublicPageBundleForRequest.ts..."
|
|
5153
5516
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/loaders/loadPublicPageBundleForRequest.ts"
|
|
5154
5517
|
import type { PublicPageContentBundle } from '@olonjs/next/server';
|
|
5518
|
+
import { resolvePageMatchFromRegistry } from '@olonjs/core';
|
|
5519
|
+
import { collectCollectionSources, resolveCollectionSources } from '@olonjs/sources';
|
|
5155
5520
|
import {
|
|
5156
5521
|
buildServerApiCandidates,
|
|
5157
5522
|
type ServerCloudBootSource,
|
|
5158
5523
|
} from '@/lib/env/serverCloudPolicy';
|
|
5524
|
+
import { collectionSources } from '@/sources';
|
|
5159
5525
|
import { loadLivePublicPageBundle } from './loadLivePublicPageBundle';
|
|
5160
5526
|
import { loadLocalPublicPageBundle } from './loadLocalPublicPageBundle';
|
|
5161
5527
|
|
|
5528
|
+
/**
|
|
5529
|
+
* Project external collection sources referenced by the requested slug's page
|
|
5530
|
+
* (ADR-0018). Without this, page JSON endpoints would expose unresolved $refs.
|
|
5531
|
+
*/
|
|
5532
|
+
export async function withProjectedSources(
|
|
5533
|
+
bundle: PublicPageContentBundle,
|
|
5534
|
+
slug: string,
|
|
5535
|
+
appRoot?: string,
|
|
5536
|
+
): Promise<PublicPageContentBundle> {
|
|
5537
|
+
const match = resolvePageMatchFromRegistry(
|
|
5538
|
+
bundle.pages as never,
|
|
5539
|
+
slug
|
|
5540
|
+
);
|
|
5541
|
+
if (!match) return bundle;
|
|
5542
|
+
|
|
5543
|
+
const needed = collectCollectionSources(match.page).filter(
|
|
5544
|
+
(name) => name in collectionSources
|
|
5545
|
+
);
|
|
5546
|
+
if (needed.length === 0) return bundle;
|
|
5547
|
+
|
|
5548
|
+
const resolved = await resolveCollectionSources({
|
|
5549
|
+
sources: collectionSources as never,
|
|
5550
|
+
needed,
|
|
5551
|
+
params: match.params,
|
|
5552
|
+
});
|
|
5553
|
+
|
|
5554
|
+
return {
|
|
5555
|
+
...bundle,
|
|
5556
|
+
collections: {
|
|
5557
|
+
...(bundle.collections ?? {}),
|
|
5558
|
+
...(resolved.collections as NonNullable<PublicPageContentBundle['collections']>),
|
|
5559
|
+
},
|
|
5560
|
+
collectionSchemas: {
|
|
5561
|
+
...(bundle.collectionSchemas ?? {}),
|
|
5562
|
+
...(resolved.collectionSchemas as NonNullable<
|
|
5563
|
+
PublicPageContentBundle['collectionSchemas']
|
|
5564
|
+
>),
|
|
5565
|
+
},
|
|
5566
|
+
};
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5162
5569
|
export type LoadPublicPageBundleForRequestInput = {
|
|
5163
5570
|
bootSource: ServerCloudBootSource;
|
|
5164
5571
|
slug: string;
|
|
@@ -5190,18 +5597,76 @@ export async function loadPublicPageBundleForRequest(
|
|
|
5190
5597
|
if (!apiUrl || !apiKey) {
|
|
5191
5598
|
throw new Error('Live public page JSON requires cloud API URL and key');
|
|
5192
5599
|
}
|
|
5193
|
-
|
|
5600
|
+
const liveBundle = await loadLivePublicPageBundle({
|
|
5194
5601
|
slug: input.slug,
|
|
5195
5602
|
apiBases: buildServerApiCandidates(apiUrl),
|
|
5196
5603
|
apiKey,
|
|
5197
5604
|
appRoot,
|
|
5198
5605
|
fetchImpl: input.fetchImpl,
|
|
5199
5606
|
});
|
|
5607
|
+
return withProjectedSources(liveBundle, input.slug, appRoot);
|
|
5200
5608
|
}
|
|
5201
5609
|
|
|
5202
|
-
|
|
5610
|
+
const localBundle = await loadLocalPublicPageBundle(appRoot);
|
|
5611
|
+
return withProjectedSources(localBundle, input.slug, appRoot);
|
|
5203
5612
|
}
|
|
5204
5613
|
|
|
5614
|
+
END_OF_FILE_CONTENT
|
|
5615
|
+
echo "Creating src/lib/loaders/loadVisitorPageBundle.ts..."
|
|
5616
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/loaders/loadVisitorPageBundle.ts"
|
|
5617
|
+
import type { JsonPagesConfig, PageConfig } from '@olonjs/core';
|
|
5618
|
+
import { collectCollectionSources, resolveCollectionSources } from '@olonjs/sources';
|
|
5619
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
5620
|
+
import { collectionSources } from '@/sources';
|
|
5621
|
+
import { getFileCollections } from './getFileCollections';
|
|
5622
|
+
|
|
5623
|
+
export interface VisitorPageBundle {
|
|
5624
|
+
collections: NonNullable<JsonPagesConfig['collections']>;
|
|
5625
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
5626
|
+
/** Provenance of the projected sources resolved for this route. */
|
|
5627
|
+
sourceProvenance: Record<string, { fetchedAt: string; readOnly: boolean }>;
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5630
|
+
/**
|
|
5631
|
+
* Load the per-route content bundle: file-backed collections plus the external
|
|
5632
|
+
* sources this page needs (projected, validated, read-only).
|
|
5633
|
+
*
|
|
5634
|
+
* Sources are resolved ONLY when referenced by the page (ADR-0018 / spec
|
|
5635
|
+
* external-collection-sources). Source names not registered as external
|
|
5636
|
+
* sources are file-backed and skipped here.
|
|
5637
|
+
*/
|
|
5638
|
+
export async function loadVisitorPageBundle(input: {
|
|
5639
|
+
page: PageConfig;
|
|
5640
|
+
params: Record<string, string>;
|
|
5641
|
+
appRoot?: string;
|
|
5642
|
+
}): Promise<VisitorPageBundle> {
|
|
5643
|
+
const fileCollections = getFileCollections(input.appRoot) as NonNullable<
|
|
5644
|
+
JsonPagesConfig['collections']
|
|
5645
|
+
>;
|
|
5646
|
+
|
|
5647
|
+
const needed = collectCollectionSources(input.page).filter(
|
|
5648
|
+
(name) => name in collectionSources
|
|
5649
|
+
);
|
|
5650
|
+
|
|
5651
|
+
const resolved = await resolveCollectionSources({
|
|
5652
|
+
sources: collectionSources as never,
|
|
5653
|
+
needed,
|
|
5654
|
+
params: input.params,
|
|
5655
|
+
});
|
|
5656
|
+
|
|
5657
|
+
return {
|
|
5658
|
+
collections: {
|
|
5659
|
+
...fileCollections,
|
|
5660
|
+
...(resolved.collections as NonNullable<JsonPagesConfig['collections']>),
|
|
5661
|
+
},
|
|
5662
|
+
collectionSchemas: {
|
|
5663
|
+
...(CollectionRegistry as JsonPagesConfig['collectionSchemas']),
|
|
5664
|
+
...(resolved.collectionSchemas as JsonPagesConfig['collectionSchemas']),
|
|
5665
|
+
},
|
|
5666
|
+
sourceProvenance: resolved.provenance,
|
|
5667
|
+
};
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5205
5670
|
END_OF_FILE_CONTENT
|
|
5206
5671
|
echo "Creating src/lib/loaders/publishedContent.test.ts..."
|
|
5207
5672
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/loaders/publishedContent.test.ts"
|
|
@@ -5347,45 +5812,49 @@ export function resolveVisitorShell(page: PageConfig, site: SiteConfig): Visitor
|
|
|
5347
5812
|
END_OF_FILE_CONTENT
|
|
5348
5813
|
echo "Creating src/lib/schemas.ts..."
|
|
5349
5814
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
5350
|
-
import { AuthorsListSchema } from '@/components/authors-list';
|
|
5351
|
-
import { BookDetailSchema } from '@/components/book-detail';
|
|
5352
|
-
import { BooksListSchema } from '@/components/books-list';
|
|
5353
|
-
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
5354
|
-
import { FooterSchema } from '@/components/footer';
|
|
5355
|
-
import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
|
|
5356
|
-
import { HeaderSchema } from '@/components/header';
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
'
|
|
5362
|
-
'
|
|
5363
|
-
|
|
5364
|
-
'
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
*
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
export
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5815
|
+
import { AuthorsListSchema } from '@/components/authors-list';
|
|
5816
|
+
import { BookDetailSchema } from '@/components/book-detail';
|
|
5817
|
+
import { BooksListSchema } from '@/components/books-list';
|
|
5818
|
+
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
5819
|
+
import { FooterSchema } from '@/components/footer';
|
|
5820
|
+
import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
|
|
5821
|
+
import { HeaderSchema } from '@/components/header';
|
|
5822
|
+
import { ProdottoDetailSchema } from '@/components/prodotto-detail';
|
|
5823
|
+
import { ProdottiListSchema } from '@/components/prodotti-list';
|
|
5824
|
+
|
|
5825
|
+
export const SECTION_SCHEMAS = {
|
|
5826
|
+
'authors-list': AuthorsListSchema,
|
|
5827
|
+
'book-detail': BookDetailSchema,
|
|
5828
|
+
'books-list': BooksListSchema,
|
|
5829
|
+
'empty-tenant': EmptyTenantSchema,
|
|
5830
|
+
footer: FooterSchema,
|
|
5831
|
+
'form-demo': FormDemoSchema,
|
|
5832
|
+
header: HeaderSchema,
|
|
5833
|
+
'prodotti-list': ProdottiListSchema,
|
|
5834
|
+
'prodotto-detail': ProdottoDetailSchema,
|
|
5835
|
+
} as const;
|
|
5836
|
+
|
|
5837
|
+
/**
|
|
5838
|
+
* Registry of per-section-type submission schemas. Keys MUST match a key of
|
|
5839
|
+
* SECTION_SCHEMAS. A section type appearing here is declaring itself as
|
|
5840
|
+
* MCP-submittable: the Zod schema describes the payload accepted by the form.
|
|
5841
|
+
*
|
|
5842
|
+
* See ADR-0002 (docs/decisions/ADR-0002-form-submission-schemas.md).
|
|
5843
|
+
*/
|
|
5844
|
+
export const SECTION_SUBMISSION_SCHEMAS = {
|
|
5845
|
+
'form-demo': FormDemoSubmissionSchema,
|
|
5846
|
+
} as const;
|
|
5847
|
+
|
|
5848
|
+
export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
5849
|
+
|
|
5850
|
+
export {
|
|
5851
|
+
BaseSectionData,
|
|
5852
|
+
BaseArrayItem,
|
|
5853
|
+
BaseCollectionItem,
|
|
5854
|
+
BaseSectionSettingsSchema,
|
|
5855
|
+
CtaSchema,
|
|
5856
|
+
ImageSelectionSchema,
|
|
5857
|
+
} from '@olonjs/core';
|
|
5389
5858
|
|
|
5390
5859
|
END_OF_FILE_CONTENT
|
|
5391
5860
|
echo "Creating src/lib/utils.ts..."
|
|
@@ -6037,55 +6506,194 @@ describe('visitor page WebMCP wiring', () => {
|
|
|
6037
6506
|
});
|
|
6038
6507
|
});
|
|
6039
6508
|
|
|
6509
|
+
END_OF_FILE_CONTENT
|
|
6510
|
+
mkdir -p "src/sources"
|
|
6511
|
+
echo "Creating src/sources/catalogo.test.ts..."
|
|
6512
|
+
cat << 'END_OF_FILE_CONTENT' > "src/sources/catalogo.test.ts"
|
|
6513
|
+
import { describe, expect, it } from 'vitest';
|
|
6514
|
+
import { runSourceConformance } from '@olonjs/sources';
|
|
6515
|
+
import { catalogoSource, fetchCatalogoFromExternalService } from './catalogo';
|
|
6516
|
+
|
|
6517
|
+
describe('catalogo (demo external source)', () => {
|
|
6518
|
+
it('passes the provider conformance suite', async () => {
|
|
6519
|
+
await expect(runSourceConformance(catalogoSource)).resolves.toBeUndefined();
|
|
6520
|
+
});
|
|
6521
|
+
|
|
6522
|
+
it('projects a deterministic document keyed by product id', async () => {
|
|
6523
|
+
const document = await fetchCatalogoFromExternalService();
|
|
6524
|
+
|
|
6525
|
+
expect(Object.keys(document).sort()).toEqual([
|
|
6526
|
+
'1984',
|
|
6527
|
+
'dune',
|
|
6528
|
+
'il-nome-della-rosa',
|
|
6529
|
+
'neuromancer',
|
|
6530
|
+
]);
|
|
6531
|
+
for (const [key, item] of Object.entries(document)) {
|
|
6532
|
+
expect(item.id).toBe(key);
|
|
6533
|
+
}
|
|
6534
|
+
});
|
|
6535
|
+
|
|
6536
|
+
it('surfaces failures for fallback/breaker policy to consume', async () => {
|
|
6537
|
+
await expect(fetchCatalogoFromExternalService({ fail: true })).rejects.toThrow(
|
|
6538
|
+
/unavailable/
|
|
6539
|
+
);
|
|
6540
|
+
});
|
|
6541
|
+
});
|
|
6542
|
+
|
|
6543
|
+
END_OF_FILE_CONTENT
|
|
6544
|
+
echo "Creating src/sources/catalogo.ts..."
|
|
6545
|
+
cat << 'END_OF_FILE_CONTENT' > "src/sources/catalogo.ts"
|
|
6546
|
+
import { z } from 'zod';
|
|
6547
|
+
import { defineCollectionSource, withCache } from '@olonjs/sources';
|
|
6548
|
+
import { ProdottoSchema, type Prodotto } from '@/collections/prodotti';
|
|
6549
|
+
|
|
6550
|
+
/**
|
|
6551
|
+
* Demo provider: a DETERMINISTIC, local stand-in for an external catalog
|
|
6552
|
+
* service (no network — keeps dev, tests and CI hermetic). The real-world twin
|
|
6553
|
+
* would be `src/sources/shopify.ts` implementing the same contract.
|
|
6554
|
+
*
|
|
6555
|
+
* The `shape` is the single declaration: it validates the projected document
|
|
6556
|
+
* and feeds the agent contracts (`/schemas/collections/catalogo.schema.json`).
|
|
6557
|
+
*/
|
|
6558
|
+
|
|
6559
|
+
const CATALOGO_FIXTURE: Record<string, Prodotto> = {
|
|
6560
|
+
dune: {
|
|
6561
|
+
id: 'dune',
|
|
6562
|
+
title: 'Dune — Edizione illustrata',
|
|
6563
|
+
price: 2490,
|
|
6564
|
+
currency: 'EUR',
|
|
6565
|
+
description: 'Arrakis, spezia e politica: il classico di Frank Herbert.',
|
|
6566
|
+
},
|
|
6567
|
+
'1984': {
|
|
6568
|
+
id: '1984',
|
|
6569
|
+
title: '1984',
|
|
6570
|
+
price: 1490,
|
|
6571
|
+
currency: 'EUR',
|
|
6572
|
+
description: 'Il Grande Fratello e il controllo del linguaggio.',
|
|
6573
|
+
},
|
|
6574
|
+
neuromancer: {
|
|
6575
|
+
id: 'neuromancer',
|
|
6576
|
+
title: 'Neuromancer',
|
|
6577
|
+
price: 1890,
|
|
6578
|
+
currency: 'EUR',
|
|
6579
|
+
description: 'Il romanzo che ha definito il cyberpunk.',
|
|
6580
|
+
},
|
|
6581
|
+
'il-nome-della-rosa': {
|
|
6582
|
+
id: 'il-nome-della-rosa',
|
|
6583
|
+
title: 'Il nome della rosa',
|
|
6584
|
+
price: 2100,
|
|
6585
|
+
currency: 'EUR',
|
|
6586
|
+
description: 'Un giallo medievale tra biblioteche e potere.',
|
|
6587
|
+
},
|
|
6588
|
+
};
|
|
6589
|
+
|
|
6590
|
+
export interface ExternalCatalogServiceOptions {
|
|
6591
|
+
/** Simulated latency in ms (default 0 — deterministic tests). */
|
|
6592
|
+
latencyMs?: number;
|
|
6593
|
+
/** Force a failure, to exercise fallback/breaker policy. */
|
|
6594
|
+
fail?: boolean;
|
|
6595
|
+
signal?: AbortSignal;
|
|
6596
|
+
}
|
|
6597
|
+
|
|
6598
|
+
/** Stand-in for the external system of record (Shopify/PIM/…). */
|
|
6599
|
+
export async function fetchCatalogoFromExternalService(
|
|
6600
|
+
options: ExternalCatalogServiceOptions = {}
|
|
6601
|
+
): Promise<Record<string, Prodotto>> {
|
|
6602
|
+
const { latencyMs = 0, fail = false, signal } = options;
|
|
6603
|
+
|
|
6604
|
+
if (latencyMs > 0) {
|
|
6605
|
+
await new Promise((resolve) => setTimeout(resolve, latencyMs));
|
|
6606
|
+
}
|
|
6607
|
+
if (signal?.aborted) throw new Error('The catalog request was aborted');
|
|
6608
|
+
if (fail) throw new Error('External catalog service unavailable');
|
|
6609
|
+
|
|
6610
|
+
return JSON.parse(JSON.stringify(CATALOGO_FIXTURE)) as Record<string, Prodotto>;
|
|
6611
|
+
}
|
|
6612
|
+
|
|
6613
|
+
export const catalogoSource = withCache(
|
|
6614
|
+
defineCollectionSource({
|
|
6615
|
+
shape: z.record(z.string(), ProdottoSchema),
|
|
6616
|
+
resolve: async ({ signal }) => fetchCatalogoFromExternalService({ signal }),
|
|
6617
|
+
readOnly: true,
|
|
6618
|
+
}),
|
|
6619
|
+
{ ttlMs: 60_000, swrMs: 300_000 }
|
|
6620
|
+
);
|
|
6621
|
+
|
|
6622
|
+
END_OF_FILE_CONTENT
|
|
6623
|
+
echo "Creating src/sources/index.ts..."
|
|
6624
|
+
cat << 'END_OF_FILE_CONTENT' > "src/sources/index.ts"
|
|
6625
|
+
import { catalogoSource } from './catalogo';
|
|
6626
|
+
|
|
6627
|
+
/**
|
|
6628
|
+
* Tenant source registry. Adding a provider is a one-line change here — no
|
|
6629
|
+
* changes to @olonjs/core (ADR-0018). Sources not listed here are file-backed
|
|
6630
|
+
* collections loaded from src/data/collections.
|
|
6631
|
+
*/
|
|
6632
|
+
export const collectionSources = {
|
|
6633
|
+
catalogo: catalogoSource,
|
|
6634
|
+
} as const;
|
|
6635
|
+
|
|
6636
|
+
export type CollectionSourceName = keyof typeof collectionSources;
|
|
6637
|
+
|
|
6040
6638
|
END_OF_FILE_CONTENT
|
|
6041
6639
|
echo "Creating src/types.ts..."
|
|
6042
6640
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
6043
|
-
import type { AuthorsListData, AuthorsListSettings } from '@/components/authors-list';
|
|
6044
|
-
import type { BookDetailData, BookDetailSettings } from '@/components/book-detail';
|
|
6045
|
-
import type { BooksListData, BooksListSettings } from '@/components/books-list';
|
|
6046
|
-
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
6047
|
-
import type { FooterData, FooterSettings } from '@/components/footer';
|
|
6048
|
-
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
6049
|
-
import type { HeaderData, HeaderSettings } from '@/components/header';
|
|
6050
|
-
import type {
|
|
6051
|
-
import type {
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
'
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
'
|
|
6061
|
-
};
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
'
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
'
|
|
6077
|
-
'
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6641
|
+
import type { AuthorsListData, AuthorsListSettings } from '@/components/authors-list';
|
|
6642
|
+
import type { BookDetailData, BookDetailSettings } from '@/components/book-detail';
|
|
6643
|
+
import type { BooksListData, BooksListSettings } from '@/components/books-list';
|
|
6644
|
+
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
6645
|
+
import type { FooterData, FooterSettings } from '@/components/footer';
|
|
6646
|
+
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
6647
|
+
import type { HeaderData, HeaderSettings } from '@/components/header';
|
|
6648
|
+
import type { ProdottoDetailData, ProdottoDetailSettings } from '@/components/prodotto-detail';
|
|
6649
|
+
import type { ProdottiListData, ProdottiListSettings } from '@/components/prodotti-list';
|
|
6650
|
+
import type { Autore } from '@/collections/autori';
|
|
6651
|
+
import type { Libro } from '@/collections/libri';
|
|
6652
|
+
import type { Prodotto } from '@/collections/prodotti';
|
|
6653
|
+
|
|
6654
|
+
export type SectionComponentPropsMap = {
|
|
6655
|
+
'authors-list': { data: AuthorsListData; settings?: AuthorsListSettings };
|
|
6656
|
+
'book-detail': { data: BookDetailData; settings?: BookDetailSettings };
|
|
6657
|
+
'books-list': { data: BooksListData; settings?: BooksListSettings };
|
|
6658
|
+
'empty-tenant': { data?: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
6659
|
+
footer: { data: FooterData; settings?: FooterSettings };
|
|
6660
|
+
header: { data: HeaderData; settings?: HeaderSettings };
|
|
6661
|
+
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
6662
|
+
'prodotti-list': { data: ProdottiListData; settings?: ProdottiListSettings };
|
|
6663
|
+
'prodotto-detail': { data: ProdottoDetailData; settings?: ProdottoDetailSettings };
|
|
6664
|
+
};
|
|
6665
|
+
|
|
6666
|
+
declare module '@olonjs/core' {
|
|
6667
|
+
export interface SectionDataRegistry {
|
|
6668
|
+
'authors-list': AuthorsListData;
|
|
6669
|
+
'book-detail': BookDetailData;
|
|
6670
|
+
'books-list': BooksListData;
|
|
6671
|
+
'empty-tenant': EmptyTenantData;
|
|
6672
|
+
footer: FooterData;
|
|
6673
|
+
header: HeaderData;
|
|
6674
|
+
'form-demo': FormDemoData;
|
|
6675
|
+
'prodotti-list': ProdottiListData;
|
|
6676
|
+
'prodotto-detail': ProdottoDetailData;
|
|
6677
|
+
}
|
|
6678
|
+
export interface SectionSettingsRegistry {
|
|
6679
|
+
'authors-list': AuthorsListSettings;
|
|
6680
|
+
'book-detail': BookDetailSettings;
|
|
6681
|
+
'books-list': BooksListSettings;
|
|
6682
|
+
'empty-tenant': EmptyTenantSettings;
|
|
6683
|
+
footer: FooterSettings;
|
|
6684
|
+
header: HeaderSettings;
|
|
6685
|
+
'form-demo': FormDemoSettings;
|
|
6686
|
+
'prodotti-list': ProdottiListSettings;
|
|
6687
|
+
'prodotto-detail': ProdottoDetailSettings;
|
|
6688
|
+
}
|
|
6689
|
+
export interface CollectionItemRegistry {
|
|
6690
|
+
autori: Autore;
|
|
6691
|
+
libri: Libro;
|
|
6692
|
+
prodotti: Prodotto;
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
|
|
6696
|
+
export * from '@olonjs/core';
|
|
6089
6697
|
|
|
6090
6698
|
END_OF_FILE_CONTENT
|
|
6091
6699
|
mkdir -p "templates"
|
|
@@ -7394,7 +8002,7 @@ export const ContactFormSchema = BaseSectionData.merge(WithFormRecipient).extend
|
|
|
7394
8002
|
});
|
|
7395
8003
|
export const ContactFormSubmissionSchema = z.object({
|
|
7396
8004
|
name: z.string().min(1).describe('Full name'),
|
|
7397
|
-
email: z.
|
|
8005
|
+
email: z.email().describe('Reply email'),
|
|
7398
8006
|
message: z.string().min(1).describe('Message body'),
|
|
7399
8007
|
});
|
|
7400
8008
|
EOF
|
|
@@ -11368,6 +11976,8 @@ import type { TagPostsData, TagPostsSettings } from '@/components/tag-posts';
|
|
|
11368
11976
|
import type { ContentBlockData, ContentBlockSettings } from '@/components/content-block';
|
|
11369
11977
|
import type { StatsBandData, StatsBandSettings } from '@/components/stats-band';
|
|
11370
11978
|
import type { CtaBannerData, CtaBannerSettings } from '@/components/cta-banner';
|
|
11979
|
+
import type { Post } from '@/collections/posts';
|
|
11980
|
+
import type { Tag } from '@/collections/tags';
|
|
11371
11981
|
|
|
11372
11982
|
export type SectionComponentPropsMap = {
|
|
11373
11983
|
'header': { data: HeaderData; settings: HeaderSettings };
|
|
@@ -11416,6 +12026,10 @@ declare module '@olonjs/core' {
|
|
|
11416
12026
|
'stats-band': StatsBandSettings;
|
|
11417
12027
|
'cta-banner': CtaBannerSettings;
|
|
11418
12028
|
}
|
|
12029
|
+
export interface CollectionItemRegistry {
|
|
12030
|
+
posts: Post;
|
|
12031
|
+
tags: Tag;
|
|
12032
|
+
}
|
|
11419
12033
|
}
|
|
11420
12034
|
|
|
11421
12035
|
export * from '@olonjs/core';
|