Skip to content

Commit 424e9b1

Browse files
committed
feat: update version to 0.6.0 and add configuration validation schema
1 parent 6d9f38c commit 424e9b1

6 files changed

Lines changed: 616 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@litodocs/cli",
3-
"version": "0.5.2",
3+
"version": "0.6.0",
44
"description": "Beautiful documentation sites from Markdown. Fast, simple, and open-source.",
55
"main": "src/index.js",
66
"type": "module",

src/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function cli() {
2020
.description(
2121
"Beautiful documentation sites from Markdown. Fast, simple, and open-source."
2222
)
23-
.version("0.5.2");
23+
.version("0.6.0");
2424

2525
program
2626
.command("build")

src/core/config-sync.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import pkg from 'fs-extra';
22
const { readFile, writeFile, pathExists } = pkg;
33
import { join } from 'path';
44
import { readdir } from 'fs/promises';
5+
import { validateConfig } from './config-validator.js';
56

67
/**
78
* Auto-generates navigation structure from docs folder
@@ -70,8 +71,11 @@ export async function generateNavigationFromDocs(docsPath) {
7071

7172
/**
7273
* Merges user config with auto-generated navigation
74+
* @param {object} options - Sync options
75+
* @param {boolean} options.validate - Whether to validate the config (default: true)
7376
*/
74-
export async function syncDocsConfig(projectDir, docsPath, userConfigPath) {
77+
export async function syncDocsConfig(projectDir, docsPath, userConfigPath, options = {}) {
78+
const { validate = true } = options;
7579
const configPath = join(projectDir, 'docs-config.json');
7680

7781
// Read base config from template
@@ -80,6 +84,16 @@ export async function syncDocsConfig(projectDir, docsPath, userConfigPath) {
8084
// If user has a custom config, merge it
8185
if (userConfigPath && await pathExists(userConfigPath)) {
8286
const userConfig = JSON.parse(await readFile(userConfigPath, 'utf-8'));
87+
88+
// Validate user config before merging (only core config is validated)
89+
if (validate) {
90+
const validationResult = validateConfig(userConfig, projectDir);
91+
if (!validationResult.valid) {
92+
const errorMessages = validationResult.errors.map(e => `${e.path}: ${e.message}`).join('\n ');
93+
throw new Error(`Configuration validation failed:\n ${errorMessages}`);
94+
}
95+
}
96+
8397
config = deepMerge(config, userConfig);
8498
}
8599

@@ -90,6 +104,8 @@ export async function syncDocsConfig(projectDir, docsPath, userConfigPath) {
90104

91105
// Write merged config
92106
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
107+
108+
return { config };
93109
}
94110

95111
function formatLabel(str) {

src/core/config-validator.js

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/**
2+
* Configuration Validator
3+
*
4+
* Validates user docs-config.json against the core schema.
5+
* Core config is strictly validated - extensions are always allowed
6+
* (templates simply ignore what they don't support).
7+
*/
8+
9+
import { readFileSync, existsSync } from 'fs';
10+
import { join, dirname } from 'path';
11+
import { fileURLToPath } from 'url';
12+
import pc from 'picocolors';
13+
14+
const __dirname = dirname(fileURLToPath(import.meta.url));
15+
16+
// Core schema defines portable config across all templates
17+
const CORE_SCHEMA_PATH = join(__dirname, '../schema/core-schema.json');
18+
19+
// Core config keys that all templates must support
20+
const CORE_CONFIG_KEYS = [
21+
'metadata',
22+
'branding',
23+
'navigation',
24+
'search',
25+
'seo',
26+
'i18n',
27+
'assets'
28+
];
29+
30+
// Extension keys that are template-specific (optional, never cause errors)
31+
const EXTENSION_KEYS = [
32+
'footer',
33+
'theme',
34+
'landing',
35+
'integrations',
36+
'versioning'
37+
];
38+
39+
/**
40+
* Load the core schema
41+
*/
42+
function loadCoreSchema() {
43+
if (!existsSync(CORE_SCHEMA_PATH)) {
44+
return null;
45+
}
46+
return JSON.parse(readFileSync(CORE_SCHEMA_PATH, 'utf-8'));
47+
}
48+
49+
/**
50+
* Load template manifest from project directory
51+
*/
52+
export function loadTemplateManifest(projectDir) {
53+
const manifestPath = join(projectDir, 'template.json');
54+
if (!existsSync(manifestPath)) {
55+
return null;
56+
}
57+
try {
58+
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
59+
} catch (e) {
60+
return null;
61+
}
62+
}
63+
64+
/**
65+
* Validate configuration against core schema (basic validation)
66+
* Only validates REQUIRED fields and types - extensions are always allowed.
67+
*/
68+
function validateCoreConfig(config, schema) {
69+
const errors = [];
70+
71+
// Check required fields
72+
if (schema.required) {
73+
for (const field of schema.required) {
74+
if (!(field in config)) {
75+
errors.push({
76+
path: field,
77+
message: `Required field '${field}' is missing`
78+
});
79+
}
80+
}
81+
}
82+
83+
// Check metadata.name is required
84+
if (config.metadata && !config.metadata.name) {
85+
errors.push({
86+
path: 'metadata.name',
87+
message: "Required field 'metadata.name' is missing"
88+
});
89+
}
90+
91+
// Validate types for core fields
92+
if (config.metadata && typeof config.metadata !== 'object') {
93+
errors.push({
94+
path: 'metadata',
95+
message: "'metadata' must be an object"
96+
});
97+
}
98+
99+
if (config.navigation?.sidebar && !Array.isArray(config.navigation.sidebar)) {
100+
errors.push({
101+
path: 'navigation.sidebar',
102+
message: "'navigation.sidebar' must be an array"
103+
});
104+
}
105+
106+
if (config.navigation?.navbar?.links && !Array.isArray(config.navigation.navbar.links)) {
107+
errors.push({
108+
path: 'navigation.navbar.links',
109+
message: "'navigation.navbar.links' must be an array"
110+
});
111+
}
112+
113+
// Validate sidebar items structure
114+
if (config.navigation?.sidebar && Array.isArray(config.navigation.sidebar)) {
115+
config.navigation.sidebar.forEach((group, i) => {
116+
if (!group.label) {
117+
errors.push({
118+
path: `navigation.sidebar[${i}].label`,
119+
message: `Sidebar group at index ${i} is missing required 'label' field`
120+
});
121+
}
122+
if (group.items && !Array.isArray(group.items)) {
123+
errors.push({
124+
path: `navigation.sidebar[${i}].items`,
125+
message: `Sidebar group '${group.label}' items must be an array`
126+
});
127+
}
128+
});
129+
}
130+
131+
return errors;
132+
}
133+
134+
/**
135+
* Validate user configuration
136+
*
137+
* Only validates core config structure. Extensions are always allowed -
138+
* templates simply ignore what they don't support.
139+
*
140+
* @param {object} config - User's docs-config.json content
141+
* @param {string} projectDir - Path to the project directory
142+
* @param {object} options - Validation options
143+
* @param {boolean} options.silent - If true, don't print anything
144+
* @returns {{ valid: boolean, errors: Array }}
145+
*/
146+
export function validateConfig(config, projectDir, options = {}) {
147+
const { silent = false } = options;
148+
149+
const coreSchema = loadCoreSchema();
150+
151+
// Only validate core config - extensions are always allowed
152+
const coreErrors = coreSchema ? validateCoreConfig(config, coreSchema) : [];
153+
154+
const result = {
155+
valid: coreErrors.length === 0,
156+
errors: coreErrors,
157+
manifest: loadTemplateManifest(projectDir)
158+
};
159+
160+
if (!silent && coreErrors.length > 0) {
161+
printValidationErrors(coreErrors);
162+
}
163+
164+
return result;
165+
}
166+
167+
/**
168+
* Print validation errors to console
169+
*/
170+
function printValidationErrors(errors) {
171+
console.log(pc.red('\n✗ Configuration validation failed:\n'));
172+
for (const error of errors) {
173+
console.log(pc.red(` • ${error.path}: ${error.message}`));
174+
}
175+
console.log('');
176+
}
177+
178+
/**
179+
* Get list of portable (core) config keys
180+
*/
181+
export function getCoreConfigKeys() {
182+
return [...CORE_CONFIG_KEYS];
183+
}
184+
185+
/**
186+
* Get list of extension config keys
187+
*/
188+
export function getExtensionKeys() {
189+
return [...EXTENSION_KEYS];
190+
}
191+
192+
/**
193+
* Extract only core config from a full config object
194+
*/
195+
export function extractCoreConfig(config) {
196+
const coreConfig = {};
197+
for (const key of CORE_CONFIG_KEYS) {
198+
if (key in config) {
199+
coreConfig[key] = config[key];
200+
}
201+
}
202+
return coreConfig;
203+
}
204+
205+
/**
206+
* Extract only extension config from a full config object
207+
*/
208+
export function extractExtensionConfig(config) {
209+
const extensionConfig = {};
210+
for (const key of EXTENSION_KEYS) {
211+
if (key in config) {
212+
extensionConfig[key] = config[key];
213+
}
214+
}
215+
return extensionConfig;
216+
}
217+
218+
/**
219+
* Check if config is portable (uses only core config)
220+
* Useful for users who want to ensure their config works with any template.
221+
*/
222+
export function isPortableConfig(config) {
223+
for (const key of EXTENSION_KEYS) {
224+
if (key in config) {
225+
return false;
226+
}
227+
}
228+
return true;
229+
}

0 commit comments

Comments
 (0)