@karaoke-cms/astro 0.9.7 → 0.10.3
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 +75 -1
- package/client.d.ts +14 -1
- package/package.json +5 -4
- package/src/codegen/generate-docs-instance.ts +210 -0
- package/src/collections.ts +33 -1
- package/src/components/Menu.astro +1 -1
- package/src/components/regions/RecentPosts.astro +7 -4
- package/src/components/regions/SiteFooter.astro +3 -1
- package/src/define-module.ts +3 -48
- package/src/define-theme.ts +3 -34
- package/src/index.ts +190 -29
- package/src/pages/rss.xml.ts +17 -10
- package/src/remark/remark-obsidian-embeds.ts +260 -0
- package/src/types.ts +40 -155
- package/src/utils/resolve-menus.ts +6 -3
- package/src/validate-config.js +83 -7
package/src/types.ts
CHANGED
|
@@ -1,155 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Theme — package name string (legacy) or a ThemeInstance from defineTheme(). */
|
|
42
|
-
theme?: string | ThemeInstance;
|
|
43
|
-
/** Modules to activate. Each is a ModuleInstance from defineModule(). */
|
|
44
|
-
modules?: ModuleInstance[];
|
|
45
|
-
/** Giscus comments configuration. */
|
|
46
|
-
comments?: CommentsConfig;
|
|
47
|
-
layout?: {
|
|
48
|
-
regions?: {
|
|
49
|
-
top?: { components?: RegionComponent[] };
|
|
50
|
-
left?: { components?: RegionComponent[] };
|
|
51
|
-
right?: { components?: RegionComponent[] };
|
|
52
|
-
bottom?: { components?: RegionComponent[] };
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Resolved (defaults filled in) modules config — available at build time via virtual module. */
|
|
58
|
-
export interface ResolvedModules {
|
|
59
|
-
comments: {
|
|
60
|
-
enabled: boolean;
|
|
61
|
-
repo: string;
|
|
62
|
-
repoId: string;
|
|
63
|
-
category: string;
|
|
64
|
-
categoryId: string;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Resolved (defaults filled in) layout config — available at build time via virtual module. */
|
|
69
|
-
export interface ResolvedLayout {
|
|
70
|
-
regions: {
|
|
71
|
-
top: { components: RegionComponent[] };
|
|
72
|
-
left: { components: RegionComponent[] };
|
|
73
|
-
right: { components: RegionComponent[] };
|
|
74
|
-
bottom: { components: RegionComponent[] };
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Menu types ─────────────────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
/** Raw shape of one entry in menus.yaml (used to type the parsed YAML). */
|
|
81
|
-
export interface MenuEntryConfig {
|
|
82
|
-
text: string;
|
|
83
|
-
href?: string;
|
|
84
|
-
weight?: number;
|
|
85
|
-
/** Visibility condition — only 'collection:name' supported in v1. */
|
|
86
|
-
when?: string;
|
|
87
|
-
entries?: MenuEntryConfig[];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Raw shape of one menu block in menus.yaml. */
|
|
91
|
-
export interface MenuConfig {
|
|
92
|
-
orientation?: 'horizontal' | 'vertical';
|
|
93
|
-
entries?: MenuEntryConfig[];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface ResolvedMenuEntry {
|
|
97
|
-
text: string;
|
|
98
|
-
href?: string;
|
|
99
|
-
weight: number;
|
|
100
|
-
when?: string;
|
|
101
|
-
entries: ResolvedMenuEntry[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface ResolvedMenu {
|
|
105
|
-
name: string;
|
|
106
|
-
orientation: 'horizontal' | 'vertical';
|
|
107
|
-
entries: ResolvedMenuEntry[];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export type ResolvedMenus = Record<string, ResolvedMenu>;
|
|
111
|
-
|
|
112
|
-
// ── Module system types ────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
/** A menu entry registered by a module instance. */
|
|
115
|
-
export interface ModuleMenuEntry {
|
|
116
|
-
id: string;
|
|
117
|
-
name: string;
|
|
118
|
-
path: string;
|
|
119
|
-
section: string;
|
|
120
|
-
weight: number;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** A resolved module instance — returned by a defineModule() factory. */
|
|
124
|
-
export interface ModuleInstance {
|
|
125
|
-
_type: 'module-instance';
|
|
126
|
-
id: string;
|
|
127
|
-
mount: string;
|
|
128
|
-
/** When false, karaoke() skips route injection and menu entries for this module. Defaults to true. */
|
|
129
|
-
enabled: boolean;
|
|
130
|
-
routes: Array<{ pattern: string; entrypoint: string }>;
|
|
131
|
-
menuEntries: ModuleMenuEntry[];
|
|
132
|
-
cssContract: readonly string[];
|
|
133
|
-
hasDefaultCss: boolean;
|
|
134
|
-
/**
|
|
135
|
-
* Page files to copy into the user's src/pages/ on first dev run.
|
|
136
|
-
* Only copied if the destination does not already exist.
|
|
137
|
-
* src: absolute path to the source .astro file in the npm package.
|
|
138
|
-
* dest: path relative to src/pages/ (e.g. "blog/index.astro").
|
|
139
|
-
*/
|
|
140
|
-
scaffoldPages?: Array<{ src: string; dest: string }>;
|
|
141
|
-
/**
|
|
142
|
-
* Absolute path to the module's default CSS file.
|
|
143
|
-
* On first dev run, copied to src/styles/{id}.css and imported into global.css.
|
|
144
|
-
*/
|
|
145
|
-
defaultCssPath?: string;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** A resolved theme instance — returned by a defineTheme() factory. */
|
|
149
|
-
export interface ThemeInstance {
|
|
150
|
-
_type: 'theme-instance';
|
|
151
|
-
id: string;
|
|
152
|
-
implementedModuleIds: string[];
|
|
153
|
-
implementedModules: ModuleInstance[];
|
|
154
|
-
toAstroIntegration: () => import('astro').AstroIntegration;
|
|
155
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* All shared types now live in @karaoke-cms/contracts.
|
|
3
|
+
*
|
|
4
|
+
* This file re-exports them for backwards compatibility — internal callers
|
|
5
|
+
* that import from './types.js' continue to work unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
// CSS contract
|
|
9
|
+
CssContractSlot,
|
|
10
|
+
CssContract,
|
|
11
|
+
// Primitive named types
|
|
12
|
+
RouteDefinition,
|
|
13
|
+
CollectionSpec,
|
|
14
|
+
ScaffoldPage,
|
|
15
|
+
// Module system
|
|
16
|
+
ModuleMenuEntry,
|
|
17
|
+
ModuleInstance,
|
|
18
|
+
// Theme system
|
|
19
|
+
ThemeInstance,
|
|
20
|
+
ThemeFactoryConfig,
|
|
21
|
+
// Factory definitions
|
|
22
|
+
ModuleDefinition,
|
|
23
|
+
ThemeDefinition,
|
|
24
|
+
// Configuration
|
|
25
|
+
RegionComponent,
|
|
26
|
+
CollectionMode,
|
|
27
|
+
CollectionConfig,
|
|
28
|
+
ResolvedCollection,
|
|
29
|
+
ResolvedCollections,
|
|
30
|
+
CommentsConfig,
|
|
31
|
+
KaraokeConfig,
|
|
32
|
+
ResolvedModules,
|
|
33
|
+
ResolvedLayout,
|
|
34
|
+
// Menu
|
|
35
|
+
MenuEntryConfig,
|
|
36
|
+
MenuConfig,
|
|
37
|
+
ResolvedMenuEntry,
|
|
38
|
+
ResolvedMenu,
|
|
39
|
+
ResolvedMenus,
|
|
40
|
+
} from '@karaoke-cms/contracts';
|
|
@@ -38,10 +38,13 @@ function defaultMain(modules: ModuleInstance[]): ResolvedMenu {
|
|
|
38
38
|
}))
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// Only inject the hardcoded Docs fallback when no module owns a docs collection.
|
|
42
|
+
// When modules supply their own docs sections (via docs() factory), those appear
|
|
43
|
+
// in moduleMainEntries and the hardcoded entry would produce a duplicate.
|
|
44
|
+
const hasDocsModule = modules.some(m => m.collection != null);
|
|
42
45
|
const themeEntries: ResolvedMenuEntry[] = [
|
|
43
|
-
{ text: 'Docs', href: '/docs', weight: 20, when: 'collection:docs', entries: [] },
|
|
44
|
-
{ text: 'Tags', href: '/tags', weight: 30, entries: [] },
|
|
46
|
+
...(!hasDocsModule ? [{ text: 'Docs', href: '/docs', weight: 20, when: 'collection:docs', entries: [] as ResolvedMenuEntry[] }] : []),
|
|
47
|
+
{ text: 'Tags', href: '/tags', weight: 30, entries: [] as ResolvedMenuEntry[] },
|
|
45
48
|
];
|
|
46
49
|
|
|
47
50
|
// If no modules provide main entries, fall back to hardcoded Blog default.
|
package/src/validate-config.js
CHANGED
|
@@ -6,20 +6,96 @@
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Validate module config at build time.
|
|
9
|
-
* Throws a clear error if comments is enabled but required fields are missing
|
|
9
|
+
* Throws a clear error if comments is enabled but required fields are missing,
|
|
10
|
+
* or if multiple docs instances share the same id, mount, folder, or collection name.
|
|
10
11
|
*
|
|
11
12
|
* @param {import('../karaoke.config').KaraokeConfig | null | undefined} config
|
|
12
13
|
*/
|
|
13
14
|
export function validateModules(config) {
|
|
14
15
|
const comments = config?.comments
|
|
15
|
-
if (
|
|
16
|
+
if (comments?.enabled) {
|
|
17
|
+
const required = ['repo', 'repoId', 'category', 'categoryId']
|
|
18
|
+
const missing = required.filter(k => !comments[k])
|
|
19
|
+
if (missing.length > 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`karaoke-cms: comments.enabled is true but the following required fields are missing: ` +
|
|
22
|
+
`${missing.join(', ')}. Get these values from https://giscus.app`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Gather all active docs instances from config.modules and theme.implementedModules.
|
|
28
|
+
const configModules = config?.modules ?? []
|
|
29
|
+
const themeModules =
|
|
30
|
+
config?.theme?._type === 'theme-instance'
|
|
31
|
+
? (config.theme.implementedModules ?? [])
|
|
32
|
+
: []
|
|
33
|
+
|
|
34
|
+
const configDocs = configModules.filter(m => m.collection != null && m.enabled !== false)
|
|
35
|
+
const themeDocs = themeModules.filter(m => m.collection != null && m.enabled !== false)
|
|
36
|
+
|
|
37
|
+
// Check for duplicate ids WITHIN each source — an id appearing twice in config.modules
|
|
38
|
+
// (or twice in theme.implementedModules) is always an error.
|
|
39
|
+
function checkDupIds(arr, source) {
|
|
40
|
+
const ids = arr.map(m => m.id)
|
|
41
|
+
const dups = ids.filter((v, i) => ids.indexOf(v) !== i)
|
|
42
|
+
if (dups.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`karaoke-cms: duplicate docs module id(s) in ${source}: ${[...new Set(dups)].join(', ')}. ` +
|
|
45
|
+
`Each docs() instance must have a unique id.`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
checkDupIds(configDocs, 'config.modules')
|
|
50
|
+
checkDupIds(themeDocs, 'theme.implementedModules')
|
|
51
|
+
|
|
52
|
+
// Combine with dedup by id — config wins (same as runtime).
|
|
53
|
+
// Cross-source duplicates (same id in config AND theme) are silently merged.
|
|
54
|
+
const seenIds = new Set()
|
|
55
|
+
const allDocsInstances = [...configDocs, ...themeDocs].filter(m => {
|
|
56
|
+
if (seenIds.has(m.id)) return false
|
|
57
|
+
seenIds.add(m.id)
|
|
58
|
+
return true
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Validate mount paths for all instances (single or multiple).
|
|
62
|
+
const MOUNT_RE = /^\/[a-z0-9\-_/]*$/i
|
|
63
|
+
for (const inst of allDocsInstances) {
|
|
64
|
+
if (!MOUNT_RE.test(inst.mount)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`karaoke-cms: docs() mount "${inst.mount}" contains invalid characters. ` +
|
|
67
|
+
`Use a simple path like "/docs" or "/api-docs" (no Astro route syntax, no trailing slash).`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (allDocsInstances.length < 2) return
|
|
73
|
+
|
|
74
|
+
const mounts = allDocsInstances.map(m => m.mount)
|
|
75
|
+
const folders = allDocsInstances.map(m => m.collection.folder)
|
|
76
|
+
const collectionNames = allDocsInstances.map(m => m.collection.name)
|
|
77
|
+
|
|
78
|
+
const dupMounts = mounts.filter((v, i) => mounts.indexOf(v) !== i)
|
|
79
|
+
if (dupMounts.length > 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`karaoke-cms: duplicate docs module mount(s): ${[...new Set(dupMounts)].join(', ')}. ` +
|
|
82
|
+
`Each docs() instance must have a unique mount path.`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dupFolders = folders.filter((v, i) => folders.indexOf(v) !== i)
|
|
87
|
+
if (dupFolders.length > 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`karaoke-cms: duplicate docs module folder(s): ${[...new Set(dupFolders)].join(', ')}. ` +
|
|
90
|
+
`Each docs() instance must read from a different vault folder.`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
16
93
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (missing.length > 0) {
|
|
94
|
+
const dupNames = collectionNames.filter((v, i) => collectionNames.indexOf(v) !== i)
|
|
95
|
+
if (dupNames.length > 0) {
|
|
20
96
|
throw new Error(
|
|
21
|
-
`karaoke-cms:
|
|
22
|
-
|
|
97
|
+
`karaoke-cms: duplicate docs collection name(s): ${[...new Set(dupNames)].join(', ')}. ` +
|
|
98
|
+
`Each docs() instance must use a unique collection name.`
|
|
23
99
|
)
|
|
24
100
|
}
|
|
25
101
|
}
|