@olonjs/cli 3.0.153 → 3.0.155
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 +415 -341
- package/assets/templates/alpha/src_tenant.sh +415 -341
- package/assets/templates/next/manifest.json +5 -0
- package/assets/templates/next/src_tenant.sh +4174 -0
- package/package.json +2 -1
- package/src/index.js +78 -25
- package/src/selectWithArrows.js +71 -0
- package/src/templateChoice.js +29 -0
- package/src/templateChoice.test.js +38 -0
|
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1821
|
-
"@olonjs/react": "^0.1.
|
|
1822
|
-
"@olonjs/studio": "^0.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.22",
|
|
1821
|
+
"@olonjs/react": "^0.1.5",
|
|
1822
|
+
"@olonjs/studio": "^0.1.5",
|
|
1823
1823
|
"class-variance-authority": "^0.7.1",
|
|
1824
1824
|
"clsx": "^2.1.1",
|
|
1825
1825
|
"lucide-react": "^0.474.0",
|
|
@@ -2641,6 +2641,371 @@ if (fs.existsSync(sourceSiteConfigPath)) {
|
|
|
2641
2641
|
|
|
2642
2642
|
console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
|
|
2643
2643
|
|
|
2644
|
+
END_OF_FILE_CONTENT
|
|
2645
|
+
mkdir -p "scripts/vite"
|
|
2646
|
+
echo "Creating scripts/vite/tenantDevApiPlugin.ts..."
|
|
2647
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/vite/tenantDevApiPlugin.ts"
|
|
2648
|
+
/**
|
|
2649
|
+
* Dev-only Vite middleware: local CMS APIs + WebMCP / page JSON parity.
|
|
2650
|
+
* Not used in production builds (configureServer only).
|
|
2651
|
+
*/
|
|
2652
|
+
import type { Plugin, ViteDevServer } from 'vite';
|
|
2653
|
+
import path from 'path';
|
|
2654
|
+
import fs from 'fs';
|
|
2655
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2656
|
+
|
|
2657
|
+
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
2658
|
+
const IMAGE_MIMES = new Set([
|
|
2659
|
+
'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
|
|
2660
|
+
]);
|
|
2661
|
+
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
|
|
2662
|
+
|
|
2663
|
+
export type TenantDevApiPluginOptions = {
|
|
2664
|
+
rootDir: string;
|
|
2665
|
+
};
|
|
2666
|
+
|
|
2667
|
+
function safeFilename(original: string, mimeType: string | undefined) {
|
|
2668
|
+
const base = (original.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9-_]/g, '_').slice(0, 128)) || 'image';
|
|
2669
|
+
const ext = original.includes('.')
|
|
2670
|
+
? path.extname(original).toLowerCase()
|
|
2671
|
+
: (mimeType?.startsWith('image/')
|
|
2672
|
+
? `.${(mimeType.split('/')[1] || 'png').replace('jpeg', 'jpg')}`
|
|
2673
|
+
: '.png');
|
|
2674
|
+
return `${Date.now()}-${base}${IMAGE_EXT.has(ext) ? ext : '.png'}`;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function listImagesInDir(dir: string, urlPrefix: string) {
|
|
2678
|
+
const list: Array<{ id: string; url: string; alt: string; tags: string[] }> = [];
|
|
2679
|
+
if (!fs.existsSync(dir)) return list;
|
|
2680
|
+
for (const name of fs.readdirSync(dir)) {
|
|
2681
|
+
if (IMAGE_EXT.has(path.extname(name).toLowerCase())) {
|
|
2682
|
+
list.push({ id: name, url: `${urlPrefix}/${name}`, alt: name, tags: [] });
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
return list;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function sendJson(res: ServerResponse, status: number, body: unknown) {
|
|
2689
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2690
|
+
res.end(JSON.stringify(body));
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function safeDataSlugPath(rootDir: string, rawSlug: string, fallback: string) {
|
|
2694
|
+
const slug = String(rawSlug || fallback)
|
|
2695
|
+
.replace(/\\/g, '/')
|
|
2696
|
+
.replace(/^\/+|\/+$/g, '');
|
|
2697
|
+
const segments = slug
|
|
2698
|
+
.split('/')
|
|
2699
|
+
.map((segment) => segment.replace(/[^a-zA-Z0-9_[\]-]/g, '_'))
|
|
2700
|
+
.filter(Boolean);
|
|
2701
|
+
const candidate = path.resolve(rootDir, `${segments.join(path.sep) || fallback}.json`);
|
|
2702
|
+
const isInsideRoot = candidate.startsWith(`${rootDir}${path.sep}`) || candidate === rootDir;
|
|
2703
|
+
if (!isInsideRoot) throw new Error('Invalid data path');
|
|
2704
|
+
return candidate;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function isTenantPageJsonRequest(req: IncomingMessage, pathname: string) {
|
|
2708
|
+
if (req.method !== 'GET' || !pathname.endsWith('.json')) return false;
|
|
2709
|
+
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
2710
|
+
return !viteOrStaticPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
function normalizeManifestSlug(raw: string) {
|
|
2714
|
+
return decodeURIComponent(raw || '')
|
|
2715
|
+
.replace(/^\/+|\/+$/g, '')
|
|
2716
|
+
.replace(/\\/g, '/')
|
|
2717
|
+
.replace(/(\.schema)?\.json$/i, '');
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function applyDevSliceFilters(
|
|
2721
|
+
page: { sections?: Array<{ data?: Record<string, unknown> }> },
|
|
2722
|
+
authored: { sections?: Array<{ data?: Record<string, unknown> }> } | undefined,
|
|
2723
|
+
params: Record<string, string>,
|
|
2724
|
+
) {
|
|
2725
|
+
if (!authored?.sections || !page?.sections) return page;
|
|
2726
|
+
const at = (o: unknown, p: string) =>
|
|
2727
|
+
p.split('.').reduce<unknown>((a, k) => (a as Record<string, unknown> | undefined)?.[k], o);
|
|
2728
|
+
return {
|
|
2729
|
+
...page,
|
|
2730
|
+
sections: page.sections.map((section, i) => {
|
|
2731
|
+
const src = authored.sections?.[i]?.data as Record<string, { $sliceFilter?: Record<string, unknown> }> | undefined;
|
|
2732
|
+
if (!src || !section.data) return section;
|
|
2733
|
+
const data = { ...section.data };
|
|
2734
|
+
for (const [key, ref] of Object.entries(src)) {
|
|
2735
|
+
if (!ref?.$sliceFilter || typeof data[key] !== 'object' || data[key] === null) continue;
|
|
2736
|
+
const filter = Object.fromEntries(
|
|
2737
|
+
Object.entries(ref.$sliceFilter)
|
|
2738
|
+
.map(([k, v]) => [
|
|
2739
|
+
k,
|
|
2740
|
+
typeof v === 'string' ? v : params[(v as { $routeParam?: string })?.$routeParam ?? ''] ?? '',
|
|
2741
|
+
])
|
|
2742
|
+
.filter(([, v]) => v),
|
|
2743
|
+
);
|
|
2744
|
+
data[key] = Object.fromEntries(
|
|
2745
|
+
Object.entries(data[key] as Record<string, unknown>).filter(([, item]) =>
|
|
2746
|
+
Object.entries(filter).every(([p, v]) => String(at(item, p) ?? '') === v),
|
|
2747
|
+
),
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
return { ...section, data };
|
|
2751
|
+
}),
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
async function loadWebMcpBuilders() {
|
|
2756
|
+
const moduleUrl = import.meta.resolve('@olonjs/core');
|
|
2757
|
+
return import(moduleUrl);
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
export function tenantDevApiPlugin(options: TenantDevApiPluginOptions): Plugin {
|
|
2761
|
+
const { rootDir } = options;
|
|
2762
|
+
const ASSETS_IMAGES_DIR = path.resolve(rootDir, 'public', 'assets', 'images');
|
|
2763
|
+
const DATA_CONFIG_DIR = path.resolve(rootDir, 'src', 'data', 'config');
|
|
2764
|
+
const DATA_PAGES_DIR = path.resolve(rootDir, 'src', 'data', 'pages');
|
|
2765
|
+
const DATA_COLLECTIONS_DIR = path.resolve(rootDir, 'src', 'data', 'collections');
|
|
2766
|
+
|
|
2767
|
+
return {
|
|
2768
|
+
name: 'upload-asset-api',
|
|
2769
|
+
configureServer(server: ViteDevServer) {
|
|
2770
|
+
server.middlewares.use((req, res, next) => {
|
|
2771
|
+
const pathname = (req.url || '').split('?')[0];
|
|
2772
|
+
const isPageJsonRequest = isTenantPageJsonRequest(req, pathname);
|
|
2773
|
+
|
|
2774
|
+
const handleManifestRequest = async () => {
|
|
2775
|
+
const core = await loadWebMcpBuilders();
|
|
2776
|
+
const { buildPageContract, buildPageManifest, buildSiteManifest, buildLlmsTxt } = core.webmcp;
|
|
2777
|
+
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
2778
|
+
const buildState = runtime.getWebMcpBuildState();
|
|
2779
|
+
|
|
2780
|
+
if (req.method === 'GET' && pathname === '/llms.txt') {
|
|
2781
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
2782
|
+
res.end(buildLlmsTxt({
|
|
2783
|
+
pages: buildState.pages,
|
|
2784
|
+
schemas: buildState.schemas,
|
|
2785
|
+
siteConfig: buildState.siteConfig,
|
|
2786
|
+
}));
|
|
2787
|
+
return true;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
if (req.method === 'GET' && pathname === '/mcp-manifest.json') {
|
|
2791
|
+
sendJson(res, 200, buildSiteManifest({
|
|
2792
|
+
pages: buildState.pages,
|
|
2793
|
+
schemas: buildState.schemas,
|
|
2794
|
+
submissionSchemas: buildState.submissionSchemas,
|
|
2795
|
+
siteConfig: buildState.siteConfig,
|
|
2796
|
+
}));
|
|
2797
|
+
return true;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
|
|
2801
|
+
if (pageManifestMatch && req.method === 'GET') {
|
|
2802
|
+
const slug = normalizeManifestSlug(pageManifestMatch[1]);
|
|
2803
|
+
const resolved = core.resolvePublicPageDocument({
|
|
2804
|
+
slug,
|
|
2805
|
+
pages: buildState.pages,
|
|
2806
|
+
siteConfig: buildState.siteConfig,
|
|
2807
|
+
themeConfig: buildState.themeConfig,
|
|
2808
|
+
menuConfig: buildState.menuConfig,
|
|
2809
|
+
collections: buildState.collections,
|
|
2810
|
+
collectionSchemas: buildState.collectionSchemas,
|
|
2811
|
+
refDocuments: buildState.refDocuments,
|
|
2812
|
+
});
|
|
2813
|
+
if (!resolved) {
|
|
2814
|
+
sendJson(res, 404, { error: 'Page manifest not found' });
|
|
2815
|
+
return true;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
sendJson(res, 200, buildPageManifest({
|
|
2819
|
+
slug,
|
|
2820
|
+
pageConfig: resolved.page,
|
|
2821
|
+
schemas: buildState.schemas,
|
|
2822
|
+
submissionSchemas: buildState.submissionSchemas,
|
|
2823
|
+
siteConfig: buildState.siteConfig,
|
|
2824
|
+
}));
|
|
2825
|
+
return true;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
|
|
2829
|
+
if (schemaMatch && req.method === 'GET') {
|
|
2830
|
+
const slug = normalizeManifestSlug(schemaMatch[1]);
|
|
2831
|
+
const resolved = core.resolvePublicPageDocument({
|
|
2832
|
+
slug,
|
|
2833
|
+
pages: buildState.pages,
|
|
2834
|
+
siteConfig: buildState.siteConfig,
|
|
2835
|
+
themeConfig: buildState.themeConfig,
|
|
2836
|
+
menuConfig: buildState.menuConfig,
|
|
2837
|
+
collections: buildState.collections,
|
|
2838
|
+
collectionSchemas: buildState.collectionSchemas,
|
|
2839
|
+
refDocuments: buildState.refDocuments,
|
|
2840
|
+
});
|
|
2841
|
+
if (!resolved) {
|
|
2842
|
+
sendJson(res, 404, { error: 'Schema contract not found' });
|
|
2843
|
+
return true;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
sendJson(res, 200, buildPageContract({
|
|
2847
|
+
slug,
|
|
2848
|
+
pageConfig: resolved.page,
|
|
2849
|
+
schemas: buildState.schemas,
|
|
2850
|
+
submissionSchemas: buildState.submissionSchemas,
|
|
2851
|
+
siteConfig: buildState.siteConfig,
|
|
2852
|
+
}));
|
|
2853
|
+
return true;
|
|
2854
|
+
}
|
|
2855
|
+
return false;
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
if (
|
|
2859
|
+
req.method === 'GET' &&
|
|
2860
|
+
(
|
|
2861
|
+
pathname === '/mcp-manifest.json'
|
|
2862
|
+
|| pathname === '/llms.txt'
|
|
2863
|
+
|| /^\/mcp-manifests\/.+\.json$/i.test(pathname)
|
|
2864
|
+
|| /^\/schemas\/.+\.schema\.json$/i.test(pathname)
|
|
2865
|
+
)
|
|
2866
|
+
) {
|
|
2867
|
+
void handleManifestRequest()
|
|
2868
|
+
.then((handled) => {
|
|
2869
|
+
if (!handled) {
|
|
2870
|
+
// Avoid Vite SPA fallback to index.html for known manifest URLs.
|
|
2871
|
+
sendJson(res, 404, { error: 'Manifest or schema not found' });
|
|
2872
|
+
}
|
|
2873
|
+
})
|
|
2874
|
+
.catch((error) => {
|
|
2875
|
+
sendJson(res, 500, { error: error?.message || 'Manifest generation failed' });
|
|
2876
|
+
});
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
if (isPageJsonRequest) {
|
|
2881
|
+
const normalizedPath = decodeURIComponent(pathname).replace(/\\/g, '/');
|
|
2882
|
+
// Optional "/pages/" prefix mirrors production public paths.
|
|
2883
|
+
const slug = normalizedPath
|
|
2884
|
+
.replace(/^\/+/, '')
|
|
2885
|
+
.replace(/^pages\//i, '')
|
|
2886
|
+
.replace(/\.json$/i, '')
|
|
2887
|
+
.replace(/^\/+|\/+$/g, '');
|
|
2888
|
+
if (!slug) {
|
|
2889
|
+
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
void (async () => {
|
|
2893
|
+
const core = await loadWebMcpBuilders();
|
|
2894
|
+
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
2895
|
+
const buildState = runtime.getWebMcpBuildState();
|
|
2896
|
+
const resolved = core.resolvePublicPageDocument({
|
|
2897
|
+
slug,
|
|
2898
|
+
pages: buildState.pages,
|
|
2899
|
+
siteConfig: buildState.siteConfig,
|
|
2900
|
+
themeConfig: buildState.themeConfig,
|
|
2901
|
+
menuConfig: buildState.menuConfig,
|
|
2902
|
+
collections: buildState.collections,
|
|
2903
|
+
collectionSchemas: buildState.collectionSchemas,
|
|
2904
|
+
refDocuments: buildState.refDocuments,
|
|
2905
|
+
});
|
|
2906
|
+
if (!resolved) {
|
|
2907
|
+
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const authored = buildState.pages[resolved.pageMatch.registrySlug];
|
|
2911
|
+
sendJson(res, 200, applyDevSliceFilters(resolved.page, authored, resolved.pageMatch.params ?? {}));
|
|
2912
|
+
})().catch((error) => {
|
|
2913
|
+
sendJson(res, 500, { error: error?.message || 'Page JSON resolution failed' });
|
|
2914
|
+
});
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
if (req.method === 'GET' && req.url === '/api/list-assets') {
|
|
2918
|
+
try {
|
|
2919
|
+
sendJson(res, 200, listImagesInDir(ASSETS_IMAGES_DIR, '/assets/images'));
|
|
2920
|
+
} catch (e) {
|
|
2921
|
+
sendJson(res, 500, { error: (e as Error)?.message || 'List failed' });
|
|
2922
|
+
}
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (req.method === 'POST' && pathname === '/api/save-to-file') {
|
|
2926
|
+
const chunks: Buffer[] = [];
|
|
2927
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
2928
|
+
req.on('end', () => {
|
|
2929
|
+
try {
|
|
2930
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
2931
|
+
if (!raw.trim()) {
|
|
2932
|
+
sendJson(res, 400, { error: 'Empty body' });
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
const body = JSON.parse(raw);
|
|
2936
|
+
const { projectState, slug } = body;
|
|
2937
|
+
if (!projectState || typeof slug !== 'string') {
|
|
2938
|
+
sendJson(res, 400, { error: 'Missing projectState or slug' });
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
|
|
2942
|
+
if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
|
|
2943
|
+
if (!fs.existsSync(DATA_COLLECTIONS_DIR)) fs.mkdirSync(DATA_COLLECTIONS_DIR, { recursive: true });
|
|
2944
|
+
if (projectState.site != null) {
|
|
2945
|
+
fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
2946
|
+
}
|
|
2947
|
+
if (projectState.menu != null) {
|
|
2948
|
+
fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
2949
|
+
}
|
|
2950
|
+
if (projectState.theme != null) {
|
|
2951
|
+
fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
2952
|
+
}
|
|
2953
|
+
if (projectState.page != null) {
|
|
2954
|
+
const pagePath = safeDataSlugPath(DATA_PAGES_DIR, slug, 'page');
|
|
2955
|
+
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
2956
|
+
fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
2957
|
+
}
|
|
2958
|
+
if (projectState.collections && typeof projectState.collections === 'object') {
|
|
2959
|
+
for (const [source, collection] of Object.entries(projectState.collections)) {
|
|
2960
|
+
const sourceSlug = String(source).replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
2961
|
+
if (!sourceSlug) continue;
|
|
2962
|
+
const collectionDir = path.join(DATA_COLLECTIONS_DIR, sourceSlug);
|
|
2963
|
+
fs.mkdirSync(collectionDir, { recursive: true });
|
|
2964
|
+
fs.writeFileSync(path.join(collectionDir, `${sourceSlug}.json`), JSON.stringify(collection, null, 2), 'utf8');
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
sendJson(res, 200, { ok: true });
|
|
2968
|
+
} catch (e) {
|
|
2969
|
+
sendJson(res, 500, { error: (e as Error)?.message || 'Save to file failed' });
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
req.on('error', () => sendJson(res, 500, { error: 'Request error' }));
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
if (req.method !== 'POST' || pathname !== '/api/upload-asset') return next();
|
|
2976
|
+
const chunks: Buffer[] = [];
|
|
2977
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
2978
|
+
req.on('end', () => {
|
|
2979
|
+
try {
|
|
2980
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
2981
|
+
const { filename, mimeType, data } = body;
|
|
2982
|
+
if (!filename || typeof data !== 'string') {
|
|
2983
|
+
sendJson(res, 400, { error: 'Missing filename or data' });
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
const buf = Buffer.from(data, 'base64');
|
|
2987
|
+
if (buf.length > MAX_FILE_SIZE_BYTES) {
|
|
2988
|
+
sendJson(res, 413, { error: 'File too large. Max 5MB.' });
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
if (mimeType && !IMAGE_MIMES.has(mimeType)) {
|
|
2992
|
+
sendJson(res, 400, { error: 'Invalid file type' });
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
const name = safeFilename(filename, mimeType);
|
|
2996
|
+
if (!fs.existsSync(ASSETS_IMAGES_DIR)) fs.mkdirSync(ASSETS_IMAGES_DIR, { recursive: true });
|
|
2997
|
+
fs.writeFileSync(path.join(ASSETS_IMAGES_DIR, name), buf);
|
|
2998
|
+
sendJson(res, 200, { url: `/assets/images/${name}` });
|
|
2999
|
+
} catch (e) {
|
|
3000
|
+
sendJson(res, 500, { error: (e as Error)?.message || 'Upload failed' });
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
req.on('error', () => sendJson(res, 500, { error: 'Request error' }));
|
|
3004
|
+
});
|
|
3005
|
+
},
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2644
3009
|
END_OF_FILE_CONTENT
|
|
2645
3010
|
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
2646
3011
|
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
@@ -2951,33 +3316,45 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
2951
3316
|
* Thin Entry Point (Tenant).
|
|
2952
3317
|
* Bootstrap, persistence, and engine wiring — logic lives in lib/hooks.
|
|
2953
3318
|
*/
|
|
2954
|
-
import { useEffect, useMemo } from 'react';
|
|
2955
|
-
import { JsonPagesEngine, OlonFormsContext, createHotSaveHandler } from '@olonjs/react';
|
|
3319
|
+
import { lazy, Suspense, useEffect, useMemo } from 'react';
|
|
2956
3320
|
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
2957
3321
|
import { withBasePath } from '@olonjs/core';
|
|
2958
|
-
import {
|
|
3322
|
+
import { JsonPagesEngine, OlonFormsContext, createHotSaveHandler } from '@olonjs/react';
|
|
3323
|
+
|
|
3324
|
+
import type { MenuConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
3325
|
+
|
|
3326
|
+
// Protocol (tenant-authored)
|
|
3327
|
+
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
2959
3328
|
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
3329
|
+
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
3330
|
+
import { iconMap } from '@/lib/IconResolver';
|
|
2960
3331
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
2961
|
-
|
|
2962
|
-
|
|
3332
|
+
|
|
3333
|
+
// Data seeds + local loaders
|
|
2963
3334
|
import siteData from '@/data/config/site.json';
|
|
2964
3335
|
import themeData from '@/data/config/theme.json';
|
|
2965
3336
|
import menuData from '@/data/config/menu.json';
|
|
2966
3337
|
import { getFileCollections } from '@/lib/loaders/getFileCollections';
|
|
2967
3338
|
import { getFilePages } from '@/lib/loaders/getFilePages';
|
|
2968
|
-
|
|
3339
|
+
|
|
3340
|
+
// Shell UI
|
|
2969
3341
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
2970
3342
|
import { TenantBootstrapChrome } from '@/components/TenantBootstrapChrome';
|
|
2971
3343
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
2972
|
-
|
|
2973
|
-
|
|
3344
|
+
|
|
3345
|
+
// Harness
|
|
2974
3346
|
import { uploadTenantAsset } from '@/lib/assets/assetUpload';
|
|
3347
|
+
import { useAssetsManifest } from '@/lib/assets/useAssetsManifest';
|
|
3348
|
+
import { useTenantBootstrap } from '@/lib/bootstrap/useTenantBootstrap';
|
|
2975
3349
|
import {
|
|
2976
3350
|
cloudFingerprintFromUrl,
|
|
2977
3351
|
normalizeSlugForCache,
|
|
2978
3352
|
readCachedCloudContent,
|
|
2979
3353
|
writeCachedCloudContent,
|
|
2980
3354
|
} from '@/lib/cloud/cloudCache';
|
|
3355
|
+
import { hydrateLocalProjectState } from '@/lib/cloud/hydrateLocalProjectState';
|
|
3356
|
+
import { useAdminStudioContent } from '@/lib/cloud/useAdminStudioContent';
|
|
3357
|
+
import { useCloudSave } from '@/lib/cloud/useCloudSave';
|
|
2981
3358
|
import {
|
|
2982
3359
|
extractLeadingRemoteCssImports,
|
|
2983
3360
|
setTenantPreviewReady,
|
|
@@ -2986,14 +3363,15 @@ import {
|
|
|
2986
3363
|
warnIfThemeFontsMissing,
|
|
2987
3364
|
} from '@/lib/css/tenantCss';
|
|
2988
3365
|
import { APP_BASE_PATH, TENANT_ID, cloudPolicy } from '@/lib/env/tenantEnv';
|
|
2989
|
-
import {
|
|
2990
|
-
import { useCloudSave } from '@/lib/cloud/useCloudSave';
|
|
2991
|
-
import { useTenantBootstrap } from '@/lib/bootstrap/useTenantBootstrap';
|
|
2992
|
-
import { useAdminStudioContent } from '@/lib/cloud/useAdminStudioContent';
|
|
2993
|
-
import { hydrateLocalProjectState } from '@/lib/cloud/hydrateLocalProjectState';
|
|
3366
|
+
import { useOlonForms } from '@/lib/forms/useOlonForms';
|
|
2994
3367
|
|
|
2995
3368
|
import tenantCss from './index.css?inline';
|
|
2996
3369
|
|
|
3370
|
+
/** Cold-save UI — admin/Save2Repo only; keep out of the visitor main chunk. */
|
|
3371
|
+
const DopaDrawer = lazy(() =>
|
|
3372
|
+
import('@/components/save-drawer/DopaDrawer').then((m) => ({ default: m.DopaDrawer })),
|
|
3373
|
+
);
|
|
3374
|
+
|
|
2997
3375
|
const themeConfigSeed = themeData as unknown as ThemeConfig;
|
|
2998
3376
|
const menuConfigSeed = menuData as unknown as MenuConfig;
|
|
2999
3377
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
@@ -3019,6 +3397,7 @@ function App() {
|
|
|
3019
3397
|
});
|
|
3020
3398
|
const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(cloudPolicy.isCloudMode);
|
|
3021
3399
|
const { cloudSaveUi, runCloudSave, closeCloudDrawer, retryCloudSave } = useCloudSave();
|
|
3400
|
+
const mountDopaDrawer = cloudSaveUi.isOpen || cloudSaveUi.phase !== 'idle';
|
|
3022
3401
|
|
|
3023
3402
|
const hotSave = useMemo(
|
|
3024
3403
|
() =>
|
|
@@ -3174,17 +3553,21 @@ function App() {
|
|
|
3174
3553
|
<JsonPagesEngine config={config} />
|
|
3175
3554
|
)
|
|
3176
3555
|
) : null}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3556
|
+
{mountDopaDrawer ? (
|
|
3557
|
+
<Suspense fallback={null}>
|
|
3558
|
+
<DopaDrawer
|
|
3559
|
+
isOpen={cloudSaveUi.isOpen}
|
|
3560
|
+
phase={cloudSaveUi.phase}
|
|
3561
|
+
currentStepId={cloudSaveUi.currentStepId}
|
|
3562
|
+
doneSteps={cloudSaveUi.doneSteps}
|
|
3563
|
+
progress={cloudSaveUi.progress}
|
|
3564
|
+
errorMessage={cloudSaveUi.errorMessage}
|
|
3565
|
+
deployUrl={cloudSaveUi.deployUrl}
|
|
3566
|
+
onClose={closeCloudDrawer}
|
|
3567
|
+
onRetry={retryCloudSave}
|
|
3568
|
+
/>
|
|
3569
|
+
</Suspense>
|
|
3570
|
+
) : null}
|
|
3188
3571
|
</OlonFormsContext.Provider>
|
|
3189
3572
|
</ThemeProvider>
|
|
3190
3573
|
);
|
|
@@ -14291,7 +14674,7 @@ cat << 'END_OF_FILE_CONTENT' > "tsconfig.node.json"
|
|
|
14291
14674
|
"allowSyntheticDefaultImports": true,
|
|
14292
14675
|
"strict": true
|
|
14293
14676
|
},
|
|
14294
|
-
"include": ["vite.config.ts"]
|
|
14677
|
+
"include": ["vite.config.ts", "scripts/vite/**/*.ts"]
|
|
14295
14678
|
}
|
|
14296
14679
|
|
|
14297
14680
|
|
|
@@ -14323,331 +14706,23 @@ END_OF_FILE_CONTENT
|
|
|
14323
14706
|
echo "Creating vite.config.ts..."
|
|
14324
14707
|
cat << 'END_OF_FILE_CONTENT' > "vite.config.ts"
|
|
14325
14708
|
/**
|
|
14326
|
-
* Generated by @
|
|
14709
|
+
* Generated by @olonjs/cli. Dev APIs live in scripts/vite/tenantDevApiPlugin.ts
|
|
14710
|
+
* (/api/save-to-file, /api/upload-asset, /api/list-assets, WebMCP manifests).
|
|
14327
14711
|
*/
|
|
14328
14712
|
import { defineConfig } from 'vite';
|
|
14329
14713
|
import react from '@vitejs/plugin-react';
|
|
14330
14714
|
import tailwindcss from '@tailwindcss/vite';
|
|
14331
14715
|
import path from 'path';
|
|
14332
|
-
import fs from 'fs';
|
|
14333
14716
|
import { fileURLToPath } from 'url';
|
|
14717
|
+
import { tenantDevApiPlugin } from './scripts/vite/tenantDevApiPlugin';
|
|
14334
14718
|
|
|
14335
14719
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14336
14720
|
|
|
14337
|
-
const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
|
|
14338
|
-
const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
|
|
14339
|
-
const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
|
|
14340
|
-
const DATA_COLLECTIONS_DIR = path.resolve(__dirname, 'src', 'data', 'collections');
|
|
14341
|
-
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
14342
|
-
const IMAGE_MIMES = new Set([
|
|
14343
|
-
'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
|
|
14344
|
-
]);
|
|
14345
|
-
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
|
|
14346
|
-
|
|
14347
|
-
function safeFilename(original, mimeType) {
|
|
14348
|
-
const base = (original.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9-_]/g, '_').slice(0, 128)) || 'image';
|
|
14349
|
-
const ext = original.includes('.') ? path.extname(original).toLowerCase() : (mimeType?.startsWith('image/') ? `.${(mimeType.split('/')[1] || 'png').replace('jpeg', 'jpg')}` : '.png');
|
|
14350
|
-
return `${Date.now()}-${base}${IMAGE_EXT.has(ext) ? ext : '.png'}`;
|
|
14351
|
-
}
|
|
14352
|
-
|
|
14353
|
-
function listImagesInDir(dir, urlPrefix) {
|
|
14354
|
-
const list = [];
|
|
14355
|
-
if (!fs.existsSync(dir)) return list;
|
|
14356
|
-
for (const name of fs.readdirSync(dir)) {
|
|
14357
|
-
if (IMAGE_EXT.has(path.extname(name).toLowerCase())) list.push({ id: name, url: `${urlPrefix}/${name}`, alt: name, tags: [] });
|
|
14358
|
-
}
|
|
14359
|
-
return list;
|
|
14360
|
-
}
|
|
14361
|
-
|
|
14362
|
-
function sendJson(res, status, body) {
|
|
14363
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
14364
|
-
res.end(JSON.stringify(body));
|
|
14365
|
-
}
|
|
14366
|
-
|
|
14367
|
-
function sendJsonFile(res, filePath) {
|
|
14368
|
-
try {
|
|
14369
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
14370
|
-
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
14371
|
-
res.end(content);
|
|
14372
|
-
} catch (e) {
|
|
14373
|
-
sendJson(res, 500, { error: e?.message || 'Read failed' });
|
|
14374
|
-
}
|
|
14375
|
-
}
|
|
14376
|
-
|
|
14377
|
-
function safeDataSlugPath(rootDir, rawSlug, fallback) {
|
|
14378
|
-
const slug = String(rawSlug || fallback)
|
|
14379
|
-
.replace(/\\/g, '/')
|
|
14380
|
-
.replace(/^\/+|\/+$/g, '');
|
|
14381
|
-
const segments = slug
|
|
14382
|
-
.split('/')
|
|
14383
|
-
.map((segment) => segment.replace(/[^a-zA-Z0-9_[\]-]/g, '_'))
|
|
14384
|
-
.filter(Boolean);
|
|
14385
|
-
const candidate = path.resolve(rootDir, `${segments.join(path.sep) || fallback}.json`);
|
|
14386
|
-
const isInsideRoot = candidate.startsWith(`${rootDir}${path.sep}`) || candidate === rootDir;
|
|
14387
|
-
if (!isInsideRoot) throw new Error('Invalid data path');
|
|
14388
|
-
return candidate;
|
|
14389
|
-
}
|
|
14390
|
-
|
|
14391
|
-
function isTenantPageJsonRequest(req, pathname) {
|
|
14392
|
-
if (req.method !== 'GET' || !pathname.endsWith('.json')) return false;
|
|
14393
|
-
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
14394
|
-
return !viteOrStaticPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
14395
|
-
}
|
|
14396
|
-
|
|
14397
|
-
function normalizeManifestSlug(raw) {
|
|
14398
|
-
return decodeURIComponent(raw || '')
|
|
14399
|
-
.replace(/^\/+|\/+$/g, '')
|
|
14400
|
-
.replace(/\\/g, '/')
|
|
14401
|
-
.replace(/(\.schema)?\.json$/i, '');
|
|
14402
|
-
}
|
|
14403
|
-
|
|
14404
|
-
function applyDevSliceFilters(page, authored, params) {
|
|
14405
|
-
if (!authored?.sections || !page?.sections) return page;
|
|
14406
|
-
const at = (o, p) => p.split('.').reduce((a, k) => a?.[k], o);
|
|
14407
|
-
return {
|
|
14408
|
-
...page,
|
|
14409
|
-
sections: page.sections.map((section, i) => {
|
|
14410
|
-
const src = authored.sections[i]?.data;
|
|
14411
|
-
if (!src || !section.data) return section;
|
|
14412
|
-
const data = { ...section.data };
|
|
14413
|
-
for (const [key, ref] of Object.entries(src)) {
|
|
14414
|
-
if (!ref?.$sliceFilter || typeof data[key] !== 'object') continue;
|
|
14415
|
-
const filter = Object.fromEntries(
|
|
14416
|
-
Object.entries(ref.$sliceFilter)
|
|
14417
|
-
.map(([k, v]) => [k, typeof v === 'string' ? v : params[v?.$routeParam] ?? ''])
|
|
14418
|
-
.filter(([, v]) => v),
|
|
14419
|
-
);
|
|
14420
|
-
data[key] = Object.fromEntries(
|
|
14421
|
-
Object.entries(data[key]).filter(([, item]) =>
|
|
14422
|
-
Object.entries(filter).every(([p, v]) => String(at(item, p) ?? '') === v),
|
|
14423
|
-
),
|
|
14424
|
-
);
|
|
14425
|
-
}
|
|
14426
|
-
return { ...section, data };
|
|
14427
|
-
}),
|
|
14428
|
-
};
|
|
14429
|
-
}
|
|
14430
|
-
|
|
14431
|
-
async function loadWebMcpBuilders() {
|
|
14432
|
-
const moduleUrl = import.meta.resolve('@olonjs/core');
|
|
14433
|
-
return import(moduleUrl);
|
|
14434
|
-
}
|
|
14435
14721
|
export default defineConfig({
|
|
14436
14722
|
plugins: [
|
|
14437
14723
|
react(),
|
|
14438
14724
|
tailwindcss(),
|
|
14439
|
-
{
|
|
14440
|
-
name: 'upload-asset-api',
|
|
14441
|
-
configureServer(server) {
|
|
14442
|
-
server.middlewares.use((req, res, next) => {
|
|
14443
|
-
const pathname = (req.url || '').split('?')[0];
|
|
14444
|
-
const isPageJsonRequest = isTenantPageJsonRequest(req, pathname);
|
|
14445
|
-
|
|
14446
|
-
const handleManifestRequest = async () => {
|
|
14447
|
-
const core = await loadWebMcpBuilders();
|
|
14448
|
-
const { buildPageContract, buildPageManifest, buildSiteManifest, buildLlmsTxt } = core.webmcp;
|
|
14449
|
-
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
14450
|
-
const buildState = runtime.getWebMcpBuildState();
|
|
14451
|
-
|
|
14452
|
-
if (req.method === 'GET' && pathname === '/llms.txt') {
|
|
14453
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
14454
|
-
res.end(buildLlmsTxt({
|
|
14455
|
-
pages: buildState.pages,
|
|
14456
|
-
schemas: buildState.schemas,
|
|
14457
|
-
siteConfig: buildState.siteConfig,
|
|
14458
|
-
}));
|
|
14459
|
-
return true;
|
|
14460
|
-
}
|
|
14461
|
-
|
|
14462
|
-
if (req.method === 'GET' && pathname === '/mcp-manifest.json') {
|
|
14463
|
-
sendJson(res, 200, buildSiteManifest({
|
|
14464
|
-
pages: buildState.pages,
|
|
14465
|
-
schemas: buildState.schemas,
|
|
14466
|
-
submissionSchemas: buildState.submissionSchemas,
|
|
14467
|
-
siteConfig: buildState.siteConfig,
|
|
14468
|
-
}));
|
|
14469
|
-
return true;
|
|
14470
|
-
}
|
|
14471
|
-
|
|
14472
|
-
const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
|
|
14473
|
-
if (pageManifestMatch && req.method === 'GET') {
|
|
14474
|
-
const slug = normalizeManifestSlug(pageManifestMatch[1]);
|
|
14475
|
-
const resolved = core.resolvePublicPageDocument({
|
|
14476
|
-
slug,
|
|
14477
|
-
pages: buildState.pages,
|
|
14478
|
-
siteConfig: buildState.siteConfig,
|
|
14479
|
-
themeConfig: buildState.themeConfig,
|
|
14480
|
-
menuConfig: buildState.menuConfig,
|
|
14481
|
-
collections: buildState.collections,
|
|
14482
|
-
collectionSchemas: buildState.collectionSchemas,
|
|
14483
|
-
refDocuments: buildState.refDocuments,
|
|
14484
|
-
});
|
|
14485
|
-
if (!resolved) {
|
|
14486
|
-
sendJson(res, 404, { error: 'Page manifest not found' });
|
|
14487
|
-
return true;
|
|
14488
|
-
}
|
|
14489
|
-
|
|
14490
|
-
sendJson(res, 200, buildPageManifest({
|
|
14491
|
-
slug,
|
|
14492
|
-
pageConfig: resolved.page,
|
|
14493
|
-
schemas: buildState.schemas,
|
|
14494
|
-
submissionSchemas: buildState.submissionSchemas,
|
|
14495
|
-
siteConfig: buildState.siteConfig,
|
|
14496
|
-
}));
|
|
14497
|
-
return true;
|
|
14498
|
-
}
|
|
14499
|
-
|
|
14500
|
-
const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
|
|
14501
|
-
if (schemaMatch && req.method === 'GET') {
|
|
14502
|
-
const slug = normalizeManifestSlug(schemaMatch[1]);
|
|
14503
|
-
const resolved = core.resolvePublicPageDocument({
|
|
14504
|
-
slug,
|
|
14505
|
-
pages: buildState.pages,
|
|
14506
|
-
siteConfig: buildState.siteConfig,
|
|
14507
|
-
themeConfig: buildState.themeConfig,
|
|
14508
|
-
menuConfig: buildState.menuConfig,
|
|
14509
|
-
collections: buildState.collections,
|
|
14510
|
-
collectionSchemas: buildState.collectionSchemas,
|
|
14511
|
-
refDocuments: buildState.refDocuments,
|
|
14512
|
-
});
|
|
14513
|
-
if (!resolved) {
|
|
14514
|
-
sendJson(res, 404, { error: 'Schema contract not found' });
|
|
14515
|
-
return true;
|
|
14516
|
-
}
|
|
14517
|
-
|
|
14518
|
-
sendJson(res, 200, buildPageContract({
|
|
14519
|
-
slug,
|
|
14520
|
-
pageConfig: resolved.page,
|
|
14521
|
-
schemas: buildState.schemas,
|
|
14522
|
-
submissionSchemas: buildState.submissionSchemas,
|
|
14523
|
-
siteConfig: buildState.siteConfig,
|
|
14524
|
-
}));
|
|
14525
|
-
return true;
|
|
14526
|
-
}
|
|
14527
|
-
return false;
|
|
14528
|
-
};
|
|
14529
|
-
|
|
14530
|
-
if (
|
|
14531
|
-
req.method === 'GET' &&
|
|
14532
|
-
(
|
|
14533
|
-
pathname === '/mcp-manifest.json'
|
|
14534
|
-
|| pathname === '/llms.txt'
|
|
14535
|
-
|| /^\/mcp-manifests\/.+\.json$/i.test(pathname)
|
|
14536
|
-
|| /^\/schemas\/.+\.schema\.json$/i.test(pathname)
|
|
14537
|
-
)
|
|
14538
|
-
) {
|
|
14539
|
-
void handleManifestRequest()
|
|
14540
|
-
.then((handled) => {
|
|
14541
|
-
if (!handled) {
|
|
14542
|
-
// Se handleManifestRequest fallisce a trovare qualcosa ma l'URL era giusto,
|
|
14543
|
-
// forziamo un 404 JSON per evitare che Vite serva l'index.html
|
|
14544
|
-
sendJson(res, 404, { error: 'Manifest or schema not found' });
|
|
14545
|
-
}
|
|
14546
|
-
})
|
|
14547
|
-
.catch((error) => {
|
|
14548
|
-
sendJson(res, 500, { error: error?.message || 'Manifest generation failed' });
|
|
14549
|
-
});
|
|
14550
|
-
return;
|
|
14551
|
-
}
|
|
14552
|
-
|
|
14553
|
-
if (isPageJsonRequest) {
|
|
14554
|
-
const normalizedPath = decodeURIComponent(pathname).replace(/\\/g, '/');
|
|
14555
|
-
// Rimuoviamo la root folder opzionale "/pages/" introdotta per matchare la prod e il file extension
|
|
14556
|
-
const slug = normalizedPath
|
|
14557
|
-
.replace(/^\/+/, '')
|
|
14558
|
-
.replace(/^pages\//i, '')
|
|
14559
|
-
.replace(/\.json$/i, '')
|
|
14560
|
-
.replace(/^\/+|\/+$/g, '');
|
|
14561
|
-
if (!slug) {
|
|
14562
|
-
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
14563
|
-
return;
|
|
14564
|
-
}
|
|
14565
|
-
void (async () => {
|
|
14566
|
-
const core = await loadWebMcpBuilders();
|
|
14567
|
-
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
14568
|
-
const buildState = runtime.getWebMcpBuildState();
|
|
14569
|
-
const resolved = core.resolvePublicPageDocument({
|
|
14570
|
-
slug,
|
|
14571
|
-
pages: buildState.pages,
|
|
14572
|
-
siteConfig: buildState.siteConfig,
|
|
14573
|
-
themeConfig: buildState.themeConfig,
|
|
14574
|
-
menuConfig: buildState.menuConfig,
|
|
14575
|
-
collections: buildState.collections,
|
|
14576
|
-
collectionSchemas: buildState.collectionSchemas,
|
|
14577
|
-
refDocuments: buildState.refDocuments,
|
|
14578
|
-
});
|
|
14579
|
-
if (!resolved) {
|
|
14580
|
-
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
14581
|
-
return;
|
|
14582
|
-
}
|
|
14583
|
-
const authored = buildState.pages[resolved.pageMatch.registrySlug];
|
|
14584
|
-
sendJson(res, 200, applyDevSliceFilters(resolved.page, authored, resolved.pageMatch.params ?? {}));
|
|
14585
|
-
})().catch((error) => {
|
|
14586
|
-
sendJson(res, 500, { error: error?.message || 'Page JSON resolution failed' });
|
|
14587
|
-
});
|
|
14588
|
-
return;
|
|
14589
|
-
}
|
|
14590
|
-
if (req.method === 'GET' && req.url === '/api/list-assets') {
|
|
14591
|
-
try { sendJson(res, 200, listImagesInDir(ASSETS_IMAGES_DIR, '/assets/images')); } catch (e) { sendJson(res, 500, { error: e?.message || 'List failed' }); }
|
|
14592
|
-
return;
|
|
14593
|
-
}
|
|
14594
|
-
if (req.method === 'POST' && pathname === '/api/save-to-file') {
|
|
14595
|
-
const chunks = [];
|
|
14596
|
-
req.on('data', (chunk) => chunks.push(chunk));
|
|
14597
|
-
req.on('end', () => {
|
|
14598
|
-
try {
|
|
14599
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
14600
|
-
if (!raw.trim()) { sendJson(res, 400, { error: 'Empty body' }); return; }
|
|
14601
|
-
const body = JSON.parse(raw);
|
|
14602
|
-
const { projectState, slug } = body;
|
|
14603
|
-
if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
|
|
14604
|
-
if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
|
|
14605
|
-
if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
|
|
14606
|
-
if (!fs.existsSync(DATA_COLLECTIONS_DIR)) fs.mkdirSync(DATA_COLLECTIONS_DIR, { recursive: true });
|
|
14607
|
-
if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
14608
|
-
if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
14609
|
-
if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
14610
|
-
if (projectState.page != null) {
|
|
14611
|
-
const pagePath = safeDataSlugPath(DATA_PAGES_DIR, slug, 'page');
|
|
14612
|
-
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
14613
|
-
fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
14614
|
-
}
|
|
14615
|
-
if (projectState.collections && typeof projectState.collections === 'object') {
|
|
14616
|
-
for (const [source, collection] of Object.entries(projectState.collections)) {
|
|
14617
|
-
const sourceSlug = String(source).replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
14618
|
-
if (!sourceSlug) continue;
|
|
14619
|
-
const collectionDir = path.join(DATA_COLLECTIONS_DIR, sourceSlug);
|
|
14620
|
-
fs.mkdirSync(collectionDir, { recursive: true });
|
|
14621
|
-
fs.writeFileSync(path.join(collectionDir, `${sourceSlug}.json`), JSON.stringify(collection, null, 2), 'utf8');
|
|
14622
|
-
}
|
|
14623
|
-
}
|
|
14624
|
-
sendJson(res, 200, { ok: true });
|
|
14625
|
-
} catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }
|
|
14626
|
-
});
|
|
14627
|
-
req.on('error', () => sendJson(res, 500, { error: 'Request error' }));
|
|
14628
|
-
return;
|
|
14629
|
-
}
|
|
14630
|
-
if (req.method !== 'POST' || pathname !== '/api/upload-asset') return next();
|
|
14631
|
-
const chunks = [];
|
|
14632
|
-
req.on('data', (chunk) => chunks.push(chunk));
|
|
14633
|
-
req.on('end', () => {
|
|
14634
|
-
try {
|
|
14635
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
14636
|
-
const { filename, mimeType, data } = body;
|
|
14637
|
-
if (!filename || typeof data !== 'string') { sendJson(res, 400, { error: 'Missing filename or data' }); return; }
|
|
14638
|
-
const buf = Buffer.from(data, 'base64');
|
|
14639
|
-
if (buf.length > MAX_FILE_SIZE_BYTES) { sendJson(res, 413, { error: 'File too large. Max 5MB.' }); return; }
|
|
14640
|
-
if (mimeType && !IMAGE_MIMES.has(mimeType)) { sendJson(res, 400, { error: 'Invalid file type' }); return; }
|
|
14641
|
-
const name = safeFilename(filename, mimeType);
|
|
14642
|
-
if (!fs.existsSync(ASSETS_IMAGES_DIR)) fs.mkdirSync(ASSETS_IMAGES_DIR, { recursive: true });
|
|
14643
|
-
fs.writeFileSync(path.join(ASSETS_IMAGES_DIR, name), buf);
|
|
14644
|
-
sendJson(res, 200, { url: `/assets/images/${name}` });
|
|
14645
|
-
} catch (e) { sendJson(res, 500, { error: e?.message || 'Upload failed' }); }
|
|
14646
|
-
});
|
|
14647
|
-
req.on('error', () => sendJson(res, 500, { error: 'Request error' }));
|
|
14648
|
-
});
|
|
14649
|
-
},
|
|
14650
|
-
},
|
|
14725
|
+
tenantDevApiPlugin({ rootDir: __dirname }),
|
|
14651
14726
|
],
|
|
14652
14727
|
resolve: {
|
|
14653
14728
|
alias: {
|
|
@@ -14668,5 +14743,4 @@ export default defineConfig({
|
|
|
14668
14743
|
},
|
|
14669
14744
|
});
|
|
14670
14745
|
|
|
14671
|
-
|
|
14672
14746
|
END_OF_FILE_CONTENT
|