@litodocs/cli 0.6.0 → 0.7.0
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/README.md +325 -124
- package/lito-manifest.schema.json +118 -0
- package/package.json +1 -1
- package/src/cli.js +48 -0
- package/src/commands/build.js +26 -17
- package/src/commands/dev.js +17 -12
- package/src/commands/doctor.js +311 -0
- package/src/commands/info.js +192 -0
- package/src/commands/init.js +284 -0
- package/src/commands/preview.js +98 -0
- package/src/commands/validate.js +124 -0
- package/src/core/config.js +62 -18
- package/src/core/framework-runner.js +208 -0
- package/src/core/landing-sync.js +708 -0
- package/src/core/output.js +10 -4
- package/src/core/sync.js +141 -15
- package/src/core/template-registry.js +8 -5
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing Page Sync Module
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* 1. Full custom landing (_landing/ folder with HTML/CSS/JS)
|
|
6
|
+
* 2. Section-based landing (mix of custom HTML and default components)
|
|
7
|
+
*
|
|
8
|
+
* The landing page configuration in docs-config.json determines which mode is used.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import pkg from 'fs-extra';
|
|
12
|
+
const { copy, ensureDir, readFile, writeFile, pathExists, readJson } = pkg;
|
|
13
|
+
import { join, relative, basename, extname } from 'path';
|
|
14
|
+
import { readdir } from 'fs/promises';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Landing page types
|
|
18
|
+
*/
|
|
19
|
+
export const LANDING_TYPES = {
|
|
20
|
+
NONE: 'none', // No landing page, go straight to docs
|
|
21
|
+
DEFAULT: 'default', // Use template's default landing
|
|
22
|
+
CONFIG: 'config', // Landing defined in docs-config.json (hero, features, etc.)
|
|
23
|
+
CUSTOM: 'custom', // Full custom HTML/CSS/JS from _landing/ folder
|
|
24
|
+
SECTIONS: 'sections', // Section-based: mix of custom HTML and default components
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default section types that templates should support
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_SECTION_TYPES = [
|
|
31
|
+
'hero',
|
|
32
|
+
'features',
|
|
33
|
+
'cta',
|
|
34
|
+
'testimonials',
|
|
35
|
+
'pricing',
|
|
36
|
+
'faq',
|
|
37
|
+
'stats',
|
|
38
|
+
'logos',
|
|
39
|
+
'comparison',
|
|
40
|
+
'footer',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detect landing page type from user's docs directory
|
|
45
|
+
* @param {string} sourcePath - User's docs directory
|
|
46
|
+
* @param {object} docsConfig - Parsed docs-config.json
|
|
47
|
+
* @returns {Promise<{type: string, config: object}>}
|
|
48
|
+
*/
|
|
49
|
+
export async function detectLandingType(sourcePath, docsConfig) {
|
|
50
|
+
const landingConfig = docsConfig?.landing || {};
|
|
51
|
+
|
|
52
|
+
// Check if landing is explicitly disabled
|
|
53
|
+
if (landingConfig.enabled === false) {
|
|
54
|
+
return { type: LANDING_TYPES.NONE, config: landingConfig };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for _landing folder (custom HTML/CSS/JS)
|
|
58
|
+
const landingFolderPath = join(sourcePath, '_landing');
|
|
59
|
+
const hasLandingFolder = await pathExists(landingFolderPath);
|
|
60
|
+
|
|
61
|
+
// Determine type based on config
|
|
62
|
+
if (landingConfig.type === 'custom' || (hasLandingFolder && !landingConfig.type)) {
|
|
63
|
+
return { type: LANDING_TYPES.CUSTOM, config: landingConfig };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (landingConfig.type === 'sections' && landingConfig.sections) {
|
|
67
|
+
return { type: LANDING_TYPES.SECTIONS, config: landingConfig };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (landingConfig.type === 'none') {
|
|
71
|
+
return { type: LANDING_TYPES.NONE, config: landingConfig };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for config-based landing (hero, features defined in config)
|
|
75
|
+
if (landingConfig.hero || landingConfig.features) {
|
|
76
|
+
return { type: LANDING_TYPES.CONFIG, config: landingConfig };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Default to template's default landing
|
|
80
|
+
return { type: LANDING_TYPES.DEFAULT, config: landingConfig };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sync custom landing page from _landing/ folder
|
|
85
|
+
* @param {string} sourcePath - User's docs directory
|
|
86
|
+
* @param {string} projectDir - Scaffolded project directory
|
|
87
|
+
* @param {object} frameworkConfig - Framework configuration
|
|
88
|
+
* @param {object} landingConfig - Landing configuration
|
|
89
|
+
*/
|
|
90
|
+
export async function syncCustomLanding(sourcePath, projectDir, frameworkConfig, landingConfig) {
|
|
91
|
+
const landingSource = join(sourcePath, landingConfig.source || '_landing');
|
|
92
|
+
|
|
93
|
+
if (!await pathExists(landingSource)) {
|
|
94
|
+
console.warn(`Warning: Landing folder not found at ${landingSource}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Read all files from _landing/
|
|
99
|
+
const files = await readdir(landingSource, { withFileTypes: true });
|
|
100
|
+
|
|
101
|
+
// Separate files by type
|
|
102
|
+
const htmlFiles = [];
|
|
103
|
+
const cssFiles = [];
|
|
104
|
+
const jsFiles = [];
|
|
105
|
+
const assetFiles = [];
|
|
106
|
+
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
if (file.isDirectory()) {
|
|
109
|
+
// Handle subdirectories (e.g., _landing/assets/)
|
|
110
|
+
if (file.name === 'assets' || file.name === 'images') {
|
|
111
|
+
assetFiles.push(file.name);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ext = extname(file.name).toLowerCase();
|
|
117
|
+
if (ext === '.html' || ext === '.htm') {
|
|
118
|
+
htmlFiles.push(file.name);
|
|
119
|
+
} else if (ext === '.css') {
|
|
120
|
+
cssFiles.push(file.name);
|
|
121
|
+
} else if (ext === '.js' || ext === '.mjs') {
|
|
122
|
+
jsFiles.push(file.name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Generate landing page based on framework
|
|
127
|
+
await generateLandingForFramework(
|
|
128
|
+
projectDir,
|
|
129
|
+
frameworkConfig,
|
|
130
|
+
{
|
|
131
|
+
sourcePath: landingSource,
|
|
132
|
+
htmlFiles,
|
|
133
|
+
cssFiles,
|
|
134
|
+
jsFiles,
|
|
135
|
+
assetFiles,
|
|
136
|
+
config: landingConfig,
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sync section-based landing page
|
|
143
|
+
* @param {string} sourcePath - User's docs directory
|
|
144
|
+
* @param {string} projectDir - Scaffolded project directory
|
|
145
|
+
* @param {object} frameworkConfig - Framework configuration
|
|
146
|
+
* @param {object} landingConfig - Landing configuration with sections array
|
|
147
|
+
*/
|
|
148
|
+
export async function syncSectionsLanding(sourcePath, projectDir, frameworkConfig, landingConfig) {
|
|
149
|
+
const sections = landingConfig.sections || [];
|
|
150
|
+
const processedSections = [];
|
|
151
|
+
|
|
152
|
+
for (const section of sections) {
|
|
153
|
+
if (section.type === 'custom' && section.html) {
|
|
154
|
+
// Load custom HTML for this section
|
|
155
|
+
const htmlPath = join(sourcePath, section.html);
|
|
156
|
+
if (await pathExists(htmlPath)) {
|
|
157
|
+
const htmlContent = await readFile(htmlPath, 'utf-8');
|
|
158
|
+
processedSections.push({
|
|
159
|
+
...section,
|
|
160
|
+
type: 'custom',
|
|
161
|
+
content: htmlContent,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
console.warn(`Warning: Section HTML not found: ${htmlPath}`);
|
|
165
|
+
processedSections.push(section);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// Use default component for this section type
|
|
169
|
+
processedSections.push(section);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Generate sections landing for framework
|
|
174
|
+
await generateSectionsLandingForFramework(
|
|
175
|
+
projectDir,
|
|
176
|
+
frameworkConfig,
|
|
177
|
+
{
|
|
178
|
+
sections: processedSections,
|
|
179
|
+
config: landingConfig,
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate landing page for specific framework
|
|
186
|
+
*/
|
|
187
|
+
async function generateLandingForFramework(projectDir, frameworkConfig, landingData) {
|
|
188
|
+
const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
|
|
189
|
+
|
|
190
|
+
switch (frameworkConfig.name) {
|
|
191
|
+
case 'astro':
|
|
192
|
+
await generateAstroLanding(projectDir, landingData);
|
|
193
|
+
break;
|
|
194
|
+
case 'react':
|
|
195
|
+
await generateReactLanding(projectDir, landingData);
|
|
196
|
+
break;
|
|
197
|
+
case 'next':
|
|
198
|
+
await generateNextLanding(projectDir, landingData);
|
|
199
|
+
break;
|
|
200
|
+
case 'vue':
|
|
201
|
+
await generateVueLanding(projectDir, landingData);
|
|
202
|
+
break;
|
|
203
|
+
case 'nuxt':
|
|
204
|
+
await generateNuxtLanding(projectDir, landingData);
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
// Default to Astro-style
|
|
208
|
+
await generateAstroLanding(projectDir, landingData);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate Astro landing page (standalone, no template imports)
|
|
214
|
+
*/
|
|
215
|
+
async function generateAstroLanding(projectDir, landingData) {
|
|
216
|
+
const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
|
|
217
|
+
|
|
218
|
+
// Read main HTML file
|
|
219
|
+
const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
|
|
220
|
+
if (!mainHtml) {
|
|
221
|
+
console.warn('Warning: No HTML file found in _landing/');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
|
|
226
|
+
|
|
227
|
+
// Read CSS files
|
|
228
|
+
let cssContent = '';
|
|
229
|
+
for (const cssFile of cssFiles) {
|
|
230
|
+
const css = await readFile(join(sourcePath, cssFile), 'utf-8');
|
|
231
|
+
cssContent += `/* ${cssFile} */\n${css}\n\n`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Read JS files
|
|
235
|
+
let jsContent = '';
|
|
236
|
+
for (const jsFile of jsFiles) {
|
|
237
|
+
const js = await readFile(join(sourcePath, jsFile), 'utf-8');
|
|
238
|
+
jsContent += `// ${jsFile}\n${js}\n\n`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Generate standalone Astro component
|
|
242
|
+
const astroContent = `---
|
|
243
|
+
// Custom landing page - auto-generated by Lito CLI
|
|
244
|
+
// Source: _landing/ folder
|
|
245
|
+
import '../styles/global.css';
|
|
246
|
+
import Header from '../components/Header.astro';
|
|
247
|
+
import Footer from '../components/Footer.astro';
|
|
248
|
+
import { getConfigFile } from '../utils/config';
|
|
249
|
+
|
|
250
|
+
const config = await getConfigFile();
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
<!doctype html>
|
|
254
|
+
<html lang="en" class="dark">
|
|
255
|
+
<head>
|
|
256
|
+
<meta charset="UTF-8" />
|
|
257
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
258
|
+
<title>{config.metadata?.name || 'Home'}</title>
|
|
259
|
+
<meta name="description" content={config.metadata?.description || ''} />
|
|
260
|
+
<link rel="icon" href={config.branding?.favicon || '/favicon.svg'} />
|
|
261
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
262
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
263
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
264
|
+
<script is:inline>
|
|
265
|
+
const savedTheme = localStorage.getItem('theme');
|
|
266
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
267
|
+
if (savedTheme === 'light' || (!savedTheme && !prefersDark)) {
|
|
268
|
+
document.documentElement.classList.remove('dark');
|
|
269
|
+
document.documentElement.classList.add('light');
|
|
270
|
+
}
|
|
271
|
+
</script>
|
|
272
|
+
<style>
|
|
273
|
+
${cssContent}
|
|
274
|
+
</style>
|
|
275
|
+
</head>
|
|
276
|
+
<body class="bg-background text-foreground font-sans antialiased">
|
|
277
|
+
<Header />
|
|
278
|
+
|
|
279
|
+
<main class="landing-custom">
|
|
280
|
+
${htmlContent}
|
|
281
|
+
</main>
|
|
282
|
+
|
|
283
|
+
<Footer />
|
|
284
|
+
|
|
285
|
+
${jsContent ? `<script>\n${jsContent}\n</script>` : ''}
|
|
286
|
+
</body>
|
|
287
|
+
</html>
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
// Write to index.astro
|
|
291
|
+
const indexPath = join(projectDir, 'src', 'pages', 'index.astro');
|
|
292
|
+
await writeFile(indexPath, astroContent, 'utf-8');
|
|
293
|
+
|
|
294
|
+
// Copy assets if they exist
|
|
295
|
+
await copyLandingAssets(sourcePath, projectDir);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate React landing page (standalone)
|
|
300
|
+
*/
|
|
301
|
+
async function generateReactLanding(projectDir, landingData) {
|
|
302
|
+
const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
|
|
303
|
+
|
|
304
|
+
// Read main HTML file
|
|
305
|
+
const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
|
|
306
|
+
if (!mainHtml) {
|
|
307
|
+
console.warn('Warning: No HTML file found in _landing/');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
|
|
312
|
+
|
|
313
|
+
// Read CSS files
|
|
314
|
+
let cssContent = '';
|
|
315
|
+
for (const cssFile of cssFiles) {
|
|
316
|
+
const css = await readFile(join(sourcePath, cssFile), 'utf-8');
|
|
317
|
+
cssContent += `/* ${cssFile} */\n${css}\n\n`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Write CSS to a separate file
|
|
321
|
+
const cssPath = join(projectDir, 'src', 'styles', 'landing.css');
|
|
322
|
+
await ensureDir(join(projectDir, 'src', 'styles'));
|
|
323
|
+
await writeFile(cssPath, cssContent, 'utf-8');
|
|
324
|
+
|
|
325
|
+
// Read JS files for inline script
|
|
326
|
+
let jsContent = '';
|
|
327
|
+
for (const jsFile of jsFiles) {
|
|
328
|
+
const js = await readFile(join(sourcePath, jsFile), 'utf-8');
|
|
329
|
+
jsContent += `// ${jsFile}\n${js}\n\n`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Generate standalone React component
|
|
333
|
+
const reactContent = `// Custom landing page - auto-generated by Lito CLI
|
|
334
|
+
// Source: _landing/ folder
|
|
335
|
+
import { useEffect } from 'react';
|
|
336
|
+
import '../styles/landing.css';
|
|
337
|
+
|
|
338
|
+
export default function LandingPage() {
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
// Landing page scripts
|
|
341
|
+
${jsContent}
|
|
342
|
+
}, []);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<main
|
|
346
|
+
className="landing-custom"
|
|
347
|
+
dangerouslySetInnerHTML={{ __html: \`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\` }}
|
|
348
|
+
/>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
// Write to index page
|
|
354
|
+
const landingPath = join(projectDir, 'src', 'pages', 'index.jsx');
|
|
355
|
+
await ensureDir(join(projectDir, 'src', 'pages'));
|
|
356
|
+
await writeFile(landingPath, reactContent, 'utf-8');
|
|
357
|
+
|
|
358
|
+
// Copy assets if they exist
|
|
359
|
+
await copyLandingAssets(sourcePath, projectDir);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Generate Next.js landing page (standalone)
|
|
364
|
+
*/
|
|
365
|
+
async function generateNextLanding(projectDir, landingData) {
|
|
366
|
+
const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
|
|
367
|
+
|
|
368
|
+
// Read main HTML file
|
|
369
|
+
const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
|
|
370
|
+
if (!mainHtml) {
|
|
371
|
+
console.warn('Warning: No HTML file found in _landing/');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
|
|
376
|
+
|
|
377
|
+
// Read CSS files
|
|
378
|
+
let cssContent = '';
|
|
379
|
+
for (const cssFile of cssFiles) {
|
|
380
|
+
const css = await readFile(join(sourcePath, cssFile), 'utf-8');
|
|
381
|
+
cssContent += `/* ${cssFile} */\n${css}\n\n`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Write CSS to globals or landing file
|
|
385
|
+
const cssPath = join(projectDir, 'app', 'landing.css');
|
|
386
|
+
await ensureDir(join(projectDir, 'app'));
|
|
387
|
+
await writeFile(cssPath, cssContent, 'utf-8');
|
|
388
|
+
|
|
389
|
+
// Read JS files
|
|
390
|
+
let jsContent = '';
|
|
391
|
+
for (const jsFile of jsFiles) {
|
|
392
|
+
const js = await readFile(join(sourcePath, jsFile), 'utf-8');
|
|
393
|
+
jsContent += `// ${jsFile}\n${js}\n\n`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Generate standalone Next.js page
|
|
397
|
+
const nextContent = `// Custom landing page - auto-generated by Lito CLI
|
|
398
|
+
// Source: _landing/ folder
|
|
399
|
+
'use client';
|
|
400
|
+
|
|
401
|
+
import { useEffect } from 'react';
|
|
402
|
+
import './landing.css';
|
|
403
|
+
|
|
404
|
+
export default function Home() {
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
// Landing page scripts
|
|
407
|
+
${jsContent}
|
|
408
|
+
}, []);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<main
|
|
412
|
+
className="landing-custom"
|
|
413
|
+
dangerouslySetInnerHTML={{ __html: \`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\` }}
|
|
414
|
+
/>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
`;
|
|
418
|
+
|
|
419
|
+
// Write to page.jsx (Next.js 13+ app router)
|
|
420
|
+
const pagePath = join(projectDir, 'app', 'page.jsx');
|
|
421
|
+
await writeFile(pagePath, nextContent, 'utf-8');
|
|
422
|
+
|
|
423
|
+
// Copy assets if they exist
|
|
424
|
+
await copyLandingAssets(sourcePath, projectDir);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Generate Vue landing page (standalone)
|
|
429
|
+
*/
|
|
430
|
+
async function generateVueLanding(projectDir, landingData) {
|
|
431
|
+
const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
|
|
432
|
+
|
|
433
|
+
// Read main HTML file
|
|
434
|
+
const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
|
|
435
|
+
if (!mainHtml) {
|
|
436
|
+
console.warn('Warning: No HTML file found in _landing/');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
|
|
441
|
+
|
|
442
|
+
// Read CSS files
|
|
443
|
+
let cssContent = '';
|
|
444
|
+
for (const cssFile of cssFiles) {
|
|
445
|
+
const css = await readFile(join(sourcePath, cssFile), 'utf-8');
|
|
446
|
+
cssContent += `/* ${cssFile} */\n${css}\n\n`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Read JS files
|
|
450
|
+
let jsContent = '';
|
|
451
|
+
for (const jsFile of jsFiles) {
|
|
452
|
+
const js = await readFile(join(sourcePath, jsFile), 'utf-8');
|
|
453
|
+
jsContent += js + '\n';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Generate standalone Vue SFC
|
|
457
|
+
const vueContent = `<script setup>
|
|
458
|
+
// Custom landing page - auto-generated by Lito CLI
|
|
459
|
+
// Source: _landing/ folder
|
|
460
|
+
import { onMounted, ref } from 'vue';
|
|
461
|
+
|
|
462
|
+
const landingHtml = ref(\`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\`);
|
|
463
|
+
|
|
464
|
+
onMounted(() => {
|
|
465
|
+
// Landing page scripts
|
|
466
|
+
${jsContent}
|
|
467
|
+
});
|
|
468
|
+
</script>
|
|
469
|
+
|
|
470
|
+
<template>
|
|
471
|
+
<main class="landing-custom" v-html="landingHtml"></main>
|
|
472
|
+
</template>
|
|
473
|
+
|
|
474
|
+
<style scoped>
|
|
475
|
+
${cssContent}
|
|
476
|
+
</style>
|
|
477
|
+
`;
|
|
478
|
+
|
|
479
|
+
// Write to index page
|
|
480
|
+
const vuePath = join(projectDir, 'src', 'pages', 'index.vue');
|
|
481
|
+
await ensureDir(join(projectDir, 'src', 'pages'));
|
|
482
|
+
await writeFile(vuePath, vueContent, 'utf-8');
|
|
483
|
+
|
|
484
|
+
// Copy assets if they exist
|
|
485
|
+
await copyLandingAssets(sourcePath, projectDir);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Generate Nuxt landing page (standalone)
|
|
490
|
+
*/
|
|
491
|
+
async function generateNuxtLanding(projectDir, landingData) {
|
|
492
|
+
const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
|
|
493
|
+
|
|
494
|
+
// Read main HTML file
|
|
495
|
+
const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
|
|
496
|
+
if (!mainHtml) {
|
|
497
|
+
console.warn('Warning: No HTML file found in _landing/');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
|
|
502
|
+
|
|
503
|
+
// Read CSS files
|
|
504
|
+
let cssContent = '';
|
|
505
|
+
for (const cssFile of cssFiles) {
|
|
506
|
+
const css = await readFile(join(sourcePath, cssFile), 'utf-8');
|
|
507
|
+
cssContent += `/* ${cssFile} */\n${css}\n\n`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Read JS files
|
|
511
|
+
let jsContent = '';
|
|
512
|
+
for (const jsFile of jsFiles) {
|
|
513
|
+
const js = await readFile(join(sourcePath, jsFile), 'utf-8');
|
|
514
|
+
jsContent += js + '\n';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Generate standalone Nuxt page
|
|
518
|
+
const nuxtContent = `<script setup>
|
|
519
|
+
// Custom landing page - auto-generated by Lito CLI
|
|
520
|
+
// Source: _landing/ folder
|
|
521
|
+
|
|
522
|
+
const landingHtml = \`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\`;
|
|
523
|
+
|
|
524
|
+
onMounted(() => {
|
|
525
|
+
// Landing page scripts
|
|
526
|
+
${jsContent}
|
|
527
|
+
});
|
|
528
|
+
</script>
|
|
529
|
+
|
|
530
|
+
<template>
|
|
531
|
+
<main class="landing-custom" v-html="landingHtml"></main>
|
|
532
|
+
</template>
|
|
533
|
+
|
|
534
|
+
<style scoped>
|
|
535
|
+
${cssContent}
|
|
536
|
+
</style>
|
|
537
|
+
`;
|
|
538
|
+
|
|
539
|
+
// Write to index page
|
|
540
|
+
const nuxtPath = join(projectDir, 'pages', 'index.vue');
|
|
541
|
+
await ensureDir(join(projectDir, 'pages'));
|
|
542
|
+
await writeFile(nuxtPath, nuxtContent, 'utf-8');
|
|
543
|
+
|
|
544
|
+
// Copy assets if they exist
|
|
545
|
+
await copyLandingAssets(sourcePath, projectDir);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Generate sections-based landing for framework
|
|
550
|
+
*/
|
|
551
|
+
async function generateSectionsLandingForFramework(projectDir, frameworkConfig, landingData) {
|
|
552
|
+
const { sections, config } = landingData;
|
|
553
|
+
|
|
554
|
+
// For now, we'll generate a JSON manifest that templates can read
|
|
555
|
+
// Templates are responsible for rendering the sections
|
|
556
|
+
const manifestPath = join(projectDir, 'src', 'data', 'landing-sections.json');
|
|
557
|
+
await ensureDir(join(projectDir, 'src', 'data'));
|
|
558
|
+
|
|
559
|
+
await writeFile(manifestPath, JSON.stringify({
|
|
560
|
+
type: 'sections',
|
|
561
|
+
sections: sections,
|
|
562
|
+
}, null, 2), 'utf-8');
|
|
563
|
+
|
|
564
|
+
// Additionally, generate a default sections component for Astro
|
|
565
|
+
if (frameworkConfig.name === 'astro') {
|
|
566
|
+
await generateAstroSectionsLanding(projectDir, landingData);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Generate Astro sections landing page
|
|
572
|
+
*/
|
|
573
|
+
async function generateAstroSectionsLanding(projectDir, landingData) {
|
|
574
|
+
const { sections, config } = landingData;
|
|
575
|
+
|
|
576
|
+
// Generate section renders
|
|
577
|
+
const sectionRenders = sections.map((section, index) => {
|
|
578
|
+
if (section.type === 'custom' && section.content) {
|
|
579
|
+
// Custom HTML section
|
|
580
|
+
return `
|
|
581
|
+
<!-- Custom Section ${index + 1} -->
|
|
582
|
+
<section class="landing-section landing-section-custom" id="section-${index}">
|
|
583
|
+
<Fragment set:html={\`${section.content.replace(/`/g, '\\`')}\`} />
|
|
584
|
+
</section>`;
|
|
585
|
+
} else {
|
|
586
|
+
// Default component section - template must provide these components
|
|
587
|
+
const componentName = section.type.charAt(0).toUpperCase() + section.type.slice(1) + 'Section';
|
|
588
|
+
return `
|
|
589
|
+
<!-- ${section.type} Section -->
|
|
590
|
+
{Astro.glob('../components/landing/${componentName}.astro').then(m => m[0] ? <m[0].default {...${JSON.stringify(section.props || {})}} /> : null)}`;
|
|
591
|
+
}
|
|
592
|
+
}).join('\n');
|
|
593
|
+
|
|
594
|
+
const astroContent = `---
|
|
595
|
+
// Sections-based landing page - auto-generated by Lito CLI
|
|
596
|
+
import '../styles/global.css';
|
|
597
|
+
import Header from '../components/Header.astro';
|
|
598
|
+
import Footer from '../components/Footer.astro';
|
|
599
|
+
import { getConfigFile } from '../utils/config';
|
|
600
|
+
|
|
601
|
+
const config = await getConfigFile();
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
<!doctype html>
|
|
605
|
+
<html lang="en" class="dark">
|
|
606
|
+
<head>
|
|
607
|
+
<meta charset="UTF-8" />
|
|
608
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
609
|
+
<title>{config.metadata?.name || 'Home'}</title>
|
|
610
|
+
<link rel="icon" href={config.branding?.favicon || '/favicon.svg'} />
|
|
611
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
612
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
613
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&display=swap" rel="stylesheet" />
|
|
614
|
+
<script is:inline>
|
|
615
|
+
const savedTheme = localStorage.getItem('theme');
|
|
616
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
617
|
+
if (savedTheme === 'light' || (!savedTheme && !prefersDark)) {
|
|
618
|
+
document.documentElement.classList.remove('dark');
|
|
619
|
+
document.documentElement.classList.add('light');
|
|
620
|
+
}
|
|
621
|
+
</script>
|
|
622
|
+
</head>
|
|
623
|
+
<body class="bg-background text-foreground font-sans antialiased">
|
|
624
|
+
<Header />
|
|
625
|
+
|
|
626
|
+
<main class="landing-sections">
|
|
627
|
+
${sectionRenders}
|
|
628
|
+
</main>
|
|
629
|
+
|
|
630
|
+
<Footer />
|
|
631
|
+
</body>
|
|
632
|
+
</html>
|
|
633
|
+
`;
|
|
634
|
+
|
|
635
|
+
// Only write if custom sections are defined
|
|
636
|
+
if (sections.some(s => s.type === 'custom')) {
|
|
637
|
+
const indexPath = join(projectDir, 'src', 'pages', 'index.astro');
|
|
638
|
+
await writeFile(indexPath, astroContent, 'utf-8');
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Copy landing assets to public directory
|
|
644
|
+
*/
|
|
645
|
+
async function copyLandingAssets(sourcePath, projectDir) {
|
|
646
|
+
const assetDirs = ['assets', 'images', 'img', 'icons'];
|
|
647
|
+
|
|
648
|
+
for (const dir of assetDirs) {
|
|
649
|
+
const assetSource = join(sourcePath, dir);
|
|
650
|
+
if (await pathExists(assetSource)) {
|
|
651
|
+
const assetTarget = join(projectDir, 'public', 'landing', dir);
|
|
652
|
+
await ensureDir(assetTarget);
|
|
653
|
+
await copy(assetSource, assetTarget, { overwrite: true });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Also copy any image files directly in _landing/
|
|
658
|
+
try {
|
|
659
|
+
const files = await readdir(sourcePath);
|
|
660
|
+
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'];
|
|
661
|
+
|
|
662
|
+
for (const file of files) {
|
|
663
|
+
const ext = extname(file).toLowerCase();
|
|
664
|
+
if (imageExts.includes(ext)) {
|
|
665
|
+
const src = join(sourcePath, file);
|
|
666
|
+
const dest = join(projectDir, 'public', 'landing', file);
|
|
667
|
+
await ensureDir(join(projectDir, 'public', 'landing'));
|
|
668
|
+
await copy(src, dest, { overwrite: true });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch {
|
|
672
|
+
// Ignore errors
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Main landing sync function
|
|
678
|
+
* @param {string} sourcePath - User's docs directory
|
|
679
|
+
* @param {string} projectDir - Scaffolded project directory
|
|
680
|
+
* @param {object} frameworkConfig - Framework configuration
|
|
681
|
+
* @param {object} docsConfig - Parsed docs-config.json
|
|
682
|
+
*/
|
|
683
|
+
export async function syncLanding(sourcePath, projectDir, frameworkConfig, docsConfig) {
|
|
684
|
+
const { type, config } = await detectLandingType(sourcePath, docsConfig);
|
|
685
|
+
|
|
686
|
+
switch (type) {
|
|
687
|
+
case LANDING_TYPES.NONE:
|
|
688
|
+
// Remove default landing page, show docs index directly
|
|
689
|
+
// This is handled by templates based on config
|
|
690
|
+
break;
|
|
691
|
+
|
|
692
|
+
case LANDING_TYPES.CUSTOM:
|
|
693
|
+
await syncCustomLanding(sourcePath, projectDir, frameworkConfig, config);
|
|
694
|
+
break;
|
|
695
|
+
|
|
696
|
+
case LANDING_TYPES.SECTIONS:
|
|
697
|
+
await syncSectionsLanding(sourcePath, projectDir, frameworkConfig, config);
|
|
698
|
+
break;
|
|
699
|
+
|
|
700
|
+
case LANDING_TYPES.CONFIG:
|
|
701
|
+
case LANDING_TYPES.DEFAULT:
|
|
702
|
+
// These are handled by templates reading docs-config.json
|
|
703
|
+
// Just ensure the config is synced (done elsewhere)
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { type, config };
|
|
708
|
+
}
|