"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateManifest = validateManifest;
const zod_1 = require("zod");
const pageSchema = zod_1.z.object({
path: zod_1.z.string().startsWith('/'),
file: zod_1.z.string(),
title: zod_1.z.string(),
editable: zod_1.z.boolean().optional(),
});
// CMS templates use flat format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
// This schema allows any string keys for collection templates
const cmsTemplatesSchema = zod_1.z.record(zod_1.z.string()).optional();
const manifestSchema = zod_1.z.object({
pages: zod_1.z.array(pageSchema),
cmsTemplates: cmsTemplatesSchema,
defaultHeadHtml: zod_1.z.string().optional(),
});
/**
* Validates a manifest.json file and returns errors or success message
*/
async function validateManifest(manifestJson) {
const errors = [];
const warnings = [];
// Try to parse JSON
let manifest;
try {
manifest = JSON.parse(manifestJson);
}
catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to parse JSON';
// Provide more specific diagnostics for common JSON errors
let diagnostics = '';
const trimmed = manifestJson.trim();
// Check for missing closing brace
if (!trimmed.endsWith('}')) {
diagnostics = `
Detected Issue: File does not end with a closing brace '}'
The manifest.json appears to be truncated or missing the final '}'.
Fix: Add a closing '}' at the end of the file.`;
}
// Check for missing opening brace
else if (!trimmed.startsWith('{')) {
diagnostics = `
Detected Issue: File does not start with an opening brace '{'
The manifest.json must be a JSON object starting with '{'.`;
}
// Check bracket balance
else {
const openBraces = (manifestJson.match(/{/g) || []).length;
const closeBraces = (manifestJson.match(/}/g) || []).length;
const openBrackets = (manifestJson.match(/\[/g) || []).length;
const closeBrackets = (manifestJson.match(/]/g) || []).length;
if (openBraces !== closeBraces) {
diagnostics = `
Detected Issue: Mismatched braces
Found ${openBraces} opening '{' but ${closeBraces} closing '}'
${openBraces > closeBraces ? `Missing ${openBraces - closeBraces} closing brace(s) '}'` : `Extra ${closeBraces - openBraces} closing brace(s) '}'`}`;
}
else if (openBrackets !== closeBrackets) {
diagnostics = `
Detected Issue: Mismatched brackets
Found ${openBrackets} opening '[' but ${closeBrackets} closing ']'
${openBrackets > closeBrackets ? `Missing ${openBrackets - closeBrackets} closing bracket(s) ']'` : `Extra ${closeBrackets - openBrackets} closing bracket(s) ']'`}`;
}
else {
// Generic guidance
diagnostics = `
Common JSON issues to check:
- Missing or extra commas between items
- Trailing comma after last item in arrays/objects
- Unquoted property names (must use "key" not key)
- Single quotes instead of double quotes
- Unescaped special characters in strings`;
}
}
return `INVALID JSON
Error: ${errorMessage}
${diagnostics}
Tip: Use a JSON validator (like jsonlint.com) to find the exact error location.`;
}
// Validate against schema
const result = manifestSchema.safeParse(manifest);
if (!result.success) {
for (const issue of result.error.issues) {
errors.push(`- ${issue.path.join('.')}: ${issue.message}`);
}
}
// Additional validation
const m = manifest;
// CRITICAL: Detect wrong "collections:" format (common AI mistake)
// AIs often use the wrong nested format instead of the flat cmsTemplates format
if (m.collections && !m.cmsTemplates) {
errors.push(`WRONG FORMAT: Use "cmsTemplates", not "collections"
This is a common mistake. The correct format is:
WRONG (what you have):
"collections": {
"posts": { "indexPath": "/blog", "indexFile": "...", "detailPath": "...", "detailFile": "..." }
}
CORRECT (what you need):
"cmsTemplates": {
"postsIndex": "templates/posts_index.html",
"postsDetail": "templates/posts_detail.html",
"postsIndexPath": "/blog",
"postsDetailPath": "/blog"
}
Note the flat structure with {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath keys.`);
}
// Detect if cmsTemplates uses wrong nested format
if (m.cmsTemplates && typeof m.cmsTemplates === 'object') {
const templates = m.cmsTemplates;
for (const [key, value] of Object.entries(templates)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const nested = value;
if (nested.indexPath || nested.detailPath || nested.indexFile || nested.detailFile) {
errors.push(`WRONG FORMAT in cmsTemplates.${key}: Using nested object format
WRONG (what you have):
"cmsTemplates": {
"${key}": { "indexPath": "...", "indexFile": "...", "detailPath": "...", "detailFile": "..." }
}
CORRECT (what you need):
"cmsTemplates": {
"${key}Index": "templates/${key}_index.html",
"${key}Detail": "templates/${key}_detail.html",
"${key}IndexPath": "${nested.indexPath || '/' + key}",
"${key}DetailPath": "${nested.detailPath || '/' + key}"
}
Use the FLAT format with separate keys for each property.`);
break; // Only show this error once
}
}
}
}
// Check pages array
if (!Array.isArray(m.pages)) {
errors.push('- pages: Must be an array');
}
else if (m.pages.length === 0) {
errors.push(`NO PAGES DEFINED: The pages array is empty.
You must define at least one page, typically the homepage:
"pages": [
{ "path": "/", "file": "pages/index.html", "title": "Home" }
]
A site without pages will have no accessible content.`);
}
else {
// Check for homepage
const hasHomepage = m.pages.some((p) => p.path === '/');
if (!hasHomepage) {
errors.push(`NO HOMEPAGE: Missing path "/" in pages array.
Every site needs a homepage. Add a page with path "/":
"pages": [
{ "path": "/", "file": "pages/index.html", "title": "Home" },
...your other pages...
]
Without a homepage, visitors will see a 404 error.`);
}
// Check file paths
for (const page of m.pages) {
if (typeof page.file === 'string') {
if (!page.file.startsWith('pages/')) {
warnings.push(`- Page "${page.path}": file should be in pages/ folder (got: ${page.file})`);
}
if (!page.file.endsWith('.html')) {
warnings.push(`- Page "${page.path}": file should end with .html (got: ${page.file})`);
}
}
}
}
// Check cmsTemplates - enforce strict pattern-based validation
const templates = m.cmsTemplates;
const customCollections = new Set();
if (templates) {
// All keys must match the unified format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
const VALID_KEY_PATTERN = /^[a-z][a-zA-Z0-9_]*(Index|Detail|IndexPath|DetailPath)$/;
const KEY_PARTS_PATTERN = /^([a-z][a-zA-Z0-9_]*)(Index|Detail|IndexPath|DetailPath)$/;
for (const key of Object.keys(templates)) {
if (!VALID_KEY_PATTERN.test(key)) {
// Provide helpful error message with suggestion
let suggestion = '';
if (key.toLowerCase().includes('post')) {
suggestion = ` Did you mean "${key.replace(/[Pp]ost/g, 'Detail')}"?`;
}
else if (!key.includes('Index') && !key.includes('Detail')) {
suggestion = ` Did you mean "${key}Index" or "${key}Detail"?`;
}
errors.push(`- Invalid cmsTemplates key "${key}". ` +
`All keys must use format: {slug}Index, {slug}Detail, {slug}IndexPath, or {slug}DetailPath.${suggestion}`);
continue;
}
// Extract collection slug from valid keys
const match = key.match(KEY_PARTS_PATTERN);
if (match) {
customCollections.add(match[1]);
}
}
// Validate each detected collection
for (const slug of customCollections) {
const indexTemplate = templates[`${slug}Index`];
const detailTemplate = templates[`${slug}Detail`];
const indexPath = templates[`${slug}IndexPath`];
const detailPath = templates[`${slug}DetailPath`];
// Check template file paths
if (typeof indexTemplate === 'string') {
if (!indexTemplate.endsWith('.html')) {
warnings.push(`- cmsTemplates.${slug}Index: should end with .html (got: ${indexTemplate})`);
}
}
if (typeof detailTemplate === 'string') {
if (!detailTemplate.endsWith('.html')) {
warnings.push(`- cmsTemplates.${slug}Detail: should end with .html (got: ${detailTemplate})`);
}
}
// Check path format
if (typeof indexPath === 'string' && !indexPath.startsWith('/')) {
errors.push(`- cmsTemplates.${slug}IndexPath: must start with "/" (got: ${indexPath})`);
}
if (typeof detailPath === 'string' && !detailPath.startsWith('/')) {
errors.push(`- cmsTemplates.${slug}DetailPath: must start with "/" (got: ${detailPath})`);
}
// Check consistency - incomplete CMS config will cause broken routes
if (indexTemplate && !detailTemplate) {
errors.push(`INCOMPLETE CMS CONFIG: Collection "${slug}" has ${slug}Index but no ${slug}Detail.
Both templates are required for a collection to work:
"cmsTemplates": {
"${slug}Index": "templates/${slug}_index.html",
"${slug}Detail": "templates/${slug}_detail.html", // <- MISSING
"${slug}IndexPath": "${indexPath || '/' + slug}",
"${slug}DetailPath": "${detailPath || '/' + slug}"
}
Without both templates, clicking on list items will lead to 404 errors.`);
}
if (detailTemplate && !indexTemplate) {
errors.push(`INCOMPLETE CMS CONFIG: Collection "${slug}" has ${slug}Detail but no ${slug}Index.
Both templates are required for a collection to work:
"cmsTemplates": {
"${slug}Index": "templates/${slug}_index.html", // <- MISSING
"${slug}Detail": "templates/${slug}_detail.html",
"${slug}IndexPath": "${indexPath || '/' + slug}",
"${slug}DetailPath": "${detailPath || '/' + slug}"
}
Without an index template, there's no way to list and navigate to items.`);
}
if (indexTemplate && !indexPath) {
warnings.push(`- Collection "${slug}": has ${slug}Index but no ${slug}IndexPath - using default path`);
}
if (detailTemplate && !detailPath) {
warnings.push(`- Collection "${slug}": has ${slug}Detail but no ${slug}DetailPath - using default path`);
}
}
}
// Build result
let output = '';
// Build collections summary
const collectionsSummary = customCollections.size > 0
? `\n- Collections: ${Array.from(customCollections).join(', ')}`
: '';
if (errors.length === 0 && warnings.length === 0) {
output = `MANIFEST VALID
The manifest.json structure is correct.
Summary:
- ${m.pages?.length || 0} static page(s) defined
- CMS templates: ${templates ? 'configured in manifest' : 'not configured (can be set via Settings → CMS Templates after upload)'}${collectionsSummary}
- Head HTML: ${m.defaultHeadHtml ? 'configured' : 'not configured'}`;
}
else if (errors.length === 0) {
output = `MANIFEST VALID WITH WARNINGS
The manifest structure is valid but has potential issues:
Warnings:
${warnings.join('\n')}
Tip: CMS templates can also be configured after upload via Dashboard → Settings → CMS Templates`;
}
else {
output = `MANIFEST INVALID
Errors (must fix):
${errors.join('\n')}`;
if (warnings.length > 0) {
output += `
Warnings:
${warnings.join('\n')}`;
}
}
return output;
}