@jsonpages/cli 3.0.28 → 3.0.29
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 +187 -0
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
30
30
|
/**
|
|
31
31
|
* Thin Entry Point (Tenant).
|
|
32
32
|
* Data from getHydratedData (file-backed or draft); assets from public/assets/images.
|
|
33
|
+
* Persistence: Export JSON (download) + optional Salva su repo (POST /api/persist → git push).
|
|
33
34
|
*/
|
|
34
35
|
import { useState, useEffect } from 'react';
|
|
35
36
|
import { JsonPagesEngine } from '@jsonpages/core';
|
|
@@ -40,6 +41,7 @@ import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
|
40
41
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
41
42
|
import type { JsonPagesConfig } from '@jsonpages/core';
|
|
42
43
|
import type { PageConfig, SiteConfig, ThemeConfig, MenuConfig } from '@/types';
|
|
44
|
+
import type { ProjectState } from '@jsonpages/core';
|
|
43
45
|
|
|
44
46
|
import siteData from '@/data/config/site.json';
|
|
45
47
|
import themeData from '@/data/config/theme.json';
|
|
@@ -84,6 +86,21 @@ function App() {
|
|
|
84
86
|
menuConfig,
|
|
85
87
|
themeCss: { tenant: tenantCss },
|
|
86
88
|
addSection: addSectionConfig,
|
|
89
|
+
persistence: {
|
|
90
|
+
async saveToRepo(state: ProjectState, slug: string) {
|
|
91
|
+
const res = await fetch('/api/persist', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ projectState: state, slug }),
|
|
95
|
+
});
|
|
96
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string; message?: string };
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
const msg = body.error ?? `Save failed: ${res.status}`;
|
|
99
|
+
alert(msg);
|
|
100
|
+
throw new Error(msg);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
87
104
|
assets: {
|
|
88
105
|
assetsBaseUrl: '/assets',
|
|
89
106
|
assetsManifest,
|
|
@@ -3411,6 +3428,176 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
3411
3428
|
|
|
3412
3429
|
END_OF_FILE_CONTENT
|
|
3413
3430
|
# SKIP: src/registry-types.ts è un file binario e non può essere convertito in testo.
|
|
3431
|
+
mkdir -p "src/server"
|
|
3432
|
+
echo "Creating src/server/persistToRepo.ts..."
|
|
3433
|
+
cat << 'END_OF_FILE_CONTENT' > "src/server/persistToRepo.ts"
|
|
3434
|
+
/**
|
|
3435
|
+
* Enterprise-grade persist: write JSON to disk and git add/commit/push.
|
|
3436
|
+
* Used by POST /api/persist. Credentials via env (GITHUB_TOKEN); no secrets in code.
|
|
3437
|
+
*/
|
|
3438
|
+
import fs from 'fs';
|
|
3439
|
+
import path from 'path';
|
|
3440
|
+
import { simpleGit, type SimpleGit } from 'simple-git';
|
|
3441
|
+
|
|
3442
|
+
/** Minimal ProjectState shape for validation (matches Core kernel). */
|
|
3443
|
+
export interface PersistBody {
|
|
3444
|
+
projectState: {
|
|
3445
|
+
page: unknown;
|
|
3446
|
+
site: unknown;
|
|
3447
|
+
menu: unknown;
|
|
3448
|
+
theme: unknown;
|
|
3449
|
+
};
|
|
3450
|
+
slug: string;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
const SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
3454
|
+
|
|
3455
|
+
function isNonEmptyObject(v: unknown): v is Record<string, unknown> {
|
|
3456
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
export function validatePersistBody(body: unknown): { ok: true; data: PersistBody } | { ok: false; status: number; error: string } {
|
|
3460
|
+
if (!isNonEmptyObject(body) || !isNonEmptyObject(body.projectState)) {
|
|
3461
|
+
return { ok: false, status: 400, error: 'Missing or invalid projectState' };
|
|
3462
|
+
}
|
|
3463
|
+
const { projectState, slug } = body as PersistBody;
|
|
3464
|
+
if (typeof slug !== 'string' || !SLUG_REGEX.test(slug)) {
|
|
3465
|
+
return { ok: false, status: 400, error: 'Invalid slug (use lowercase alphanumeric and hyphens)' };
|
|
3466
|
+
}
|
|
3467
|
+
if (
|
|
3468
|
+
projectState.page == null ||
|
|
3469
|
+
projectState.site == null ||
|
|
3470
|
+
projectState.menu == null ||
|
|
3471
|
+
projectState.theme == null
|
|
3472
|
+
) {
|
|
3473
|
+
return { ok: false, status: 400, error: 'projectState must include page, site, menu, theme' };
|
|
3474
|
+
}
|
|
3475
|
+
return { ok: true, data: { projectState, slug } };
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
export interface PersistOptions {
|
|
3479
|
+
/** Absolute path to data dir (e.g. .../src/data). */
|
|
3480
|
+
dataDir: string;
|
|
3481
|
+
/** Git working directory (repo root). Defaults to dataDir's app root. */
|
|
3482
|
+
gitDir: string;
|
|
3483
|
+
/** Optional: inject into origin for push (e.g. GITHUB_TOKEN). */
|
|
3484
|
+
token?: string;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
function writeFiles(data: PersistBody, dataDir: string): string[] {
|
|
3488
|
+
const configDir = path.join(dataDir, 'config');
|
|
3489
|
+
const pagesDir = path.join(dataDir, 'pages');
|
|
3490
|
+
const written: string[] = [];
|
|
3491
|
+
|
|
3492
|
+
[configDir, pagesDir].forEach((dir) => {
|
|
3493
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
3494
|
+
});
|
|
3495
|
+
|
|
3496
|
+
const { projectState, slug } = data;
|
|
3497
|
+
const sitePath = path.join(configDir, 'site.json');
|
|
3498
|
+
const themePath = path.join(configDir, 'theme.json');
|
|
3499
|
+
const menuPath = path.join(configDir, 'menu.json');
|
|
3500
|
+
const pagePath = path.join(pagesDir, `${slug}.json`);
|
|
3501
|
+
|
|
3502
|
+
fs.writeFileSync(sitePath, JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
3503
|
+
written.push(sitePath);
|
|
3504
|
+
fs.writeFileSync(themePath, JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
3505
|
+
written.push(themePath);
|
|
3506
|
+
fs.writeFileSync(menuPath, JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
3507
|
+
written.push(menuPath);
|
|
3508
|
+
fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
3509
|
+
written.push(pagePath);
|
|
3510
|
+
|
|
3511
|
+
return written;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
/** Inject token into HTTPS origin for push (no token stored in config). */
|
|
3515
|
+
function originWithToken(origin: string, token: string): string {
|
|
3516
|
+
if (origin.startsWith('https://')) {
|
|
3517
|
+
return origin.replace(/^https:\/\/([^@]*@)?/, `https://${token}@`);
|
|
3518
|
+
}
|
|
3519
|
+
return origin;
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
async function gitAddCommitPush(
|
|
3523
|
+
gitDir: string,
|
|
3524
|
+
relativePaths: string[],
|
|
3525
|
+
commitMessage: string,
|
|
3526
|
+
token?: string
|
|
3527
|
+
): Promise<{ success: true } | { success: false; error: string }> {
|
|
3528
|
+
const git: SimpleGit = simpleGit(gitDir);
|
|
3529
|
+
let originalOrigin: string | null = null;
|
|
3530
|
+
|
|
3531
|
+
try {
|
|
3532
|
+
const status = await git.status();
|
|
3533
|
+
if (status.isBare()) {
|
|
3534
|
+
return { success: false, error: 'Repository is bare; cannot commit' };
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
if (token) {
|
|
3538
|
+
const remotes = await git.getRemotes(true);
|
|
3539
|
+
const origin = remotes.origin?.refs?.fetch ?? remotes.origin?.refs?.push;
|
|
3540
|
+
if (origin) {
|
|
3541
|
+
originalOrigin = origin;
|
|
3542
|
+
const authUrl = originWithToken(origin, token);
|
|
3543
|
+
await git.raw(['remote', 'set-url', 'origin', authUrl]);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
await git.add(relativePaths);
|
|
3548
|
+
const commitResult = await git.commit(commitMessage);
|
|
3549
|
+
if (commitResult.commit === null && relativePaths.length > 0) {
|
|
3550
|
+
return { success: false, error: 'Nothing to commit (files unchanged or not in repo)' };
|
|
3551
|
+
}
|
|
3552
|
+
await git.push();
|
|
3553
|
+
return { success: true };
|
|
3554
|
+
} catch (e) {
|
|
3555
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3556
|
+
return { success: false, error: msg };
|
|
3557
|
+
} finally {
|
|
3558
|
+
if (originalOrigin) {
|
|
3559
|
+
try {
|
|
3560
|
+
await git.raw(['remote', 'set-url', 'origin', originalOrigin]);
|
|
3561
|
+
} catch {
|
|
3562
|
+
// best-effort restore; do not mask original error
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
/**
|
|
3569
|
+
* Write project state to disk and push to remote.
|
|
3570
|
+
* File paths are resolved from dataDir; git paths are relative to gitDir.
|
|
3571
|
+
*/
|
|
3572
|
+
export async function runPersist(
|
|
3573
|
+
data: PersistBody,
|
|
3574
|
+
options: PersistOptions
|
|
3575
|
+
): Promise<{ success: true; message: string } | { success: false; status: number; error: string }> {
|
|
3576
|
+
const { dataDir, gitDir, token } = options;
|
|
3577
|
+
|
|
3578
|
+
if (!fs.existsSync(dataDir)) {
|
|
3579
|
+
return { success: false, status: 500, error: 'Data directory does not exist' };
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
const written = writeFiles(data, dataDir);
|
|
3583
|
+
const relativePaths = written.map((p) => path.relative(gitDir, p)).filter((p) => !p.startsWith('..'));
|
|
3584
|
+
if (relativePaths.length === 0) {
|
|
3585
|
+
return { success: false, status: 500, error: 'Computed paths are outside git directory' };
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
const commitMessage = `jsonpages: persist ${data.slug} @ ${new Date().toISOString().slice(0, 19)}Z`;
|
|
3589
|
+
const result = await gitAddCommitPush(gitDir, relativePaths, commitMessage, token);
|
|
3590
|
+
|
|
3591
|
+
if (result.success) {
|
|
3592
|
+
return { success: true, message: 'Saved and pushed to remote' };
|
|
3593
|
+
}
|
|
3594
|
+
if (result.error.toLowerCase().includes('failed to push') || result.error.toLowerCase().includes('authentication')) {
|
|
3595
|
+
return { success: false, status: 502, error: result.error };
|
|
3596
|
+
}
|
|
3597
|
+
return { success: false, status: 500, error: result.error };
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
END_OF_FILE_CONTENT
|
|
3414
3601
|
echo "Creating src/types.ts..."
|
|
3415
3602
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
3416
3603
|
import type { MenuItem } from '@jsonpages/core';
|