@litodocs/cli 0.5.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.
@@ -0,0 +1,78 @@
1
+ import { join } from 'path';
2
+ import pkg from 'fs-extra';
3
+ const { writeFile, readFile, pathExists } = pkg;
4
+ import { installPackage } from './package-manager.js';
5
+
6
+ export async function configureProvider(projectDir, provider, rendering = 'static') {
7
+ // Cloudflare
8
+ if (provider === 'cloudflare') {
9
+ if (rendering !== 'static') {
10
+ await installPackage(projectDir, '@astrojs/cloudflare');
11
+ await updateAstroConfig(projectDir, 'cloudflare', rendering);
12
+ }
13
+ // Cloudflare usually requires a _routes.json or wrangler.toml, but the adapter handles the build output.
14
+ // We can add a basic wrangler.toml if needed, but often not required for Pages.
15
+ }
16
+
17
+ // Vercel
18
+ else if (provider === 'vercel') {
19
+ if (rendering !== 'static') {
20
+ await installPackage(projectDir, '@astrojs/vercel');
21
+ await updateAstroConfig(projectDir, 'vercel', rendering);
22
+ }
23
+ const configPath = join(projectDir, 'vercel.json');
24
+ if (!await pathExists(configPath)) {
25
+ const content = {
26
+ "cleanUrls": true,
27
+ "framework": "astro",
28
+ "buildCommand": "lito build",
29
+ "outputDirectory": "dist"
30
+ };
31
+ await writeFile(configPath, JSON.stringify(content, null, 2), 'utf-8');
32
+ }
33
+ }
34
+
35
+ // Netlify
36
+ else if (provider === 'netlify') {
37
+ if (rendering !== 'static') {
38
+ await installPackage(projectDir, '@astrojs/netlify');
39
+ await updateAstroConfig(projectDir, 'netlify', rendering);
40
+ }
41
+ const configPath = join(projectDir, 'netlify.toml');
42
+ if (!await pathExists(configPath)) {
43
+ const content = `[build]
44
+ publish = "dist"
45
+ command = "lito build"
46
+
47
+ [[headers]]
48
+ for = "/*"
49
+ [headers.values]
50
+ X-Frame-Options = "DENY"
51
+ X-XSS-Protection = "1; mode=block"
52
+ `;
53
+ await writeFile(configPath, content, 'utf-8');
54
+ }
55
+ }
56
+ }
57
+
58
+ async function updateAstroConfig(projectDir, adapterName, rendering) {
59
+ const configPath = join(projectDir, 'astro.config.mjs');
60
+ let content = await readFile(configPath, 'utf-8');
61
+
62
+ // Add import
63
+ const importStatement = `import ${adapterName} from '@astrojs/${adapterName}';\n`;
64
+ if (!content.includes(`@astrojs/${adapterName}`)) {
65
+ content = importStatement + content;
66
+ }
67
+
68
+ // Add adapter and output to defineConfig
69
+ // We look for "export default defineConfig({"
70
+ if (content.includes('export default defineConfig({') && !content.includes('output:')) {
71
+ content = content.replace(
72
+ 'export default defineConfig({\n',
73
+ `export default defineConfig({\n adapter: ${adapterName}(),\n output: '${rendering}',`
74
+ );
75
+ }
76
+
77
+ await writeFile(configPath, content, 'utf-8');
78
+ }
@@ -0,0 +1,53 @@
1
+ import pkg from 'fs-extra';
2
+ const { ensureDir, emptyDir, copy, remove } = pkg;
3
+ import { tmpdir } from 'os';
4
+ import { join, basename } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Use a consistent directory path instead of random temp directories
12
+ const LITO_DIR = join(tmpdir(), '.lito');
13
+
14
+ export async function scaffoldProject(customTemplatePath = null) {
15
+ // Ensure the directory exists (creates if it doesn't)
16
+ await ensureDir(LITO_DIR);
17
+
18
+ // Empty the directory to ensure a clean state
19
+ await emptyDir(LITO_DIR);
20
+
21
+ const tempDir = LITO_DIR;
22
+
23
+ // Use custom template path if provided, otherwise use bundled template
24
+ const templatePath = customTemplatePath || join(__dirname, '../template');
25
+ await copy(templatePath, tempDir, {
26
+ filter: (src) => {
27
+ const name = basename(src);
28
+
29
+ // Exclude node_modules directory
30
+ if (name === 'node_modules') return false;
31
+
32
+ // Exclude lock files
33
+ if (name === 'pnpm-lock.yaml' || name === 'bun.lock' ||
34
+ name === 'package-lock.json' || name === 'yarn.lock') return false;
35
+
36
+ // Exclude .astro cache directory (but NOT .astro component files)
37
+ if (name === '.astro') return false;
38
+
39
+ return true;
40
+ }
41
+ });
42
+
43
+ return tempDir;
44
+ }
45
+
46
+ // Cleanup function to remove the temp directory on exit
47
+ export async function cleanupProject() {
48
+ try {
49
+ await remove(LITO_DIR);
50
+ } catch (error) {
51
+ // Ignore errors during cleanup (directory might not exist)
52
+ }
53
+ }
@@ -0,0 +1,364 @@
1
+ import pkg from 'fs-extra';
2
+ const { copy, ensureDir, remove, readFile, writeFile, pathExists } = pkg;
3
+ import { join, relative, sep } from 'path';
4
+ import { readdir, stat } from 'fs/promises';
5
+
6
+ // Known locale codes (ISO 639-1)
7
+ const KNOWN_LOCALES = [
8
+ 'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'zh', 'ja', 'ko',
9
+ 'ar', 'hi', 'bn', 'pa', 'id', 'ms', 'th', 'vi', 'tr', 'pl',
10
+ 'nl', 'sv', 'da', 'no', 'fi', 'cs', 'sk', 'hu', 'ro', 'bg',
11
+ 'uk', 'he', 'fa', 'ur', 'ta', 'te', 'mr', 'gu', 'kn', 'ml',
12
+ ];
13
+
14
+ // Special folders that are not content
15
+ const SPECIAL_FOLDERS = ['_assets', '_css', '_images', '_static', 'public'];
16
+
17
+ /**
18
+ * Get i18n configuration from docs-config.json
19
+ */
20
+ async function getI18nConfig(sourcePath) {
21
+ const configPath = join(sourcePath, 'docs-config.json');
22
+ if (await pathExists(configPath)) {
23
+ try {
24
+ const config = JSON.parse(await readFile(configPath, 'utf-8'));
25
+ return config.i18n || { defaultLocale: 'en', locales: ['en'] };
26
+ } catch {
27
+ return { defaultLocale: 'en', locales: ['en'] };
28
+ }
29
+ }
30
+ return { defaultLocale: 'en', locales: ['en'] };
31
+ }
32
+
33
+ /**
34
+ * Get versioning configuration from docs-config.json
35
+ */
36
+ async function getVersioningConfig(sourcePath) {
37
+ const configPath = join(sourcePath, 'docs-config.json');
38
+ if (await pathExists(configPath)) {
39
+ try {
40
+ const config = JSON.parse(await readFile(configPath, 'utf-8'));
41
+ return config.versioning || { enabled: false };
42
+ } catch {
43
+ return { enabled: false };
44
+ }
45
+ }
46
+ return { enabled: false };
47
+ }
48
+
49
+ /**
50
+ * Detect locale folders in the docs directory
51
+ */
52
+ async function detectLocaleFolders(sourcePath, configuredLocales) {
53
+ const detectedLocales = [];
54
+
55
+ try {
56
+ const entries = await readdir(sourcePath, { withFileTypes: true });
57
+
58
+ for (const entry of entries) {
59
+ if (entry.isDirectory()) {
60
+ const folderName = entry.name.toLowerCase();
61
+ // Check if it's a known locale or configured locale
62
+ if (KNOWN_LOCALES.includes(folderName) || configuredLocales.includes(folderName)) {
63
+ detectedLocales.push(entry.name);
64
+ }
65
+ }
66
+ }
67
+ } catch {
68
+ // Ignore errors
69
+ }
70
+
71
+ return detectedLocales;
72
+ }
73
+
74
+ /**
75
+ * Detect version folders based on versioning config
76
+ */
77
+ async function detectVersionFolders(sourcePath, versioningConfig) {
78
+ if (!versioningConfig?.enabled || !versioningConfig?.versions?.length) {
79
+ return [];
80
+ }
81
+
82
+ const versionFolders = [];
83
+
84
+ try {
85
+ const entries = await readdir(sourcePath, { withFileTypes: true });
86
+ const configuredPaths = versioningConfig.versions.map(v => v.path);
87
+
88
+ for (const entry of entries) {
89
+ if (entry.isDirectory() && configuredPaths.includes(entry.name)) {
90
+ versionFolders.push(entry.name);
91
+ }
92
+ }
93
+ } catch {
94
+ // Ignore errors
95
+ }
96
+
97
+ return versionFolders;
98
+ }
99
+
100
+ export async function syncDocs(sourcePath, projectDir) {
101
+ const targetPath = join(projectDir, 'src', 'pages');
102
+
103
+ // Clear existing pages (except index if it exists in template)
104
+ await ensureDir(targetPath);
105
+
106
+ // Get i18n configuration
107
+ const i18nConfig = await getI18nConfig(sourcePath);
108
+ const configuredLocales = i18nConfig.locales || ['en'];
109
+ const defaultLocale = i18nConfig.defaultLocale || 'en';
110
+
111
+ // Get versioning configuration
112
+ const versioningConfig = await getVersioningConfig(sourcePath);
113
+ const versionFolders = await detectVersionFolders(sourcePath, versioningConfig);
114
+
115
+ // Detect locale folders
116
+ const localeFolders = await detectLocaleFolders(sourcePath, configuredLocales);
117
+
118
+ // Combine folders to exclude from root sync
119
+ const excludeFolders = [...localeFolders, ...versionFolders];
120
+
121
+ // Copy default content (root level, excluding locale and version folders)
122
+ await copy(sourcePath, targetPath, {
123
+ overwrite: true,
124
+ filter: (src) => {
125
+ const relativePath = relative(sourcePath, src);
126
+ const firstSegment = relativePath.split(sep)[0];
127
+
128
+ // Exclude special folders
129
+ if (SPECIAL_FOLDERS.some(folder => relativePath.startsWith(folder))) {
130
+ return false;
131
+ }
132
+
133
+ // Exclude locale and version folders (they'll be synced separately)
134
+ if (excludeFolders.includes(firstSegment)) {
135
+ return false;
136
+ }
137
+
138
+ // Exclude config files
139
+ if (relativePath === 'docs-config.json' || relativePath === 'vercel.json') {
140
+ return false;
141
+ }
142
+
143
+ // Only copy markdown/mdx files and directories
144
+ return src.endsWith('.md') || src.endsWith('.mdx') || !src.includes('.');
145
+ },
146
+ });
147
+
148
+ // Sync version folders
149
+ await Promise.all(versionFolders.map(async (version) => {
150
+ const versionSourcePath = join(sourcePath, version);
151
+ const versionTargetPath = join(targetPath, version);
152
+
153
+ await ensureDir(versionTargetPath);
154
+ await copy(versionSourcePath, versionTargetPath, {
155
+ overwrite: true,
156
+ filter: (src) => {
157
+ // Only copy markdown/mdx files and directories
158
+ return src.endsWith('.md') || src.endsWith('.mdx') || !src.includes('.');
159
+ },
160
+ });
161
+
162
+ // Inject layout into version markdown files
163
+ await injectLayoutIntoMarkdown(versionTargetPath, targetPath, null, [], version);
164
+ }));
165
+
166
+ // Sync locale folders
167
+ await Promise.all(localeFolders.map(async (locale) => {
168
+ const localeSourcePath = join(sourcePath, locale);
169
+ const localeTargetPath = join(targetPath, locale);
170
+
171
+ await ensureDir(localeTargetPath);
172
+ await copy(localeSourcePath, localeTargetPath, {
173
+ overwrite: true,
174
+ filter: (src) => {
175
+ // Only copy markdown/mdx files and directories
176
+ return src.endsWith('.md') || src.endsWith('.mdx') || !src.includes('.');
177
+ },
178
+ });
179
+
180
+ // Inject layout into locale markdown files
181
+ await injectLayoutIntoMarkdown(localeTargetPath, targetPath, locale);
182
+ }));
183
+
184
+ // Inject layout into default locale markdown files
185
+ await injectLayoutIntoMarkdown(targetPath, targetPath, null, excludeFolders);
186
+
187
+ // Check for custom landing page conflict
188
+ const hasUserIndex = ['index.md', 'index.mdx'].some(file =>
189
+ pkg.existsSync(join(targetPath, file))
190
+ );
191
+
192
+ if (hasUserIndex) {
193
+ const defaultIndexAstro = join(targetPath, 'index.astro');
194
+ if (pkg.existsSync(defaultIndexAstro)) {
195
+ await remove(defaultIndexAstro);
196
+ }
197
+ }
198
+
199
+ // Sync user assets (images, css, static files)
200
+ await syncUserAssets(sourcePath, projectDir);
201
+ }
202
+
203
+ /**
204
+ * Sync user assets from docs folder to the Astro project
205
+ */
206
+ async function syncUserAssets(sourcePath, projectDir) {
207
+ const tasks = [];
208
+
209
+ // Sync _assets folder to public/assets
210
+ tasks.push((async () => {
211
+ const assetsSource = join(sourcePath, '_assets');
212
+ const assetsTarget = join(projectDir, 'public', 'assets');
213
+ if (await pathExists(assetsSource)) {
214
+ await ensureDir(assetsTarget);
215
+ await copy(assetsSource, assetsTarget, { overwrite: true });
216
+ }
217
+ })());
218
+
219
+ // Sync _images folder to public/images
220
+ tasks.push((async () => {
221
+ const imagesSource = join(sourcePath, '_images');
222
+ const imagesTarget = join(projectDir, 'public', 'images');
223
+ if (await pathExists(imagesSource)) {
224
+ await ensureDir(imagesTarget);
225
+ await copy(imagesSource, imagesTarget, { overwrite: true });
226
+ }
227
+ })());
228
+
229
+ // Sync public folder to public root
230
+ tasks.push((async () => {
231
+ const publicSource = join(sourcePath, 'public');
232
+ const publicTarget = join(projectDir, 'public');
233
+ if (await pathExists(publicSource)) {
234
+ await ensureDir(publicTarget);
235
+ await copy(publicSource, publicTarget, { overwrite: true });
236
+ }
237
+ })());
238
+
239
+ // Sync _css folder for custom styles
240
+ tasks.push((async () => {
241
+ const cssSource = join(sourcePath, '_css');
242
+ const cssTarget = join(projectDir, 'src', 'styles');
243
+ if (await pathExists(cssSource)) {
244
+ await ensureDir(cssTarget);
245
+
246
+ const cssTasks = [];
247
+
248
+ // Copy custom.css if it exists
249
+ const customCssSource = join(cssSource, 'custom.css');
250
+ cssTasks.push((async () => {
251
+ if (await pathExists(customCssSource)) {
252
+ await copy(customCssSource, join(cssTarget, 'custom.css'), { overwrite: true });
253
+ }
254
+ })());
255
+
256
+ // Copy theme.css override if it exists
257
+ const themeOverrideSource = join(cssSource, 'theme.css');
258
+ cssTasks.push((async () => {
259
+ if (await pathExists(themeOverrideSource)) {
260
+ await copy(themeOverrideSource, join(cssTarget, 'theme.css'), { overwrite: true });
261
+ }
262
+ })());
263
+
264
+ // Copy any additional CSS files
265
+ cssTasks.push((async () => {
266
+ const cssFiles = await readdir(cssSource).catch(() => []);
267
+ await Promise.all(cssFiles.map(async (file) => {
268
+ if (file.endsWith('.css') && file !== 'custom.css' && file !== 'theme.css') {
269
+ await copy(join(cssSource, file), join(cssTarget, file), { overwrite: true });
270
+ }
271
+ }));
272
+ })());
273
+
274
+ await Promise.all(cssTasks);
275
+ }
276
+
277
+ // Create/update custom.css import in global.css if custom.css exists
278
+ const customCssPath = join(cssTarget, 'custom.css');
279
+ const globalCssPath = join(cssTarget, 'global.css');
280
+
281
+ if (await pathExists(customCssPath) && await pathExists(globalCssPath)) {
282
+ let globalCss = await readFile(globalCssPath, 'utf-8');
283
+ const customImport = '@import "./custom.css";';
284
+
285
+ if (!globalCss.includes(customImport)) {
286
+ globalCss = globalCss.trimEnd() + '\n\n/* User custom styles */\n' + customImport + '\n';
287
+ await writeFile(globalCssPath, globalCss, 'utf-8');
288
+ }
289
+ }
290
+ })());
291
+
292
+ await Promise.all(tasks);
293
+ }
294
+
295
+ /**
296
+ * Inject layout frontmatter into markdown files
297
+ * @param {string} dir - Directory to process
298
+ * @param {string} rootDir - Root pages directory
299
+ * @param {string|null} locale - Current locale (null for default)
300
+ * @param {string[]} skipFolders - Folders to skip (locale folders when processing root)
301
+ * @param {string|null} version - Current version (null for non-versioned)
302
+ */
303
+ async function injectLayoutIntoMarkdown(dir, rootDir, locale = null, skipFolders = [], version = null) {
304
+ const entries = await readdir(dir, { withFileTypes: true });
305
+
306
+ await Promise.all(entries.map(async (entry) => {
307
+ const fullPath = join(dir, entry.name);
308
+
309
+ if (entry.isDirectory()) {
310
+ // Skip locale folders when processing root
311
+ if (skipFolders.includes(entry.name)) {
312
+ return;
313
+ }
314
+ await injectLayoutIntoMarkdown(fullPath, rootDir, locale, [], version);
315
+ } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
316
+ let content = await readFile(fullPath, 'utf-8');
317
+
318
+ // Calculate relative path to layout
319
+ const relPath = relative(rootDir, fullPath);
320
+ const depth = relPath.split(sep).length - 1;
321
+ const layoutPath = '../'.repeat(depth + 1) + 'layouts/MarkdownLayout.astro';
322
+
323
+ // Check if file already has frontmatter
324
+ if (content.startsWith('---')) {
325
+ const endOfFrontmatter = content.indexOf('---', 3);
326
+ if (endOfFrontmatter !== -1) {
327
+ let frontmatterBlock = content.substring(3, endOfFrontmatter).trim();
328
+ const body = content.substring(endOfFrontmatter + 3);
329
+
330
+ // Determine which layout to use
331
+ let targetLayout = layoutPath;
332
+ if (frontmatterBlock.includes('api:')) {
333
+ targetLayout = '../'.repeat(depth + 1) + 'layouts/APILayout.astro';
334
+ }
335
+
336
+ // Add locale to frontmatter if present
337
+ if (locale && !frontmatterBlock.includes('lang:')) {
338
+ frontmatterBlock = `lang: ${locale}\n${frontmatterBlock}`;
339
+ }
340
+
341
+ // Add version to frontmatter if present
342
+ if (version && !frontmatterBlock.includes('docVersion:')) {
343
+ frontmatterBlock = `docVersion: ${version}\n${frontmatterBlock}`;
344
+ }
345
+
346
+ // Check if layout is already specified
347
+ if (!frontmatterBlock.includes('layout:')) {
348
+ content = `---\n${frontmatterBlock}\nlayout: ${targetLayout}\n---${body}`;
349
+ } else {
350
+ content = `---\n${frontmatterBlock}\n---${body}`;
351
+ }
352
+ }
353
+ } else {
354
+ // No frontmatter, add minimal frontmatter with layout
355
+ const title = entry.name.replace(/\.(md|mdx)$/, '').replace(/-/g, ' ');
356
+ const langLine = locale ? `lang: ${locale}\n` : '';
357
+ const versionLine = version ? `docVersion: ${version}\n` : '';
358
+ content = `---\n${langLine}${versionLine}title: ${title}\nlayout: ${layoutPath}\n---\n\n${content}`;
359
+ }
360
+
361
+ await writeFile(fullPath, content, 'utf-8');
362
+ }
363
+ }));
364
+ }