@karaoke-cms/astro 0.9.0 → 0.9.1
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/package.json +2 -2
- package/src/components/regions/SiteFooter.astro +17 -3
- package/src/define-module.ts +38 -0
- package/src/define-theme.ts +37 -0
- package/src/index.ts +41 -21
- package/src/types.ts +32 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karaoke-cms/astro",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.1",
|
|
5
5
|
"description": "Core Astro integration for karaoke-cms — virtual config, wikilinks, handbook routes",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
"astro": "^6.0.8"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test": "vitest run test/validate-config.test.js test/theme-loading.test.js test/resolve-modules.test.js test/resolve-layout.test.js test/resolve-collections.test.js test/collections.test.js test/load-env.test.js test/resolve-menus.test.js"
|
|
44
|
+
"test": "vitest run test/validate-config.test.js test/theme-loading.test.js test/resolve-modules.test.js test/resolve-layout.test.js test/resolve-collections.test.js test/collections.test.js test/load-env.test.js test/resolve-menus.test.js test/define-module.test.js test/define-theme.test.js"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Menu from '../Menu.astro';
|
|
3
|
+
import { siteTitle } from 'virtual:karaoke-cms/config';
|
|
3
4
|
---
|
|
4
|
-
<
|
|
5
|
-
<div class="footer-
|
|
6
|
-
|
|
5
|
+
<div class="footer-grid">
|
|
6
|
+
<div class="footer-col">
|
|
7
|
+
<p class="footer-brand">karaoke-cms</p>
|
|
8
|
+
<p class="footer-tagline">An Astro framework for publishing Obsidian vaults as private-by-default static sites.</p>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="footer-col">
|
|
11
|
+
<Menu name="footer-2" />
|
|
12
|
+
</div>
|
|
13
|
+
<div class="footer-col"></div>
|
|
14
|
+
<div class="footer-col">
|
|
15
|
+
<Menu name="footer" />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="footer-below">
|
|
19
|
+
<span>© {new Date().getFullYear()} {siteTitle}</span>
|
|
20
|
+
<span class="footer-attr">Built with <a href="https://karaoke-cms.org">karaoke-cms</a></span>
|
|
7
21
|
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ModuleInstance, ModuleMenuEntry } from './types.js';
|
|
2
|
+
|
|
3
|
+
export interface ModuleDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
cssContract: readonly string[];
|
|
6
|
+
defaultCss?: () => Promise<unknown>;
|
|
7
|
+
routes: (mount: string) => Array<{ pattern: string; entrypoint: string }>;
|
|
8
|
+
menuEntries: (mount: string, id: string) => ModuleMenuEntry[];
|
|
9
|
+
collection?: () => unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Define a karaoke-cms module.
|
|
14
|
+
*
|
|
15
|
+
* Returns a factory function. Call the factory with `{ mount }` (and optionally
|
|
16
|
+
* `{ id }` for multi-instance support) to get a ModuleInstance that can be
|
|
17
|
+
* passed to `defineConfig({ modules: [...] })`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* export const blog = defineModule({ id: 'blog', routes: (mount) => [...], ... })
|
|
21
|
+
* // In karaoke.config.ts:
|
|
22
|
+
* modules: [blog({ mount: '/blog' })]
|
|
23
|
+
*/
|
|
24
|
+
export function defineModule(def: ModuleDefinition) {
|
|
25
|
+
return function moduleFactory(config: { id?: string; mount: string }): ModuleInstance {
|
|
26
|
+
const id = config.id ?? def.id;
|
|
27
|
+
const mount = config.mount.replace(/\/$/, '');
|
|
28
|
+
return {
|
|
29
|
+
_type: 'module-instance',
|
|
30
|
+
id,
|
|
31
|
+
mount,
|
|
32
|
+
routes: def.routes(mount),
|
|
33
|
+
menuEntries: def.menuEntries(mount, id),
|
|
34
|
+
cssContract: def.cssContract,
|
|
35
|
+
hasDefaultCss: !!def.defaultCss,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import type { ModuleInstance, ThemeInstance } from './types.js';
|
|
3
|
+
|
|
4
|
+
export interface ThemeFactoryConfig {
|
|
5
|
+
implements?: ModuleInstance[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ThemeDefinition {
|
|
9
|
+
id: string;
|
|
10
|
+
toAstroIntegration: (config: ThemeFactoryConfig) => AstroIntegration;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Define a karaoke-cms theme.
|
|
15
|
+
*
|
|
16
|
+
* Returns a factory function. Call the factory with `{ implements: [...] }` to
|
|
17
|
+
* declare which module instances this theme provides CSS for. The result is a
|
|
18
|
+
* ThemeInstance that can be passed to `defineConfig({ theme: ... })`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* export const themeDefault = defineTheme({
|
|
22
|
+
* id: 'theme-default',
|
|
23
|
+
* toAstroIntegration: (config) => ({ name: '...', hooks: { ... } }),
|
|
24
|
+
* })
|
|
25
|
+
* // In karaoke.config.ts:
|
|
26
|
+
* theme: themeDefault({ implements: [blog({ mount: '/blog' })] })
|
|
27
|
+
*/
|
|
28
|
+
export function defineTheme(def: ThemeDefinition) {
|
|
29
|
+
return function themeFactory(config: ThemeFactoryConfig = {}): ThemeInstance {
|
|
30
|
+
return {
|
|
31
|
+
_type: 'theme-instance',
|
|
32
|
+
id: def.id,
|
|
33
|
+
implementedModuleIds: (config.implements ?? []).map(m => m.id),
|
|
34
|
+
toAstroIntegration: () => def.toAstroIntegration(config),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,12 @@ import { resolveModules } from './utils/resolve-modules.js';
|
|
|
9
9
|
import { resolveLayout } from './utils/resolve-layout.js';
|
|
10
10
|
import { resolveCollections } from './utils/resolve-collections.js';
|
|
11
11
|
import { resolveMenus } from './utils/resolve-menus.js';
|
|
12
|
-
import type { KaraokeConfig, ResolvedModules, ResolvedLayout, ResolvedCollections, ResolvedMenus } from './types.js';
|
|
12
|
+
import type { KaraokeConfig, ResolvedModules, ResolvedLayout, ResolvedCollections, ResolvedMenus, ThemeInstance } from './types.js';
|
|
13
|
+
|
|
14
|
+
function isThemeInstance(theme: unknown): theme is ThemeInstance {
|
|
15
|
+
return typeof theme === 'object' && theme !== null &&
|
|
16
|
+
(theme as ThemeInstance)._type === 'theme-instance';
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* Resolve a theme config value to an npm package name.
|
|
@@ -47,6 +52,11 @@ function resolveThemePkg(theme: string | undefined): string {
|
|
|
47
52
|
* integrations: [karaoke(karaokeConfig)],
|
|
48
53
|
* });
|
|
49
54
|
*/
|
|
55
|
+
/** Identity wrapper for karaoke.config.ts — provides type inference. */
|
|
56
|
+
export function defineConfig(config: KaraokeConfig): KaraokeConfig {
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
51
61
|
validateModules(config);
|
|
52
62
|
const resolved = resolveModules(config);
|
|
@@ -67,27 +77,34 @@ export default function karaoke(config: KaraokeConfig = {}): AstroIntegration {
|
|
|
67
77
|
const _resolvedMenus = resolveMenus(vaultDir);
|
|
68
78
|
|
|
69
79
|
// ── Load active theme as a nested Astro integration ───────────────
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
let themeIntegration: AstroIntegration;
|
|
81
|
+
|
|
82
|
+
if (isThemeInstance(config.theme)) {
|
|
83
|
+
// New API: theme is a ThemeInstance from defineTheme()
|
|
84
|
+
themeIntegration = config.theme.toAstroIntegration();
|
|
85
|
+
} else {
|
|
86
|
+
// Legacy API: theme is a package name string.
|
|
87
|
+
// Resolve from the user's project root so their node_modules is searched.
|
|
88
|
+
const themePkg = resolveThemePkg(config.theme as string | undefined);
|
|
89
|
+
const projectRequire = createRequire(new URL('package.json', astroConfig.root));
|
|
90
|
+
let resolvedThemePath: string;
|
|
91
|
+
try {
|
|
92
|
+
resolvedThemePath = projectRequire.resolve(themePkg);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[karaoke-cms] Theme package "${themePkg}" is not installed.\n` +
|
|
96
|
+
`Run: pnpm add ${themePkg}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
// Use new Function to bypass Vite's import() interception —
|
|
100
|
+
// this runs the native Node.js ESM loader, not Vite's module runner.
|
|
101
|
+
const nativeImport: (specifier: string) => Promise<unknown> =
|
|
102
|
+
new Function('specifier', 'return import(specifier)');
|
|
103
|
+
const themeModule = await nativeImport(pathToFileURL(resolvedThemePath).href) as {
|
|
104
|
+
default: (cfg: KaraokeConfig) => AstroIntegration;
|
|
105
|
+
};
|
|
106
|
+
themeIntegration = themeModule.default(config);
|
|
82
107
|
}
|
|
83
|
-
// Use new Function to bypass Vite's import() interception —
|
|
84
|
-
// this runs the native Node.js ESM loader, not Vite's module runner.
|
|
85
|
-
const nativeImport: (specifier: string) => Promise<unknown> =
|
|
86
|
-
new Function('specifier', 'return import(specifier)');
|
|
87
|
-
const themeModule = await nativeImport(pathToFileURL(resolvedThemePath).href) as {
|
|
88
|
-
default: (cfg: KaraokeConfig) => AstroIntegration;
|
|
89
|
-
};
|
|
90
|
-
const themeIntegration = themeModule.default(config);
|
|
91
108
|
|
|
92
109
|
// ── Core routes — always present regardless of theme ─────────────
|
|
93
110
|
injectRoute({ pattern: '/rss.xml', entrypoint: '@karaoke-cms/astro/pages/rss.xml.ts' });
|
|
@@ -165,4 +182,7 @@ export const resolvedMenus = ${JSON.stringify(resolvedMenus)};
|
|
|
165
182
|
};
|
|
166
183
|
}
|
|
167
184
|
|
|
185
|
+
export { defineModule } from './define-module.js';
|
|
186
|
+
export { defineTheme } from './define-theme.js';
|
|
187
|
+
export type { ModuleInstance, ThemeInstance, ModuleMenuEntry } from './types.js';
|
|
168
188
|
export type { KaraokeConfig, ResolvedModules, ResolvedLayout, ResolvedCollections, RegionComponent, ResolvedMenus, ResolvedMenu, ResolvedMenuEntry } from './types.js';
|
package/src/types.ts
CHANGED
|
@@ -29,8 +29,8 @@ export interface KaraokeConfig {
|
|
|
29
29
|
title?: string;
|
|
30
30
|
/** Site description — used in RSS feed and OG meta tags. */
|
|
31
31
|
description?: string;
|
|
32
|
-
/** Theme
|
|
33
|
-
theme?: string;
|
|
32
|
+
/** Theme — package name string (legacy) or a ThemeInstance from defineTheme(). */
|
|
33
|
+
theme?: string | ThemeInstance;
|
|
34
34
|
modules?: {
|
|
35
35
|
search?: { enabled?: boolean };
|
|
36
36
|
comments?: {
|
|
@@ -107,3 +107,33 @@ export interface ResolvedMenu {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export type ResolvedMenus = Record<string, ResolvedMenu>;
|
|
110
|
+
|
|
111
|
+
// ── Module system types ────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/** A menu entry registered by a module instance. */
|
|
114
|
+
export interface ModuleMenuEntry {
|
|
115
|
+
id: string;
|
|
116
|
+
name: string;
|
|
117
|
+
path: string;
|
|
118
|
+
section: string;
|
|
119
|
+
weight: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A resolved module instance — returned by a defineModule() factory. */
|
|
123
|
+
export interface ModuleInstance {
|
|
124
|
+
_type: 'module-instance';
|
|
125
|
+
id: string;
|
|
126
|
+
mount: string;
|
|
127
|
+
routes: Array<{ pattern: string; entrypoint: string }>;
|
|
128
|
+
menuEntries: ModuleMenuEntry[];
|
|
129
|
+
cssContract: readonly string[];
|
|
130
|
+
hasDefaultCss: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A resolved theme instance — returned by a defineTheme() factory. */
|
|
134
|
+
export interface ThemeInstance {
|
|
135
|
+
_type: 'theme-instance';
|
|
136
|
+
id: string;
|
|
137
|
+
implementedModuleIds: string[];
|
|
138
|
+
toAstroIntegration: () => import('astro').AstroIntegration;
|
|
139
|
+
}
|