@life-and-dev/mdsite 0.2.1 ā 0.3.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 +65 -178
- package/dist/commands/commands.test.js +53 -13
- package/dist/commands/commands.test.js.map +1 -1
- package/dist/commands/init.js +71 -27
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/workflows.test.js +70 -2
- package/dist/commands/workflows.test.js.map +1 -1
- package/dist/config/mdsite-config.js +19 -2
- package/dist/config/mdsite-config.js.map +1 -1
- package/dist/config/mdsite-config.test.js +15 -0
- package/dist/config/mdsite-config.test.js.map +1 -1
- package/dist/index.js +1 -1
- package/mdsite-nuxt/app/components/content/ProsePre.vue +4 -0
- package/mdsite-nuxt/app/composables/useAppTheme.ts +4 -0
- package/mdsite-nuxt/app/pages/[...slug].vue +4 -2
- package/mdsite-nuxt/app/pages/index.vue +4 -2
- package/mdsite-nuxt/assets/default-favicon.svg +223 -0
- package/mdsite-nuxt/nuxt.config.ts +2 -2
- package/mdsite-nuxt/scripts/generate-favicons.test.ts +177 -0
- package/mdsite-nuxt/scripts/generate-favicons.ts +70 -27
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +48 -5
- package/mdsite-nuxt/scripts/renderer-hooks.ts +12 -8
- package/mdsite-nuxt/utils/mdsite-config.ts +22 -2
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { generateFavicons, generateWebManifest, resolveFaviconSource } from './generate-favicons.js'
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const DEFAULT_FAVICON_PATH = path.resolve(__dirname, '..', 'assets', 'default-favicon.svg')
|
|
12
|
+
|
|
13
|
+
describe('generate-favicons', () => {
|
|
14
|
+
let tmpDir: string
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdsite-favicon-'))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('resolveFaviconSource', () => {
|
|
25
|
+
it('falls back to the bundled default favicon when favicon is an empty string', () => {
|
|
26
|
+
const result = resolveFaviconSource(tmpDir, '')
|
|
27
|
+
|
|
28
|
+
expect(result).not.toBeNull()
|
|
29
|
+
expect(result!.isDefault).toBe(true)
|
|
30
|
+
expect(path.basename(result!.sourcePath)).toBe('default-favicon.svg')
|
|
31
|
+
expect(fs.existsSync(result!.sourcePath)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('falls back to the bundled default favicon when favicon is only whitespace', () => {
|
|
35
|
+
const result = resolveFaviconSource(tmpDir, ' ')
|
|
36
|
+
|
|
37
|
+
expect(result).not.toBeNull()
|
|
38
|
+
expect(result!.isDefault).toBe(true)
|
|
39
|
+
expect(path.basename(result!.sourcePath)).toBe('default-favicon.svg')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('uses the configured source when the favicon file exists under the content dir', () => {
|
|
43
|
+
const relPath = 'favicon.svg'
|
|
44
|
+
const absPath = path.join(tmpDir, relPath)
|
|
45
|
+
const customSvg =
|
|
46
|
+
'<?xml version="1.0" encoding="UTF-8"?>' +
|
|
47
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' +
|
|
48
|
+
'<rect width="16" height="16" fill="red"/></svg>'
|
|
49
|
+
fs.writeFileSync(absPath, customSvg, 'utf8')
|
|
50
|
+
|
|
51
|
+
const result = resolveFaviconSource(tmpDir, relPath)
|
|
52
|
+
|
|
53
|
+
expect(result).not.toBeNull()
|
|
54
|
+
expect(result!.isDefault).toBe(false)
|
|
55
|
+
expect(result!.sourcePath).toBe(path.resolve(tmpDir, relPath))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('falls back to the bundled default favicon when the configured file does not exist', () => {
|
|
59
|
+
const result = resolveFaviconSource(tmpDir, 'does-not-exist.svg')
|
|
60
|
+
|
|
61
|
+
expect(result).not.toBeNull()
|
|
62
|
+
expect(result!.isDefault).toBe(true)
|
|
63
|
+
expect(path.basename(result!.sourcePath)).toBe('default-favicon.svg')
|
|
64
|
+
expect(fs.existsSync(result!.sourcePath)).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('generateFavicons', () => {
|
|
69
|
+
it('uses the bundled default favicon and writes all expected assets when config.favicon is empty', async () => {
|
|
70
|
+
const outputDir = path.join(tmpDir, 'output')
|
|
71
|
+
|
|
72
|
+
const ok = await generateFavicons({
|
|
73
|
+
contentDir: tmpDir,
|
|
74
|
+
config: { favicon: '' },
|
|
75
|
+
outputDir,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(ok).toBe(true)
|
|
79
|
+
|
|
80
|
+
const expectedFiles = [
|
|
81
|
+
'favicon.svg',
|
|
82
|
+
'favicon.ico',
|
|
83
|
+
'apple-touch-icon.png',
|
|
84
|
+
'icon-192.png',
|
|
85
|
+
'icon-512.png',
|
|
86
|
+
]
|
|
87
|
+
for (const file of expectedFiles) {
|
|
88
|
+
const filePath = path.join(outputDir, file)
|
|
89
|
+
expect(fs.existsSync(filePath)).toBe(true)
|
|
90
|
+
expect(fs.statSync(filePath).size).toBeGreaterThan(0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The default source svg is copied verbatim as favicon.svg
|
|
94
|
+
const defaultSvgContent = fs.readFileSync(DEFAULT_FAVICON_PATH, 'utf8')
|
|
95
|
+
const writtenSvgContent = fs.readFileSync(path.join(outputDir, 'favicon.svg'), 'utf8')
|
|
96
|
+
expect(writtenSvgContent).toBe(defaultSvgContent)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('uses the configured custom source svg over the bundled default', async () => {
|
|
100
|
+
const outputDir = path.join(tmpDir, 'output')
|
|
101
|
+
const customSvg =
|
|
102
|
+
'<?xml version="1.0" encoding="UTF-8"?>' +
|
|
103
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' +
|
|
104
|
+
'<rect width="16" height="16" fill="blue"/></svg>'
|
|
105
|
+
fs.writeFileSync(path.join(tmpDir, 'favicon.svg'), customSvg, 'utf8')
|
|
106
|
+
|
|
107
|
+
const ok = await generateFavicons({
|
|
108
|
+
contentDir: tmpDir,
|
|
109
|
+
config: { favicon: 'favicon.svg' },
|
|
110
|
+
outputDir,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(ok).toBe(true)
|
|
114
|
+
|
|
115
|
+
const writtenSvgContent = fs.readFileSync(path.join(outputDir, 'favicon.svg'), 'utf8')
|
|
116
|
+
expect(writtenSvgContent).toBe(customSvg)
|
|
117
|
+
|
|
118
|
+
// And it is NOT the bundled default
|
|
119
|
+
const defaultSvgContent = fs.readFileSync(DEFAULT_FAVICON_PATH, 'utf8')
|
|
120
|
+
expect(writtenSvgContent).not.toBe(defaultSvgContent)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('generateWebManifest', () => {
|
|
125
|
+
it('writes manifest with theme colors from config when name and themes are provided', async () => {
|
|
126
|
+
const outputDir = path.join(tmpDir, 'output')
|
|
127
|
+
|
|
128
|
+
await generateWebManifest({
|
|
129
|
+
name: 'My Site',
|
|
130
|
+
themes: {
|
|
131
|
+
light: { colors: { primary: '#abcdef', surface: '#fedcba' } },
|
|
132
|
+
dark: { colors: {} },
|
|
133
|
+
},
|
|
134
|
+
outputDir,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const manifestPath = path.join(outputDir, 'site.webmanifest')
|
|
138
|
+
expect(fs.existsSync(manifestPath)).toBe(true)
|
|
139
|
+
const written = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
140
|
+
expect(written).toEqual({
|
|
141
|
+
name: 'My Site',
|
|
142
|
+
short_name: 'My Site',
|
|
143
|
+
icons: [
|
|
144
|
+
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
|
|
145
|
+
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png' },
|
|
146
|
+
],
|
|
147
|
+
theme_color: '#abcdef',
|
|
148
|
+
background_color: '#fedcba',
|
|
149
|
+
display: 'standalone',
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('falls back to safe defaults when themes are missing colors', async () => {
|
|
154
|
+
const outputDir = path.join(tmpDir, 'output')
|
|
155
|
+
|
|
156
|
+
await generateWebManifest({
|
|
157
|
+
name: 'No Themes',
|
|
158
|
+
themes: { light: { colors: {} }, dark: { colors: {} } },
|
|
159
|
+
outputDir,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const written = JSON.parse(fs.readFileSync(path.join(outputDir, 'site.webmanifest'), 'utf8'))
|
|
163
|
+
expect(written.theme_color).toBe('#000000')
|
|
164
|
+
expect(written.background_color).toBe('#ffffff')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('falls back to safe defaults when themes are not provided at all', async () => {
|
|
168
|
+
const outputDir = path.join(tmpDir, 'output')
|
|
169
|
+
|
|
170
|
+
await generateWebManifest({ name: 'No Themes', outputDir })
|
|
171
|
+
|
|
172
|
+
const written = JSON.parse(fs.readFileSync(path.join(outputDir, 'site.webmanifest'), 'utf8'))
|
|
173
|
+
expect(written.theme_color).toBe('#000000')
|
|
174
|
+
expect(written.background_color).toBe('#ffffff')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
import sharp from 'sharp'
|
|
4
4
|
import fs from 'fs-extra'
|
|
5
5
|
import path from 'path'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
7
|
-
import { loadMdsiteConfigSync, resolveMdsiteConfigPath } from '../utils/mdsite-config.js'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { loadMdsiteConfigSync, resolveMdsiteConfigPath, type MdsiteConfig } from '../utils/mdsite-config.js'
|
|
8
8
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
10
|
const __dirname = path.dirname(__filename)
|
|
11
11
|
|
|
12
|
+
const DEFAULT_FAVICON_PATH = fileURLToPath(new URL('../assets/default-favicon.svg', import.meta.url))
|
|
13
|
+
|
|
12
14
|
const FAVICON_SIZES = {
|
|
13
15
|
ico: [16, 32],
|
|
14
16
|
appleTouchIcon: 180,
|
|
@@ -16,28 +18,56 @@ const FAVICON_SIZES = {
|
|
|
16
18
|
pwaIcon512: 512
|
|
17
19
|
} as const
|
|
18
20
|
|
|
21
|
+
export function resolveFaviconSource(
|
|
22
|
+
contentDir: string,
|
|
23
|
+
favicon: string,
|
|
24
|
+
): { sourcePath: string; isDefault: boolean } | null {
|
|
25
|
+
if (typeof favicon === 'string' && favicon.trim().length > 0) {
|
|
26
|
+
const candidate = path.resolve(contentDir, favicon)
|
|
27
|
+
if (fs.pathExistsSync(candidate)) {
|
|
28
|
+
return { sourcePath: candidate, isDefault: false }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (fs.pathExistsSync(DEFAULT_FAVICON_PATH)) {
|
|
33
|
+
return { sourcePath: DEFAULT_FAVICON_PATH, isDefault: true }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GenerateFaviconsOptions {
|
|
40
|
+
contentDir?: string
|
|
41
|
+
config?: { favicon?: string; site?: { name?: string } }
|
|
42
|
+
outputDir?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
/**
|
|
20
46
|
* Generate favicons from the active mdsite config
|
|
21
47
|
*/
|
|
22
|
-
export async function generateFavicons(): Promise<boolean> {
|
|
23
|
-
const
|
|
48
|
+
export async function generateFavicons(options: GenerateFaviconsOptions = {}): Promise<boolean> {
|
|
49
|
+
const resolved = options.contentDir && options.config
|
|
50
|
+
? { contentDir: options.contentDir, config: options.config }
|
|
51
|
+
: loadMdsiteConfigSync()
|
|
52
|
+
const { contentDir, config } = resolved
|
|
53
|
+
const siteName = (config as { site?: { name?: string } }).site?.name ?? 'site'
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
console.warn(`ā ļø No favicon source configured (config.favicon is empty). Skipping favicon generation.`)
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
55
|
+
const resolvedSource = resolveFaviconSource(contentDir, config.favicon ?? '')
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!await fs.pathExists(sourcePath)) {
|
|
33
|
-
console.error(`ā Favicon source not found: ${sourcePath}`)
|
|
57
|
+
if (!resolvedSource) {
|
|
58
|
+
console.error('ā No favicon source available (configured source missing AND bundled default not found).')
|
|
34
59
|
return false
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
const
|
|
62
|
+
const { sourcePath } = resolvedSource
|
|
63
|
+
const publicDir = options.outputDir ?? path.resolve(__dirname, '..', 'public')
|
|
38
64
|
await fs.ensureDir(publicDir)
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
if (resolvedSource.isDefault) {
|
|
67
|
+
console.log('ā¹ļø No favicon source configured (config.favicon empty or file not found). Using bundled default favicon.')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`šØ Generating favicons for site: ${siteName}`)
|
|
41
71
|
console.log(` Source: ${sourcePath}`)
|
|
42
72
|
console.log(` Output: ${publicDir}`)
|
|
43
73
|
|
|
@@ -92,25 +122,38 @@ export async function generateFavicons(): Promise<boolean> {
|
|
|
92
122
|
.toFile(icon512Path)
|
|
93
123
|
console.log(` ā PWA Icon 512: icon-512.png`)
|
|
94
124
|
|
|
95
|
-
console.log(`ā
Favicons generated successfully for ${
|
|
125
|
+
console.log(`ā
Favicons generated successfully for ${siteName}\n`)
|
|
96
126
|
return true
|
|
97
127
|
} catch (error) {
|
|
98
|
-
console.error(`ā Failed to generate favicons for ${
|
|
128
|
+
console.error(`ā Failed to generate favicons for ${siteName}:`, error)
|
|
99
129
|
return false
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
export interface GenerateWebManifestOptions {
|
|
134
|
+
name?: string
|
|
135
|
+
themes?: MdsiteConfig['themes']
|
|
136
|
+
outputDir?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
/**
|
|
104
|
-
* Generate web manifest for PWA support
|
|
140
|
+
* Generate web manifest for PWA support.
|
|
141
|
+
* Theme/background colors are taken from the light theme in mdsite.yml
|
|
142
|
+
* to match the existing `<meta name="theme-color" media="(prefers-color-scheme: light)">` tag.
|
|
143
|
+
* The web manifest spec only supports a single `theme_color`, so light values are used.
|
|
105
144
|
*/
|
|
106
|
-
export async function generateWebManifest(
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
145
|
+
export async function generateWebManifest(options: GenerateWebManifestOptions = {}): Promise<void> {
|
|
146
|
+
const siteName = options.name ?? 'site'
|
|
147
|
+
const themeColor = options.themes?.light?.colors?.primary ?? '#000000'
|
|
148
|
+
const backgroundColor = options.themes?.light?.colors?.surface ?? '#ffffff'
|
|
149
|
+
|
|
150
|
+
const outputDir = options.outputDir ?? path.resolve(__dirname, '..', 'public')
|
|
151
|
+
await fs.ensureDir(outputDir)
|
|
152
|
+
const manifestPath = path.join(outputDir, 'site.webmanifest')
|
|
110
153
|
|
|
111
154
|
const manifest = {
|
|
112
|
-
name:
|
|
113
|
-
short_name:
|
|
155
|
+
name: siteName,
|
|
156
|
+
short_name: siteName,
|
|
114
157
|
icons: [
|
|
115
158
|
{
|
|
116
159
|
src: 'icon-192.png',
|
|
@@ -123,13 +166,13 @@ export async function generateWebManifest(name: string) {
|
|
|
123
166
|
type: 'image/png'
|
|
124
167
|
}
|
|
125
168
|
],
|
|
126
|
-
theme_color:
|
|
127
|
-
background_color:
|
|
169
|
+
theme_color: themeColor,
|
|
170
|
+
background_color: backgroundColor,
|
|
128
171
|
display: 'standalone'
|
|
129
172
|
}
|
|
130
173
|
|
|
131
174
|
await fs.writeJson(manifestPath, manifest, { spaces: 2 })
|
|
132
|
-
console.log(`š± Web manifest generated: site.webmanifest\n`)
|
|
175
|
+
console.log(`š± Web manifest generated: site.webmanifest (theme_color=${themeColor})\n`)
|
|
133
176
|
}
|
|
134
177
|
|
|
135
178
|
/**
|
|
@@ -153,7 +196,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
153
196
|
; (async () => {
|
|
154
197
|
const success = await generateFavicons()
|
|
155
198
|
if (success) {
|
|
156
|
-
await generateWebManifest(config.site.name)
|
|
199
|
+
await generateWebManifest({ name: config.site.name, themes: config.themes })
|
|
157
200
|
} else {
|
|
158
201
|
process.exit(1)
|
|
159
202
|
}
|
|
@@ -87,7 +87,20 @@ describe('renderer hooks orchestration', () => {
|
|
|
87
87
|
|
|
88
88
|
resolveMdsiteConfigPathMock.mockReturnValue('/renderer/mdsite.yml')
|
|
89
89
|
loadMdsiteConfigSyncMock.mockReturnValue({
|
|
90
|
-
config: {
|
|
90
|
+
config: {
|
|
91
|
+
site: { name: 'Docs' },
|
|
92
|
+
themes: {
|
|
93
|
+
light: {
|
|
94
|
+
colors: {
|
|
95
|
+
primary: '#abcdef',
|
|
96
|
+
surface: '#fedcba',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
dark: {
|
|
100
|
+
colors: {},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
91
104
|
configPath: '/renderer/mdsite.yml',
|
|
92
105
|
contentDir: '/renderer/docs',
|
|
93
106
|
})
|
|
@@ -117,7 +130,13 @@ describe('renderer hooks orchestration', () => {
|
|
|
117
130
|
contentPath: '/input/docs',
|
|
118
131
|
})
|
|
119
132
|
expect(runtime).toEqual({
|
|
120
|
-
config: {
|
|
133
|
+
config: {
|
|
134
|
+
site: { name: 'Docs' },
|
|
135
|
+
themes: {
|
|
136
|
+
light: { colors: { primary: '#abcdef', surface: '#fedcba' } },
|
|
137
|
+
dark: { colors: {} },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
121
140
|
configPath: '/renderer/mdsite.yml',
|
|
122
141
|
contentDir: '/renderer/docs',
|
|
123
142
|
rootDir: '/renderer',
|
|
@@ -233,6 +252,15 @@ describe('renderer hooks orchestration', () => {
|
|
|
233
252
|
expect(syncContentMock).not.toHaveBeenCalled()
|
|
234
253
|
expect(buildContentDataMock).not.toHaveBeenCalled()
|
|
235
254
|
expect(generateFaviconsMock).not.toHaveBeenCalled()
|
|
255
|
+
expect(generateWebManifestMock).toHaveBeenCalledTimes(1)
|
|
256
|
+
expect(generateWebManifestMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
257
|
+
name: 'Docs',
|
|
258
|
+
themes: expect.objectContaining({
|
|
259
|
+
light: expect.objectContaining({
|
|
260
|
+
colors: expect.objectContaining({ primary: '#abcdef' }),
|
|
261
|
+
}),
|
|
262
|
+
}),
|
|
263
|
+
}))
|
|
236
264
|
expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
|
|
237
265
|
expect(runtime.contentDir).toBe('/renderer/docs')
|
|
238
266
|
})
|
|
@@ -243,6 +271,7 @@ describe('renderer hooks orchestration', () => {
|
|
|
243
271
|
expect(rmMock).not.toHaveBeenCalled()
|
|
244
272
|
expect(startWatcherMock).toHaveBeenCalledTimes(1)
|
|
245
273
|
expect(syncContentMock).not.toHaveBeenCalled()
|
|
274
|
+
expect(generateWebManifestMock).toHaveBeenCalledTimes(1)
|
|
246
275
|
})
|
|
247
276
|
|
|
248
277
|
it('runs non-dev setup hooks in orchestration order and publishes favicon assets on success', async () => {
|
|
@@ -251,7 +280,14 @@ describe('renderer hooks orchestration', () => {
|
|
|
251
280
|
expect(syncContentMock).toHaveBeenCalledTimes(1)
|
|
252
281
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
253
282
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
254
|
-
expect(generateWebManifestMock).toHaveBeenCalledWith(
|
|
283
|
+
expect(generateWebManifestMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
284
|
+
name: 'Docs',
|
|
285
|
+
themes: expect.objectContaining({
|
|
286
|
+
light: expect.objectContaining({
|
|
287
|
+
colors: expect.objectContaining({ primary: '#abcdef' }),
|
|
288
|
+
}),
|
|
289
|
+
}),
|
|
290
|
+
}))
|
|
255
291
|
expect(syncContentMock.mock.invocationCallOrder[0]).toBeLessThan(buildContentDataMock.mock.invocationCallOrder[0])
|
|
256
292
|
expect(buildContentDataMock.mock.invocationCallOrder[0]).toBeLessThan(generateFaviconsMock.mock.invocationCallOrder[0])
|
|
257
293
|
expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
|
|
@@ -263,7 +299,14 @@ describe('renderer hooks orchestration', () => {
|
|
|
263
299
|
expect(syncContentMock).toHaveBeenCalledTimes(1)
|
|
264
300
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
265
301
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
266
|
-
expect(generateWebManifestMock).toHaveBeenCalledWith(
|
|
302
|
+
expect(generateWebManifestMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
303
|
+
name: 'Docs',
|
|
304
|
+
themes: expect.objectContaining({
|
|
305
|
+
light: expect.objectContaining({
|
|
306
|
+
colors: expect.objectContaining({ primary: '#abcdef' }),
|
|
307
|
+
}),
|
|
308
|
+
}),
|
|
309
|
+
}))
|
|
267
310
|
expect(startWatcherMock).not.toHaveBeenCalled()
|
|
268
311
|
expect(rmMock).not.toHaveBeenCalled()
|
|
269
312
|
expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
|
|
@@ -282,7 +325,7 @@ describe('renderer hooks orchestration', () => {
|
|
|
282
325
|
it('runs build fallback hooks without publishing favicon assets when generation reports no output', async () => {
|
|
283
326
|
generateFaviconsMock.mockResolvedValue(false)
|
|
284
327
|
|
|
285
|
-
await runBuildFallbackHooks('Docs')
|
|
328
|
+
await runBuildFallbackHooks({ site: { name: 'Docs' }, themes: { light: { colors: {} }, dark: { colors: {} } } } as any)
|
|
286
329
|
|
|
287
330
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
288
331
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
@@ -5,7 +5,7 @@ import YAML from 'yaml'
|
|
|
5
5
|
import { buildContentData } from './generate-indices.js'
|
|
6
6
|
import { generateFavicons, generateWebManifest } from './generate-favicons.js'
|
|
7
7
|
import { startWatcher, syncContent } from './sync-content.js'
|
|
8
|
-
import { loadMdsiteConfigSync, resolveMdsiteConfigPath } from '../utils/mdsite-config.js'
|
|
8
|
+
import { loadMdsiteConfigSync, resolveMdsiteConfigPath, type MdsiteConfig } from '../utils/mdsite-config.js'
|
|
9
9
|
|
|
10
10
|
export interface RendererRuntime {
|
|
11
11
|
config: ReturnType<typeof loadMdsiteConfigSync>['config']
|
|
@@ -69,7 +69,7 @@ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev'
|
|
|
69
69
|
if (!options.cached) {
|
|
70
70
|
await fs.promises.rm(path.join(rootDir, '.data'), { recursive: true, force: true })
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
await generateDevManifestAssets(runtime.config)
|
|
73
73
|
process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
|
|
74
74
|
await startWatcher()
|
|
75
75
|
return runtime
|
|
@@ -78,28 +78,32 @@ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev'
|
|
|
78
78
|
await syncContent()
|
|
79
79
|
console.log(`\nšØ Generating navigation and search index...`)
|
|
80
80
|
await buildContentData()
|
|
81
|
-
await generateFaviconAssets(runtime.config
|
|
81
|
+
await generateFaviconAssets(runtime.config)
|
|
82
82
|
process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
|
|
83
83
|
|
|
84
84
|
return runtime
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export async function runBuildFallbackHooks(
|
|
87
|
+
export async function runBuildFallbackHooks(config: MdsiteConfig): Promise<void> {
|
|
88
88
|
console.log(`\nšØ Generating navigation and search index...`)
|
|
89
89
|
await buildContentData()
|
|
90
|
-
await generateFaviconAssets(
|
|
90
|
+
await generateFaviconAssets(config)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function generateFaviconAssets(
|
|
93
|
+
async function generateFaviconAssets(config: MdsiteConfig): Promise<void> {
|
|
94
94
|
console.log(`\nšØ Generating favicons for build...`)
|
|
95
95
|
const success = await generateFavicons()
|
|
96
96
|
|
|
97
97
|
if (success) {
|
|
98
|
-
await generateWebManifest(
|
|
99
|
-
console.log(`ā
Favicons ready for ${
|
|
98
|
+
await generateWebManifest({ name: config.site.name, themes: config.themes })
|
|
99
|
+
console.log(`ā
Favicons ready for ${config.site.name}\n`)
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
async function generateDevManifestAssets(config: MdsiteConfig): Promise<void> {
|
|
104
|
+
await generateWebManifest({ name: config.site.name, themes: config.themes })
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
function ensureLegacyCompatibilityConfig(rootDir: string): string | undefined {
|
|
104
108
|
const legacyConfigPath = path.join(rootDir, 'content.config.yml')
|
|
105
109
|
|
|
@@ -164,7 +164,8 @@ export function resolveContentDir(options: {
|
|
|
164
164
|
|
|
165
165
|
if (resolvedConfigPath) {
|
|
166
166
|
const configDir = path.dirname(resolvedConfigPath);
|
|
167
|
-
|
|
167
|
+
const contentPath = resolveContentConfigPath(rawConfig.content);
|
|
168
|
+
return contentPath ? path.resolve(configDir, contentPath) : configDir;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
if (options.searchFrom) {
|
|
@@ -174,6 +175,24 @@ export function resolveContentDir(options: {
|
|
|
174
175
|
return process.cwd();
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Extract the content path from either the shorthand string form
|
|
180
|
+
* (`content: docs`) or the explicit object form (`content:\n path: docs`).
|
|
181
|
+
* Returns undefined when no usable path is configured.
|
|
182
|
+
*/
|
|
183
|
+
function resolveContentConfigPath(rawContent: unknown): string | undefined {
|
|
184
|
+
if (typeof rawContent === 'string') {
|
|
185
|
+
const trimmed = rawContent.trim();
|
|
186
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (rawContent && typeof rawContent === 'object' && typeof (rawContent as { path?: unknown }).path === 'string') {
|
|
190
|
+
return (rawContent as { path: string }).path;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
177
196
|
function createDefaultMdsiteConfig(siteName: string): MdsiteConfig {
|
|
178
197
|
return {
|
|
179
198
|
favicon: '',
|
|
@@ -204,6 +223,7 @@ function createDefaultMdsiteConfig(siteName: string): MdsiteConfig {
|
|
|
204
223
|
|
|
205
224
|
function normalizeMdsiteConfig(rawConfig: Record<string, any>, contentDir: string): MdsiteConfig {
|
|
206
225
|
const fallbackConfig = createDefaultMdsiteConfig(path.basename(contentDir) || 'Site');
|
|
226
|
+
const contentPath = resolveContentConfigPath(rawConfig.content);
|
|
207
227
|
|
|
208
228
|
return {
|
|
209
229
|
favicon: typeof rawConfig.favicon === 'string' ? rawConfig.favicon : fallbackConfig.favicon,
|
|
@@ -211,7 +231,7 @@ function normalizeMdsiteConfig(rawConfig: Record<string, any>, contentDir: strin
|
|
|
211
231
|
bibleTooltips: rawConfig.features?.bibleTooltips ?? fallbackConfig.features.bibleTooltips,
|
|
212
232
|
sourceEdit: rawConfig.features?.sourceEdit ?? fallbackConfig.features.sourceEdit
|
|
213
233
|
},
|
|
214
|
-
content:
|
|
234
|
+
content: contentPath ? { path: contentPath } : fallbackConfig.content,
|
|
215
235
|
menu: Array.isArray(rawConfig.menu) ? rawConfig.menu : fallbackConfig.menu,
|
|
216
236
|
server: {
|
|
217
237
|
output: typeof rawConfig.server?.output === 'string' ? rawConfig.server.output : fallbackConfig.server.output,
|