cosmolo 0.3.3 → 0.3.4
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/dist/articles.d.ts +51 -0
- package/dist/articles.js +94 -0
- package/dist/categories.d.ts +7 -0
- package/dist/categories.js +26 -0
- package/dist/cli/generate.d.ts +1 -0
- package/dist/cli/generate.js +171 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +154 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/loaders.d.ts +105 -0
- package/dist/loaders.js +119 -0
- package/dist/markdown.d.ts +3 -0
- package/dist/markdown.js +69 -0
- package/dist/pages.d.ts +3 -0
- package/dist/pages.js +20 -0
- package/dist/plugin.d.ts +15 -0
- package/{src/plugin.ts → dist/plugin.js} +22 -30
- package/dist/types.d.ts +58 -0
- package/dist/types.js +2 -0
- package/package.json +12 -9
- package/src/articles.ts +0 -124
- package/src/categories.ts +0 -35
- package/src/cli/generate.ts +0 -190
- package/src/cli/index.ts +0 -22
- package/src/cli/init.ts +0 -175
- package/src/config.ts +0 -12
- package/src/index.ts +0 -34
- package/src/loaders.ts +0 -135
- package/src/markdown.ts +0 -74
- package/src/pages.ts +0 -23
- package/src/types.ts +0 -65
- /package/{src → dist}/virtual.d.ts +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Article, ResolvedCosmoloConfig } from './types.js';
|
|
3
|
+
export declare const articleFrontmatterSchema: z.ZodObject<{
|
|
4
|
+
title: z.ZodString;
|
|
5
|
+
category: z.ZodString;
|
|
6
|
+
excerpt: z.ZodString;
|
|
7
|
+
sort: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
date: z.ZodEffects<z.ZodDefault<z.ZodString>, string, unknown>;
|
|
9
|
+
tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
10
|
+
series: z.ZodOptional<z.ZodString>;
|
|
11
|
+
seriesOrder: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
draft: z.ZodDefault<z.ZodBoolean>;
|
|
13
|
+
related: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
title: string;
|
|
16
|
+
category: string;
|
|
17
|
+
excerpt: string;
|
|
18
|
+
sort: number;
|
|
19
|
+
date: string;
|
|
20
|
+
tags: string[];
|
|
21
|
+
draft: boolean;
|
|
22
|
+
related: string[];
|
|
23
|
+
series?: string | undefined;
|
|
24
|
+
seriesOrder?: number | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
title: string;
|
|
27
|
+
category: string;
|
|
28
|
+
excerpt: string;
|
|
29
|
+
sort?: number | undefined;
|
|
30
|
+
date?: unknown;
|
|
31
|
+
tags?: string[] | undefined;
|
|
32
|
+
series?: string | undefined;
|
|
33
|
+
seriesOrder?: number | undefined;
|
|
34
|
+
draft?: boolean | undefined;
|
|
35
|
+
related?: string[] | undefined;
|
|
36
|
+
}>;
|
|
37
|
+
/** Returns all non-draft articles sorted by `sort` descending. */
|
|
38
|
+
export declare function getArticles(config: ResolvedCosmoloConfig): Article[];
|
|
39
|
+
/** Returns all non-draft article slugs. */
|
|
40
|
+
export declare function getSlugs(config: ResolvedCosmoloConfig): string[];
|
|
41
|
+
/** Returns a single article with rendered HTML and TOC. Draft articles are included. */
|
|
42
|
+
export declare function getArticle(config: ResolvedCosmoloConfig, slug: string): Promise<Article>;
|
|
43
|
+
export declare function getArticlesByCategory(config: ResolvedCosmoloConfig, categorySlug: string, excludeSlug?: string): Article[];
|
|
44
|
+
export declare function getArticlesByTag(config: ResolvedCosmoloConfig, tag: string): Article[];
|
|
45
|
+
export declare function getArticlesBySeries(config: ResolvedCosmoloConfig, seriesKey: string): Article[];
|
|
46
|
+
export declare function getTags(config: ResolvedCosmoloConfig): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Returns the Svelte component constructor for an .svx article, or undefined
|
|
49
|
+
* if the article is a plain .md file. Safe to call from Svelte components.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getSvxComponent(config: ResolvedCosmoloConfig, slug: string): unknown | undefined;
|
package/dist/articles.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { renderMarkdown, generateToc } from './markdown.js';
|
|
4
|
+
import { isKnownCategory } from './categories.js';
|
|
5
|
+
import { rawMdFiles, svxModules } from 'cosmolo:content';
|
|
6
|
+
export const articleFrontmatterSchema = z.object({
|
|
7
|
+
title: z.string(),
|
|
8
|
+
category: z.string(),
|
|
9
|
+
excerpt: z.string(),
|
|
10
|
+
sort: z.number().default(0),
|
|
11
|
+
date: z.preprocess((val) => (val instanceof Date ? val.toISOString().split('T')[0] : val), z.string().default('')),
|
|
12
|
+
tags: z.array(z.string()).default([]),
|
|
13
|
+
series: z.string().optional(),
|
|
14
|
+
seriesOrder: z.number().optional(),
|
|
15
|
+
draft: z.boolean().default(false),
|
|
16
|
+
related: z.array(z.string()).default([]),
|
|
17
|
+
});
|
|
18
|
+
function slugFromPath(filePath, dir) {
|
|
19
|
+
const prefix = dir.replace(/^\//, '');
|
|
20
|
+
return filePath
|
|
21
|
+
.replace(new RegExp(`^/?${prefix}/`), '')
|
|
22
|
+
.replace(/\.(md|svx)$/, '');
|
|
23
|
+
}
|
|
24
|
+
/** Returns all non-draft articles sorted by `sort` descending. */
|
|
25
|
+
export function getArticles(config) {
|
|
26
|
+
const articles = [];
|
|
27
|
+
for (const [filePath, raw] of Object.entries(rawMdFiles)) {
|
|
28
|
+
const slug = slugFromPath(filePath, config.articlesDir);
|
|
29
|
+
const { data } = matter(raw);
|
|
30
|
+
const frontmatter = articleFrontmatterSchema.parse(data);
|
|
31
|
+
if (frontmatter.draft)
|
|
32
|
+
continue;
|
|
33
|
+
articles.push({ ...frontmatter, slug, html: '', markdown: '', toc: [] });
|
|
34
|
+
}
|
|
35
|
+
for (const [filePath, mod] of Object.entries(svxModules)) {
|
|
36
|
+
const slug = slugFromPath(filePath, config.articlesDir);
|
|
37
|
+
const frontmatter = articleFrontmatterSchema.parse(mod.metadata);
|
|
38
|
+
if (frontmatter.draft)
|
|
39
|
+
continue;
|
|
40
|
+
articles.push({ ...frontmatter, slug, html: '', markdown: '', toc: [] });
|
|
41
|
+
}
|
|
42
|
+
return articles.sort((a, b) => b.sort - a.sort);
|
|
43
|
+
}
|
|
44
|
+
/** Returns all non-draft article slugs. */
|
|
45
|
+
export function getSlugs(config) {
|
|
46
|
+
return getArticles(config).map((a) => a.slug);
|
|
47
|
+
}
|
|
48
|
+
/** Returns a single article with rendered HTML and TOC. Draft articles are included. */
|
|
49
|
+
export async function getArticle(config, slug) {
|
|
50
|
+
const dir = config.articlesDir.replace(/^\//, '');
|
|
51
|
+
const mdPath = `/${dir}/${slug}.md`;
|
|
52
|
+
const svxPath = `/${dir}/${slug}.svx`;
|
|
53
|
+
if (rawMdFiles[mdPath] !== undefined) {
|
|
54
|
+
const raw = rawMdFiles[mdPath];
|
|
55
|
+
const { data, content } = matter(raw);
|
|
56
|
+
const frontmatter = articleFrontmatterSchema.parse(data);
|
|
57
|
+
const html = await renderMarkdown(content);
|
|
58
|
+
const toc = generateToc(content);
|
|
59
|
+
return { ...frontmatter, slug, html, markdown: content, toc };
|
|
60
|
+
}
|
|
61
|
+
if (svxModules[svxPath] !== undefined) {
|
|
62
|
+
const frontmatter = articleFrontmatterSchema.parse(svxModules[svxPath].metadata);
|
|
63
|
+
return { ...frontmatter, slug, html: '', markdown: '', toc: [] };
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Article not found: ${slug}`);
|
|
66
|
+
}
|
|
67
|
+
export function getArticlesByCategory(config, categorySlug, excludeSlug) {
|
|
68
|
+
const all = getArticles(config);
|
|
69
|
+
const filtered = categorySlug === 'other'
|
|
70
|
+
? all.filter((a) => !isKnownCategory(config, a.category))
|
|
71
|
+
: all.filter((a) => a.category === categorySlug);
|
|
72
|
+
return excludeSlug ? filtered.filter((a) => a.slug !== excludeSlug) : filtered;
|
|
73
|
+
}
|
|
74
|
+
export function getArticlesByTag(config, tag) {
|
|
75
|
+
return getArticles(config).filter((a) => a.tags.includes(tag));
|
|
76
|
+
}
|
|
77
|
+
export function getArticlesBySeries(config, seriesKey) {
|
|
78
|
+
return getArticles(config)
|
|
79
|
+
.filter((a) => a.series === seriesKey)
|
|
80
|
+
.sort((a, b) => (a.seriesOrder ?? 0) - (b.seriesOrder ?? 0));
|
|
81
|
+
}
|
|
82
|
+
export function getTags(config) {
|
|
83
|
+
const tags = new Set();
|
|
84
|
+
getArticles(config).forEach((a) => a.tags.forEach((t) => tags.add(t)));
|
|
85
|
+
return Array.from(tags).sort();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns the Svelte component constructor for an .svx article, or undefined
|
|
89
|
+
* if the article is a plain .md file. Safe to call from Svelte components.
|
|
90
|
+
*/
|
|
91
|
+
export function getSvxComponent(config, slug) {
|
|
92
|
+
const dir = '/' + config.articlesDir.replace(/^\/|\/$/g, '');
|
|
93
|
+
return svxModules[`${dir}/${slug}.svx`]?.default;
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CategoryEntry, ResolvedCosmoloConfig, SiteConfig } from './types.js';
|
|
2
|
+
export declare function getAllCategories(_config: ResolvedCosmoloConfig): CategoryEntry[];
|
|
3
|
+
export declare function isKnownCategory(_config: ResolvedCosmoloConfig, key: string): boolean;
|
|
4
|
+
export declare function getCategoryLabel(_config: ResolvedCosmoloConfig, key: string): string;
|
|
5
|
+
export declare function getCategoryDescription(_config: ResolvedCosmoloConfig, key: string): string;
|
|
6
|
+
export declare function getCategorySlugs(_config: ResolvedCosmoloConfig): string[];
|
|
7
|
+
export declare function loadSiteConfig(_config: ResolvedCosmoloConfig): SiteConfig;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { categoriesData, siteConfigData } from 'cosmolo:content';
|
|
2
|
+
const map = categoriesData;
|
|
3
|
+
export function getAllCategories(_config) {
|
|
4
|
+
return Object.entries(map).map(([slug, { label, description }]) => ({
|
|
5
|
+
slug,
|
|
6
|
+
label,
|
|
7
|
+
description,
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
export function isKnownCategory(_config, key) {
|
|
11
|
+
return Object.prototype.hasOwnProperty.call(map, key);
|
|
12
|
+
}
|
|
13
|
+
export function getCategoryLabel(_config, key) {
|
|
14
|
+
if (Object.prototype.hasOwnProperty.call(map, key))
|
|
15
|
+
return map[key].label;
|
|
16
|
+
return siteConfigData.fallbackCategoryLabel;
|
|
17
|
+
}
|
|
18
|
+
export function getCategoryDescription(_config, key) {
|
|
19
|
+
return map[key]?.description ?? '';
|
|
20
|
+
}
|
|
21
|
+
export function getCategorySlugs(_config) {
|
|
22
|
+
return [...Object.keys(map), 'other'];
|
|
23
|
+
}
|
|
24
|
+
export function loadSiteConfig(_config) {
|
|
25
|
+
return siteConfigData;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(): Promise<void>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { DEFAULT_CONFIG } from '../config.js';
|
|
5
|
+
// ─── config ──────────────────────────────────────────────────────────────────
|
|
6
|
+
async function loadConfig() {
|
|
7
|
+
const root = process.cwd();
|
|
8
|
+
for (const name of ['cosmolo.config.ts', 'cosmolo.config.js']) {
|
|
9
|
+
const p = path.join(root, name);
|
|
10
|
+
if (fs.existsSync(p)) {
|
|
11
|
+
try {
|
|
12
|
+
const mod = await import(p);
|
|
13
|
+
return mod.default;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// fall through to defaults
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return DEFAULT_CONFIG;
|
|
21
|
+
}
|
|
22
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
function ask(rl, question, fallback = '') {
|
|
24
|
+
const hint = fallback ? ` [${fallback}]` : '';
|
|
25
|
+
return new Promise((resolve) => rl.question(` ${question}${hint}: `, (ans) => resolve(ans.trim() || fallback)));
|
|
26
|
+
}
|
|
27
|
+
function slugify(text) {
|
|
28
|
+
return text
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^\w\s-]/g, '')
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/[\s_]+/g, '-')
|
|
33
|
+
.replace(/-+/g, '-');
|
|
34
|
+
}
|
|
35
|
+
function today() {
|
|
36
|
+
return new Date().toISOString().split('T')[0];
|
|
37
|
+
}
|
|
38
|
+
function readCategories(config) {
|
|
39
|
+
const p = path.resolve(process.cwd(), config.categoriesConfigPath);
|
|
40
|
+
if (!fs.existsSync(p))
|
|
41
|
+
return {};
|
|
42
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
// ─── generators ──────────────────────────────────────────────────────────────
|
|
45
|
+
async function generateArticle(rl, config) {
|
|
46
|
+
console.log('\nGenerate Article\n');
|
|
47
|
+
const title = await ask(rl, 'Title');
|
|
48
|
+
if (!title) {
|
|
49
|
+
console.error('\nError: Title is required.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const defaultSlug = slugify(title);
|
|
53
|
+
const slug = await ask(rl, 'Slug', defaultSlug);
|
|
54
|
+
const articlesDir = path.resolve(process.cwd(), config.articlesDir);
|
|
55
|
+
const filePath = path.join(articlesDir, `${slug}.md`);
|
|
56
|
+
if (fs.existsSync(filePath)) {
|
|
57
|
+
console.error(`\nError: ${config.articlesDir}/${slug}.md already exists.`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const categories = readCategories(config);
|
|
61
|
+
const catKeys = Object.keys(categories);
|
|
62
|
+
if (catKeys.length > 0)
|
|
63
|
+
console.log(`\n Categories: ${catKeys.join(', ')}`);
|
|
64
|
+
const category = await ask(rl, 'Category', catKeys[0] ?? 'other');
|
|
65
|
+
const excerpt = await ask(rl, 'Excerpt', '');
|
|
66
|
+
const tagsRaw = await ask(rl, 'Tags (comma-separated)', '');
|
|
67
|
+
const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
68
|
+
const sort = await ask(rl, 'Sort', '0');
|
|
69
|
+
const date = await ask(rl, 'Date', today());
|
|
70
|
+
const draftAns = await ask(rl, 'Draft? (y/N)', 'N');
|
|
71
|
+
const draft = draftAns.toLowerCase() === 'y';
|
|
72
|
+
const series = await ask(rl, 'Series (optional)', '');
|
|
73
|
+
const seriesOrder = series ? await ask(rl, 'Series order', '1') : '';
|
|
74
|
+
const lines = [
|
|
75
|
+
'---',
|
|
76
|
+
`title: "${title}"`,
|
|
77
|
+
`category: "${category}"`,
|
|
78
|
+
`excerpt: "${excerpt}"`,
|
|
79
|
+
`sort: ${parseInt(sort, 10) || 0}`,
|
|
80
|
+
`date: "${date}"`,
|
|
81
|
+
];
|
|
82
|
+
if (tags.length > 0)
|
|
83
|
+
lines.push(`tags: [${tags.map((t) => `"${t}"`).join(', ')}]`);
|
|
84
|
+
if (series) {
|
|
85
|
+
lines.push(`series: "${series}"`);
|
|
86
|
+
lines.push(`seriesOrder: ${parseInt(seriesOrder, 10) || 1}`);
|
|
87
|
+
}
|
|
88
|
+
if (draft)
|
|
89
|
+
lines.push('draft: true');
|
|
90
|
+
lines.push('---', '', '');
|
|
91
|
+
fs.mkdirSync(articlesDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
93
|
+
console.log(`\nCreated: ${config.articlesDir}/${slug}.md`);
|
|
94
|
+
}
|
|
95
|
+
async function generatePage(rl, config) {
|
|
96
|
+
console.log('\nGenerate Page\n');
|
|
97
|
+
const title = await ask(rl, 'Title');
|
|
98
|
+
if (!title) {
|
|
99
|
+
console.error('\nError: Title is required.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const defaultSlug = slugify(title);
|
|
103
|
+
const slug = await ask(rl, 'Slug', defaultSlug);
|
|
104
|
+
const pagesDir = path.resolve(process.cwd(), config.pagesDir);
|
|
105
|
+
const filePath = path.join(pagesDir, `${slug}.md`);
|
|
106
|
+
if (fs.existsSync(filePath)) {
|
|
107
|
+
console.error(`\nError: ${config.pagesDir}/${slug}.md already exists.`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const content = ['---', `title: "${title}"`, '---', '', ''].join('\n');
|
|
111
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
112
|
+
fs.writeFileSync(filePath, content);
|
|
113
|
+
console.log(`\nCreated: ${config.pagesDir}/${slug}.md`);
|
|
114
|
+
}
|
|
115
|
+
async function generateCategory(rl, config) {
|
|
116
|
+
console.log('\nGenerate Category\n');
|
|
117
|
+
const key = await ask(rl, 'Key (slug)');
|
|
118
|
+
if (!key) {
|
|
119
|
+
console.error('\nError: Key is required.');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const categoriesPath = path.resolve(process.cwd(), config.categoriesConfigPath);
|
|
123
|
+
const categories = readCategories(config);
|
|
124
|
+
if (categories[key]) {
|
|
125
|
+
console.error(`\nError: Category "${key}" already exists.`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const label = await ask(rl, 'Label');
|
|
129
|
+
const description = await ask(rl, 'Description', '');
|
|
130
|
+
categories[key] = { label, description };
|
|
131
|
+
fs.mkdirSync(path.dirname(categoriesPath), { recursive: true });
|
|
132
|
+
fs.writeFileSync(categoriesPath, JSON.stringify(categories, null, '\t') + '\n');
|
|
133
|
+
console.log(`\nAdded: "${key}" to ${config.categoriesConfigPath}`);
|
|
134
|
+
}
|
|
135
|
+
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
136
|
+
export async function main() {
|
|
137
|
+
const config = await loadConfig();
|
|
138
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
139
|
+
const subcommand = process.argv[3]; // argv[2] is "generate"
|
|
140
|
+
const commands = {
|
|
141
|
+
article: generateArticle,
|
|
142
|
+
page: generatePage,
|
|
143
|
+
category: generateCategory,
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
if (subcommand && commands[subcommand]) {
|
|
147
|
+
await commands[subcommand](rl, config);
|
|
148
|
+
}
|
|
149
|
+
else if (subcommand) {
|
|
150
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
151
|
+
console.error('Available: article, page, category');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
console.log('Cosmolo Generator\n');
|
|
156
|
+
console.log(' 1. Article');
|
|
157
|
+
console.log(' 2. Page');
|
|
158
|
+
console.log(' 3. Category');
|
|
159
|
+
const choice = await new Promise((resolve) => rl.question('\nWhat would you like to generate? (1/2/3): ', (ans) => resolve(ans.trim())));
|
|
160
|
+
const picked = { '1': generateArticle, '2': generatePage, '3': generateCategory }[choice];
|
|
161
|
+
if (!picked) {
|
|
162
|
+
console.error('Invalid choice.');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
await picked(rl, config);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
rl.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
const cmd = process.argv[2];
|
|
3
|
+
switch (cmd) {
|
|
4
|
+
case 'init':
|
|
5
|
+
await (await import('./init.js')).main();
|
|
6
|
+
break;
|
|
7
|
+
case 'generate':
|
|
8
|
+
await (await import('./generate.js')).main();
|
|
9
|
+
break;
|
|
10
|
+
default: {
|
|
11
|
+
const isUnknown = Boolean(cmd);
|
|
12
|
+
if (isUnknown)
|
|
13
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
14
|
+
console.log('Usage: cosmolo <command>\n');
|
|
15
|
+
console.log('Commands:');
|
|
16
|
+
console.log(' init Scaffold routes into an existing SvelteKit project');
|
|
17
|
+
console.log(' generate [article|page|category] Create content files');
|
|
18
|
+
process.exit(isUnknown ? 1 : 0);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(): Promise<void>;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const TEMPLATE_DIR = path.resolve(__dirname, '../../templates');
|
|
8
|
+
function ask(rl, question) {
|
|
9
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
10
|
+
}
|
|
11
|
+
function copyFile(src, dest) {
|
|
12
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
13
|
+
fs.copyFileSync(src, dest);
|
|
14
|
+
}
|
|
15
|
+
function writeFile(dest, content) {
|
|
16
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
17
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
function collectFiles(dir) {
|
|
20
|
+
const results = [];
|
|
21
|
+
function walk(current) {
|
|
22
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
23
|
+
const full = path.join(current, entry.name);
|
|
24
|
+
if (entry.isDirectory())
|
|
25
|
+
walk(full);
|
|
26
|
+
else
|
|
27
|
+
results.push([full, path.relative(dir, full)]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
walk(dir);
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
function destPath(relativePath, projectRoot) {
|
|
34
|
+
const mapped = relativePath
|
|
35
|
+
.replace(/^routes\//, 'src/routes/')
|
|
36
|
+
.replace(/^lib\//, 'src/lib/');
|
|
37
|
+
return path.join(projectRoot, mapped);
|
|
38
|
+
}
|
|
39
|
+
function injectPackageScripts(projectRoot) {
|
|
40
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
41
|
+
if (!fs.existsSync(pkgPath))
|
|
42
|
+
return;
|
|
43
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
44
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
45
|
+
let added = false;
|
|
46
|
+
const scripts = {
|
|
47
|
+
'generate:article': 'cosmolo generate article',
|
|
48
|
+
'generate:page': 'cosmolo generate page',
|
|
49
|
+
'generate:category': 'cosmolo generate category',
|
|
50
|
+
};
|
|
51
|
+
for (const [key, val] of Object.entries(scripts)) {
|
|
52
|
+
if (!pkg.scripts[key]) {
|
|
53
|
+
pkg.scripts[key] = val;
|
|
54
|
+
added = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (added) {
|
|
58
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n');
|
|
59
|
+
console.log(' updated package.json (added generate:* scripts)');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
63
|
+
export async function main() {
|
|
64
|
+
const PROJECT_ROOT = process.cwd();
|
|
65
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
66
|
+
console.log('\ncosmolo init\n');
|
|
67
|
+
// ── Mode selection ──────────────────────────────────────────────────────
|
|
68
|
+
console.log('Choose an initialization mode:\n');
|
|
69
|
+
console.log(' 1) Full — server routes + Svelte page components');
|
|
70
|
+
console.log(' 2) Slim — server routes only (bring your own Svelte UI)\n');
|
|
71
|
+
let modeRaw = '';
|
|
72
|
+
while (!['1', '2'].includes(modeRaw)) {
|
|
73
|
+
modeRaw = (await ask(rl, 'Mode [1/2]: ')).trim();
|
|
74
|
+
if (!['1', '2'].includes(modeRaw))
|
|
75
|
+
console.log(' Please enter 1 or 2.');
|
|
76
|
+
}
|
|
77
|
+
const mode = modeRaw === '1' ? 'full' : 'slim';
|
|
78
|
+
// ── Adapter selection ───────────────────────────────────────────────────
|
|
79
|
+
console.log('\nChoose your deployment adapter:\n');
|
|
80
|
+
console.log(' 1) SSG — @sveltejs/adapter-static (Cloudflare Pages static, GitHub Pages, etc.)');
|
|
81
|
+
console.log(' 2) Serverless/SSR — Cloudflare Workers, Vercel, Node, etc.\n');
|
|
82
|
+
let adapterRaw = '';
|
|
83
|
+
while (!['1', '2'].includes(adapterRaw)) {
|
|
84
|
+
adapterRaw = (await ask(rl, 'Adapter [1/2]: ')).trim();
|
|
85
|
+
if (!['1', '2'].includes(adapterRaw))
|
|
86
|
+
console.log(' Please enter 1 or 2.');
|
|
87
|
+
}
|
|
88
|
+
const isSSG = adapterRaw === '1';
|
|
89
|
+
// ── Collect files ───────────────────────────────────────────────────────
|
|
90
|
+
const sharedFiles = collectFiles(path.join(TEMPLATE_DIR, 'shared'));
|
|
91
|
+
const fullFiles = mode === 'full' ? collectFiles(path.join(TEMPLATE_DIR, 'full')) : [];
|
|
92
|
+
const allFiles = [
|
|
93
|
+
...sharedFiles.map(([src, rel]) => [src, rel]),
|
|
94
|
+
...fullFiles.map(([src, rel]) => [src, rel]),
|
|
95
|
+
];
|
|
96
|
+
const layoutTsPath = path.join(PROJECT_ROOT, 'src/routes/+layout.ts');
|
|
97
|
+
const layoutTsContent = 'export const prerender = true;\n';
|
|
98
|
+
// ── Conflict detection ──────────────────────────────────────────────────
|
|
99
|
+
const conflicts = [];
|
|
100
|
+
for (const [, rel] of allFiles) {
|
|
101
|
+
const dest = destPath(rel, PROJECT_ROOT);
|
|
102
|
+
if (fs.existsSync(dest))
|
|
103
|
+
conflicts.push(path.relative(PROJECT_ROOT, dest));
|
|
104
|
+
}
|
|
105
|
+
if (isSSG && fs.existsSync(layoutTsPath)) {
|
|
106
|
+
conflicts.push(path.relative(PROJECT_ROOT, layoutTsPath));
|
|
107
|
+
}
|
|
108
|
+
if (conflicts.length > 0) {
|
|
109
|
+
console.log('\nThe following files already exist:\n');
|
|
110
|
+
for (const f of conflicts)
|
|
111
|
+
console.log(` ${f}`);
|
|
112
|
+
let overwriteRaw = '';
|
|
113
|
+
while (!['y', 'n'].includes(overwriteRaw)) {
|
|
114
|
+
overwriteRaw = (await ask(rl, '\nOverwrite all? [y/N]: ')).trim().toLowerCase() || 'n';
|
|
115
|
+
if (!['y', 'n'].includes(overwriteRaw))
|
|
116
|
+
console.log(' Please enter y or n.');
|
|
117
|
+
}
|
|
118
|
+
if (overwriteRaw === 'n') {
|
|
119
|
+
console.log('\nAborted. No files were written.\n');
|
|
120
|
+
rl.close();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
rl.close();
|
|
125
|
+
// ── Write files ─────────────────────────────────────────────────────────
|
|
126
|
+
for (const [src, rel] of allFiles) {
|
|
127
|
+
const dest = destPath(rel, PROJECT_ROOT);
|
|
128
|
+
copyFile(src, dest);
|
|
129
|
+
console.log(` created ${path.relative(PROJECT_ROOT, dest)}`);
|
|
130
|
+
}
|
|
131
|
+
if (isSSG) {
|
|
132
|
+
writeFile(layoutTsPath, layoutTsContent);
|
|
133
|
+
console.log(` created src/routes/+layout.ts`);
|
|
134
|
+
}
|
|
135
|
+
injectPackageScripts(PROJECT_ROOT);
|
|
136
|
+
// ── Next steps ──────────────────────────────────────────────────────────
|
|
137
|
+
console.log('\nDone! Next steps:\n');
|
|
138
|
+
console.log(' 1. Install cosmolo: npm install cosmolo (or bun add cosmolo)');
|
|
139
|
+
if (isSSG) {
|
|
140
|
+
console.log(' 2. Install adapter: npm install -D @sveltejs/adapter-static');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(' 2. Install adapter: npm install -D @sveltejs/adapter-cloudflare (or your adapter)');
|
|
144
|
+
}
|
|
145
|
+
if (mode === 'full') {
|
|
146
|
+
console.log(' 3. Install sass: npm install -D sass (SCSS used in Svelte templates)');
|
|
147
|
+
console.log(' 4. Run: npm run dev');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(' 3. Add your own +page.svelte files for each route.');
|
|
151
|
+
console.log(' 4. Run: npm run dev');
|
|
152
|
+
}
|
|
153
|
+
console.log('\n See https://github.com/alcogy/cosmolo for full documentation.\n');
|
|
154
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const DEFAULT_CONFIG = {
|
|
2
|
+
articlesDir: 'src/content/articles',
|
|
3
|
+
pagesDir: 'src/content/pages',
|
|
4
|
+
siteConfigPath: 'config/site.json',
|
|
5
|
+
categoriesConfigPath: 'config/categories.json',
|
|
6
|
+
};
|
|
7
|
+
export function resolveConfig(userConfig = {}) {
|
|
8
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { CosmoloConfig, ResolvedCosmoloConfig, TocEntry, ArticleFrontmatter, Article, Page, CategoryEntry, SiteConfig, } from './types.js';
|
|
2
|
+
export { DEFAULT_CONFIG, resolveConfig } from './config.js';
|
|
3
|
+
export { getAllCategories, isKnownCategory, getCategoryLabel, getCategoryDescription, getCategorySlugs, loadSiteConfig, } from './categories.js';
|
|
4
|
+
export { articleFrontmatterSchema, getArticles, getArticle, getSlugs, getArticlesByCategory, getArticlesByTag, getArticlesBySeries, getTags, getSvxComponent } from './articles.js';
|
|
5
|
+
export { getPageSlugs, getPage } from './pages.js';
|
|
6
|
+
export { renderMarkdown, generateToc } from './markdown.js';
|
|
7
|
+
export { createArticlesLoader, createArticleLoader, createCategoryLoader, createTagLoader, createPageLoader, createArticleEntries, createCategoryEntries, createTagEntries, createPageEntries, } from './loaders.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DEFAULT_CONFIG, resolveConfig } from './config.js';
|
|
2
|
+
export { getAllCategories, isKnownCategory, getCategoryLabel, getCategoryDescription, getCategorySlugs, loadSiteConfig, } from './categories.js';
|
|
3
|
+
export { articleFrontmatterSchema, getArticles, getArticle, getSlugs, getArticlesByCategory, getArticlesByTag, getArticlesBySeries, getTags, getSvxComponent } from './articles.js';
|
|
4
|
+
export { getPageSlugs, getPage } from './pages.js';
|
|
5
|
+
export { renderMarkdown, generateToc } from './markdown.js';
|
|
6
|
+
export { createArticlesLoader, createArticleLoader, createCategoryLoader, createTagLoader, createPageLoader, createArticleEntries, createCategoryEntries, createTagEntries, createPageEntries, } from './loaders.js';
|