@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
package/src/core/output.js
CHANGED
|
@@ -2,12 +2,18 @@ import pkg from 'fs-extra';
|
|
|
2
2
|
const { copy, ensureDir } = pkg;
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Copy built output to the specified output path
|
|
7
|
+
* @param {string} projectDir - The scaffolded project directory
|
|
8
|
+
* @param {string} outputPath - User's desired output path
|
|
9
|
+
* @param {string} buildOutputDir - Framework's build output directory (default: 'dist')
|
|
10
|
+
*/
|
|
11
|
+
export async function copyOutput(projectDir, outputPath, buildOutputDir = 'dist') {
|
|
12
|
+
const distPath = join(projectDir, buildOutputDir);
|
|
13
|
+
|
|
8
14
|
// Ensure output directory exists
|
|
9
15
|
await ensureDir(outputPath);
|
|
10
|
-
|
|
16
|
+
|
|
11
17
|
// Copy built files to output
|
|
12
18
|
await copy(distPath, outputPath, {
|
|
13
19
|
overwrite: true,
|
package/src/core/sync.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import pkg from 'fs-extra';
|
|
2
2
|
const { copy, ensureDir, remove, readFile, writeFile, pathExists } = pkg;
|
|
3
3
|
import { join, relative, sep } from 'path';
|
|
4
|
-
import { readdir, stat } from 'fs/promises';
|
|
4
|
+
import { readdir, stat, utimes } from 'fs/promises';
|
|
5
|
+
import { syncLanding } from './landing-sync.js';
|
|
5
6
|
|
|
6
7
|
// Known locale codes (ISO 639-1)
|
|
7
8
|
const KNOWN_LOCALES = [
|
|
@@ -12,7 +13,7 @@ const KNOWN_LOCALES = [
|
|
|
12
13
|
];
|
|
13
14
|
|
|
14
15
|
// Special folders that are not content
|
|
15
|
-
const SPECIAL_FOLDERS = ['_assets', '_css', '_images', '_static', 'public'];
|
|
16
|
+
const SPECIAL_FOLDERS = ['_assets', '_css', '_images', '_static', '_landing', 'public'];
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Get i18n configuration from docs-config.json
|
|
@@ -30,6 +31,21 @@ async function getI18nConfig(sourcePath) {
|
|
|
30
31
|
return { defaultLocale: 'en', locales: ['en'] };
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Get full docs-config.json
|
|
36
|
+
*/
|
|
37
|
+
async function getDocsConfig(sourcePath) {
|
|
38
|
+
const configPath = join(sourcePath, 'docs-config.json');
|
|
39
|
+
if (await pathExists(configPath)) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(await readFile(configPath, 'utf-8'));
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
/**
|
|
34
50
|
* Get versioning configuration from docs-config.json
|
|
35
51
|
*/
|
|
@@ -97,8 +113,17 @@ async function detectVersionFolders(sourcePath, versioningConfig) {
|
|
|
97
113
|
return versionFolders;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Sync documentation files to the project
|
|
118
|
+
* @param {string} sourcePath - User's docs directory
|
|
119
|
+
* @param {string} projectDir - Scaffolded project directory
|
|
120
|
+
* @param {object} frameworkConfig - Framework configuration (optional, defaults to Astro)
|
|
121
|
+
*/
|
|
122
|
+
export async function syncDocs(sourcePath, projectDir, frameworkConfig = null) {
|
|
123
|
+
// Default to Astro's src/pages for backward compatibility
|
|
124
|
+
const contentDir = frameworkConfig?.contentDir || 'src/pages';
|
|
125
|
+
const targetPath = join(projectDir, contentDir);
|
|
126
|
+
const shouldInjectLayout = frameworkConfig?.layoutInjection !== false;
|
|
102
127
|
|
|
103
128
|
// Clear existing pages (except index if it exists in template)
|
|
104
129
|
await ensureDir(targetPath);
|
|
@@ -159,8 +184,10 @@ export async function syncDocs(sourcePath, projectDir) {
|
|
|
159
184
|
},
|
|
160
185
|
});
|
|
161
186
|
|
|
162
|
-
// Inject layout into version markdown files
|
|
163
|
-
|
|
187
|
+
// Inject layout into version markdown files (only for frameworks that need it)
|
|
188
|
+
if (shouldInjectLayout) {
|
|
189
|
+
await injectLayoutIntoMarkdown(versionTargetPath, targetPath, null, [], version, frameworkConfig);
|
|
190
|
+
}
|
|
164
191
|
}));
|
|
165
192
|
|
|
166
193
|
// Sync locale folders
|
|
@@ -177,12 +204,16 @@ export async function syncDocs(sourcePath, projectDir) {
|
|
|
177
204
|
},
|
|
178
205
|
});
|
|
179
206
|
|
|
180
|
-
// Inject layout into locale markdown files
|
|
181
|
-
|
|
207
|
+
// Inject layout into locale markdown files (only for frameworks that need it)
|
|
208
|
+
if (shouldInjectLayout) {
|
|
209
|
+
await injectLayoutIntoMarkdown(localeTargetPath, targetPath, locale, [], null, frameworkConfig);
|
|
210
|
+
}
|
|
182
211
|
}));
|
|
183
212
|
|
|
184
|
-
// Inject layout into default locale markdown files
|
|
185
|
-
|
|
213
|
+
// Inject layout into default locale markdown files (only for frameworks that need it)
|
|
214
|
+
if (shouldInjectLayout) {
|
|
215
|
+
await injectLayoutIntoMarkdown(targetPath, targetPath, null, excludeFolders, null, frameworkConfig);
|
|
216
|
+
}
|
|
186
217
|
|
|
187
218
|
// Check for custom landing page conflict
|
|
188
219
|
const hasUserIndex = ['index.md', 'index.mdx'].some(file =>
|
|
@@ -198,6 +229,18 @@ export async function syncDocs(sourcePath, projectDir) {
|
|
|
198
229
|
|
|
199
230
|
// Sync user assets (images, css, static files)
|
|
200
231
|
await syncUserAssets(sourcePath, projectDir);
|
|
232
|
+
|
|
233
|
+
// Sync landing page (custom HTML/CSS/JS or sections)
|
|
234
|
+
const docsConfig = await getDocsConfig(sourcePath);
|
|
235
|
+
if (docsConfig.landing) {
|
|
236
|
+
await syncLanding(sourcePath, projectDir, frameworkConfig, docsConfig);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Trigger HMR for non-Astro frameworks (Vite-based)
|
|
240
|
+
// Vite doesn't automatically watch content directories, so we need to trigger a reload
|
|
241
|
+
if (frameworkConfig && frameworkConfig.name !== 'astro') {
|
|
242
|
+
await triggerHMR(projectDir, frameworkConfig);
|
|
243
|
+
}
|
|
201
244
|
}
|
|
202
245
|
|
|
203
246
|
/**
|
|
@@ -283,7 +326,27 @@ async function syncUserAssets(sourcePath, projectDir) {
|
|
|
283
326
|
const customImport = '@import "./custom.css";';
|
|
284
327
|
|
|
285
328
|
if (!globalCss.includes(customImport)) {
|
|
286
|
-
|
|
329
|
+
// @import must come at the very top of the file (before @tailwind directives)
|
|
330
|
+
// Only @charset can precede @import
|
|
331
|
+
const lines = globalCss.split('\n');
|
|
332
|
+
let insertIndex = 0;
|
|
333
|
+
|
|
334
|
+
// Find position after @charset if it exists (only thing allowed before @import)
|
|
335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
336
|
+
const trimmed = lines[i].trim();
|
|
337
|
+
if (trimmed.startsWith('@charset')) {
|
|
338
|
+
insertIndex = i + 1;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
// Stop at first non-empty, non-comment line
|
|
342
|
+
if (trimmed && !trimmed.startsWith('/*') && !trimmed.startsWith('*') && !trimmed.startsWith('//')) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Insert the import at the top (after @charset if present)
|
|
348
|
+
lines.splice(insertIndex, 0, '/* User custom styles */', customImport, '');
|
|
349
|
+
globalCss = lines.join('\n');
|
|
287
350
|
await writeFile(globalCssPath, globalCss, 'utf-8');
|
|
288
351
|
}
|
|
289
352
|
}
|
|
@@ -299,10 +362,15 @@ async function syncUserAssets(sourcePath, projectDir) {
|
|
|
299
362
|
* @param {string|null} locale - Current locale (null for default)
|
|
300
363
|
* @param {string[]} skipFolders - Folders to skip (locale folders when processing root)
|
|
301
364
|
* @param {string|null} version - Current version (null for non-versioned)
|
|
365
|
+
* @param {object|null} frameworkConfig - Framework configuration for layout paths
|
|
302
366
|
*/
|
|
303
|
-
async function injectLayoutIntoMarkdown(dir, rootDir, locale = null, skipFolders = [], version = null) {
|
|
367
|
+
async function injectLayoutIntoMarkdown(dir, rootDir, locale = null, skipFolders = [], version = null, frameworkConfig = null) {
|
|
304
368
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
305
369
|
|
|
370
|
+
// Get layout paths from framework config or use defaults
|
|
371
|
+
const baseLayoutPath = frameworkConfig?.layoutPath || 'layouts/MarkdownLayout.astro';
|
|
372
|
+
const apiLayoutPath = frameworkConfig?.apiLayoutPath || 'layouts/APILayout.astro';
|
|
373
|
+
|
|
306
374
|
await Promise.all(entries.map(async (entry) => {
|
|
307
375
|
const fullPath = join(dir, entry.name);
|
|
308
376
|
|
|
@@ -311,14 +379,14 @@ async function injectLayoutIntoMarkdown(dir, rootDir, locale = null, skipFolders
|
|
|
311
379
|
if (skipFolders.includes(entry.name)) {
|
|
312
380
|
return;
|
|
313
381
|
}
|
|
314
|
-
await injectLayoutIntoMarkdown(fullPath, rootDir, locale, [], version);
|
|
382
|
+
await injectLayoutIntoMarkdown(fullPath, rootDir, locale, [], version, frameworkConfig);
|
|
315
383
|
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
|
|
316
384
|
let content = await readFile(fullPath, 'utf-8');
|
|
317
385
|
|
|
318
386
|
// Calculate relative path to layout
|
|
319
387
|
const relPath = relative(rootDir, fullPath);
|
|
320
388
|
const depth = relPath.split(sep).length - 1;
|
|
321
|
-
const layoutPath = '../'.repeat(depth + 1) +
|
|
389
|
+
const layoutPath = '../'.repeat(depth + 1) + baseLayoutPath;
|
|
322
390
|
|
|
323
391
|
// Check if file already has frontmatter
|
|
324
392
|
if (content.startsWith('---')) {
|
|
@@ -330,7 +398,7 @@ async function injectLayoutIntoMarkdown(dir, rootDir, locale = null, skipFolders
|
|
|
330
398
|
// Determine which layout to use
|
|
331
399
|
let targetLayout = layoutPath;
|
|
332
400
|
if (frontmatterBlock.includes('api:')) {
|
|
333
|
-
targetLayout = '../'.repeat(depth + 1) +
|
|
401
|
+
targetLayout = '../'.repeat(depth + 1) + apiLayoutPath;
|
|
334
402
|
}
|
|
335
403
|
|
|
336
404
|
// Add locale to frontmatter if present
|
|
@@ -362,3 +430,61 @@ async function injectLayoutIntoMarkdown(dir, rootDir, locale = null, skipFolders
|
|
|
362
430
|
}
|
|
363
431
|
}));
|
|
364
432
|
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Trigger HMR for Vite-based frameworks
|
|
436
|
+
* Vite watches certain files for changes - we touch a trigger file to force a reload
|
|
437
|
+
* @param {string} projectDir - Project directory
|
|
438
|
+
* @param {object} frameworkConfig - Framework configuration
|
|
439
|
+
*/
|
|
440
|
+
async function triggerHMR(projectDir, frameworkConfig) {
|
|
441
|
+
// Use framework-specific HMR trigger file if defined
|
|
442
|
+
const triggerFilePath = frameworkConfig.hmrTriggerFile
|
|
443
|
+
? join(projectDir, frameworkConfig.hmrTriggerFile)
|
|
444
|
+
: join(projectDir, 'src', '.lito-hmr-trigger.js');
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
// Ensure parent directory exists
|
|
448
|
+
await ensureDir(join(projectDir, 'src'));
|
|
449
|
+
|
|
450
|
+
// Write a timestamp to the trigger file
|
|
451
|
+
// This file should be imported in the app's entry point
|
|
452
|
+
const timestamp = Date.now();
|
|
453
|
+
const content = `// Auto-generated by Lito CLI for HMR triggering
|
|
454
|
+
// This file is updated when docs content changes
|
|
455
|
+
// Import this file in your app's entry point to enable content hot reload
|
|
456
|
+
export const LITO_CONTENT_VERSION = ${timestamp};
|
|
457
|
+
export const LITO_LAST_UPDATE = "${new Date().toISOString()}";
|
|
458
|
+
|
|
459
|
+
// Force Vite to treat this as a module that triggers HMR
|
|
460
|
+
if (import.meta.hot) {
|
|
461
|
+
import.meta.hot.accept();
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
|
|
465
|
+
await writeFile(triggerFilePath, content, 'utf-8');
|
|
466
|
+
} catch {
|
|
467
|
+
// Fallback: Touch the main entry file or vite config
|
|
468
|
+
const fallbackFiles = [
|
|
469
|
+
join(projectDir, 'src', 'main.jsx'),
|
|
470
|
+
join(projectDir, 'src', 'main.tsx'),
|
|
471
|
+
join(projectDir, 'src', 'App.jsx'),
|
|
472
|
+
join(projectDir, 'src', 'App.tsx'),
|
|
473
|
+
join(projectDir, 'src', 'index.jsx'),
|
|
474
|
+
join(projectDir, 'src', 'index.tsx'),
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
for (const file of fallbackFiles) {
|
|
478
|
+
if (await pathExists(file)) {
|
|
479
|
+
try {
|
|
480
|
+
// Touch the file by updating its mtime
|
|
481
|
+
const now = new Date();
|
|
482
|
+
await utimes(file, now, now);
|
|
483
|
+
break;
|
|
484
|
+
} catch {
|
|
485
|
+
// Continue to next file
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export const TEMPLATE_REGISTRY = {
|
|
12
|
-
// Default template
|
|
13
|
-
'default': 'github:Lito-docs/template',
|
|
12
|
+
// Default template - Astro-based
|
|
13
|
+
'default': 'github:Lito-docs/lito-astro-template',
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Framework-specific templates
|
|
16
|
+
'astro': 'github:Lito-docs/lito-astro-template',
|
|
17
|
+
'react': 'github:Lito-docs/lito-react-template',
|
|
18
|
+
'next': 'github:Lito-docs/lito-next-template',
|
|
19
|
+
'vue': 'github:Lito-docs/lito-vue-template',
|
|
20
|
+
'nuxt': 'github:Lito-docs/lito-nuxt-template',
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
/**
|