@life-and-dev/mdsite 0.1.1 → 0.2.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 +39 -21
- package/dist/commands/commands.test.js +91 -30
- package/dist/commands/commands.test.js.map +1 -1
- package/dist/commands/init.js +54 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/prepare.js +22 -14
- package/dist/commands/prepare.js.map +1 -1
- package/dist/commands/prepare.test.js +33 -39
- package/dist/commands/prepare.test.js.map +1 -1
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +11 -10
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +23 -10
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.js +6 -3
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/workflows.test.js +76 -19
- package/dist/commands/workflows.test.js.map +1 -1
- package/dist/config/mdsite-config.js +1 -1
- package/dist/config/mdsite-config.js.map +1 -1
- package/dist/config/mdsite-config.test.js +1 -1
- package/dist/config/mdsite-config.test.js.map +1 -1
- package/dist/config/menu.js +0 -1
- package/dist/config/menu.js.map +1 -1
- package/dist/index.js +42 -7
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +40 -0
- package/dist/index.test.js.map +1 -1
- package/dist/process/child-process.test.js +3 -3
- package/dist/process/child-process.test.js.map +1 -1
- package/dist/process/runtime-state.d.ts +6 -5
- package/dist/process/runtime-state.js +13 -17
- package/dist/process/runtime-state.js.map +1 -1
- package/dist/process/runtime-state.test.js +21 -13
- package/dist/process/runtime-state.test.js.map +1 -1
- package/dist/renderer/mdsite-nuxt.d.ts +2 -0
- package/dist/renderer/mdsite-nuxt.js +29 -32
- package/dist/renderer/mdsite-nuxt.js.map +1 -1
- package/dist/renderer/mdsite-nuxt.test.js +37 -48
- package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
- package/mdsite-nuxt/app/composables/useAppTheme.ts +22 -3
- package/mdsite-nuxt/app/config/themes.ts +38 -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 +11 -1
- package/mdsite-nuxt/scripts/generate-favicons.test.ts +123 -0
- package/mdsite-nuxt/scripts/generate-favicons.ts +60 -61
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +0 -8
- package/mdsite-nuxt/scripts/renderer-hooks.ts +1 -2
- package/mdsite-nuxt/scripts/sync-content.ts +5 -79
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
width="348.061"
|
|
6
|
+
height="340"
|
|
7
|
+
viewBox="0 0 34.8061 34"
|
|
8
|
+
version="1.1"
|
|
9
|
+
id="svg1"
|
|
10
|
+
inkscape:version="1.4.3 (1:1.4.3+202512261035+0d15f75042)"
|
|
11
|
+
sodipodi:docname="logo.svg"
|
|
12
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
13
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
14
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
17
|
+
<sodipodi:namedview
|
|
18
|
+
id="namedview1"
|
|
19
|
+
pagecolor="#505050"
|
|
20
|
+
bordercolor="#eeeeee"
|
|
21
|
+
borderopacity="1"
|
|
22
|
+
inkscape:showpageshadow="0"
|
|
23
|
+
inkscape:pageopacity="0"
|
|
24
|
+
inkscape:pagecheckerboard="0"
|
|
25
|
+
inkscape:deskcolor="#505050"
|
|
26
|
+
inkscape:document-units="px"
|
|
27
|
+
showgrid="true"
|
|
28
|
+
inkscape:zoom="2.9179688"
|
|
29
|
+
inkscape:cx="181.11914"
|
|
30
|
+
inkscape:cy="144.27844"
|
|
31
|
+
inkscape:window-width="3032"
|
|
32
|
+
inkscape:window-height="1696"
|
|
33
|
+
inkscape:window-x="40"
|
|
34
|
+
inkscape:window-y="0"
|
|
35
|
+
inkscape:window-maximized="1"
|
|
36
|
+
inkscape:current-layer="layer1">
|
|
37
|
+
<inkscape:grid
|
|
38
|
+
id="grid1"
|
|
39
|
+
units="px"
|
|
40
|
+
originx="1.2100008"
|
|
41
|
+
originy="1"
|
|
42
|
+
spacingx="1"
|
|
43
|
+
spacingy="1"
|
|
44
|
+
empcolor="#0099e5"
|
|
45
|
+
empopacity="0.30196078"
|
|
46
|
+
color="#0099e5"
|
|
47
|
+
opacity="0.14901961"
|
|
48
|
+
empspacing="5"
|
|
49
|
+
enabled="true"
|
|
50
|
+
visible="true" />
|
|
51
|
+
</sodipodi:namedview>
|
|
52
|
+
<defs
|
|
53
|
+
id="defs1">
|
|
54
|
+
<linearGradient
|
|
55
|
+
id="linearGradient12"
|
|
56
|
+
inkscape:collect="always">
|
|
57
|
+
<stop
|
|
58
|
+
style="stop-color:#000000;stop-opacity:1;"
|
|
59
|
+
offset="0"
|
|
60
|
+
id="stop13" />
|
|
61
|
+
<stop
|
|
62
|
+
style="stop-color:#000000;stop-opacity:0;"
|
|
63
|
+
offset="1"
|
|
64
|
+
id="stop14" />
|
|
65
|
+
</linearGradient>
|
|
66
|
+
<linearGradient
|
|
67
|
+
id="linearGradient4"
|
|
68
|
+
inkscape:collect="always">
|
|
69
|
+
<stop
|
|
70
|
+
style="stop-color:#ffffff;stop-opacity:1;"
|
|
71
|
+
offset="0.49735323"
|
|
72
|
+
id="stop5" />
|
|
73
|
+
<stop
|
|
74
|
+
style="stop-color:#ffffff;stop-opacity:0.75119454;"
|
|
75
|
+
offset="1"
|
|
76
|
+
id="stop6" />
|
|
77
|
+
</linearGradient>
|
|
78
|
+
<linearGradient
|
|
79
|
+
id="linearGradient3"
|
|
80
|
+
inkscape:collect="always">
|
|
81
|
+
<stop
|
|
82
|
+
style="stop-color:#f9f9f9;stop-opacity:1;"
|
|
83
|
+
offset="0.49600685"
|
|
84
|
+
id="stop3" />
|
|
85
|
+
<stop
|
|
86
|
+
style="stop-color:#ffffff;stop-opacity:0.7457487;"
|
|
87
|
+
offset="1"
|
|
88
|
+
id="stop4" />
|
|
89
|
+
</linearGradient>
|
|
90
|
+
<radialGradient
|
|
91
|
+
inkscape:collect="always"
|
|
92
|
+
xlink:href="#linearGradient3"
|
|
93
|
+
id="radialGradient4"
|
|
94
|
+
cx="10.5"
|
|
95
|
+
cy="16"
|
|
96
|
+
fx="10.5"
|
|
97
|
+
fy="16"
|
|
98
|
+
r="10.5"
|
|
99
|
+
gradientTransform="matrix(1,0,0,1.3333333,0,-5.3333333)"
|
|
100
|
+
gradientUnits="userSpaceOnUse" />
|
|
101
|
+
<radialGradient
|
|
102
|
+
inkscape:collect="always"
|
|
103
|
+
xlink:href="#linearGradient4"
|
|
104
|
+
id="radialGradient6"
|
|
105
|
+
cx="24.514727"
|
|
106
|
+
cy="12"
|
|
107
|
+
fx="24.514727"
|
|
108
|
+
fy="12"
|
|
109
|
+
r="7.5147257"
|
|
110
|
+
gradientTransform="matrix(1,0,0,1.3307206,0,-3.9686472)"
|
|
111
|
+
gradientUnits="userSpaceOnUse" />
|
|
112
|
+
<radialGradient
|
|
113
|
+
inkscape:collect="always"
|
|
114
|
+
xlink:href="#linearGradient12"
|
|
115
|
+
id="radialGradient14"
|
|
116
|
+
cx="16"
|
|
117
|
+
cy="16"
|
|
118
|
+
fx="16"
|
|
119
|
+
fy="16"
|
|
120
|
+
r="16"
|
|
121
|
+
gradientUnits="userSpaceOnUse"
|
|
122
|
+
gradientTransform="matrix(1.0624999,0,0,1.0624999,0.21000074,0)" />
|
|
123
|
+
<filter
|
|
124
|
+
style="color-interpolation-filters:sRGB"
|
|
125
|
+
inkscape:label="Drop Shadow"
|
|
126
|
+
id="filter66"
|
|
127
|
+
x="-0.057619048"
|
|
128
|
+
y="-0.055841193"
|
|
129
|
+
width="1.132074"
|
|
130
|
+
height="1.1384681">
|
|
131
|
+
<feFlood
|
|
132
|
+
result="flood"
|
|
133
|
+
in="SourceGraphic"
|
|
134
|
+
flood-opacity="0.498039"
|
|
135
|
+
flood-color="rgb(0,0,0)"
|
|
136
|
+
id="feFlood65" />
|
|
137
|
+
<feGaussianBlur
|
|
138
|
+
result="blur"
|
|
139
|
+
in="SourceGraphic"
|
|
140
|
+
stdDeviation="0.400000"
|
|
141
|
+
id="feGaussianBlur65" />
|
|
142
|
+
<feOffset
|
|
143
|
+
result="offset"
|
|
144
|
+
in="blur"
|
|
145
|
+
dx="0.000000"
|
|
146
|
+
dy="1.000000"
|
|
147
|
+
id="feOffset65" />
|
|
148
|
+
<feComposite
|
|
149
|
+
result="comp1"
|
|
150
|
+
operator="in"
|
|
151
|
+
in="flood"
|
|
152
|
+
in2="offset"
|
|
153
|
+
id="feComposite65" />
|
|
154
|
+
<feComposite
|
|
155
|
+
result="comp2"
|
|
156
|
+
operator="over"
|
|
157
|
+
in="SourceGraphic"
|
|
158
|
+
in2="comp1"
|
|
159
|
+
id="feComposite66" />
|
|
160
|
+
</filter>
|
|
161
|
+
<filter
|
|
162
|
+
style="color-interpolation-filters:sRGB"
|
|
163
|
+
inkscape:label="Drop Shadow"
|
|
164
|
+
id="filter68"
|
|
165
|
+
x="-0.10403264"
|
|
166
|
+
y="-0.060500001"
|
|
167
|
+
width="1.2082745"
|
|
168
|
+
height="1.171">
|
|
169
|
+
<feFlood
|
|
170
|
+
result="flood"
|
|
171
|
+
in="SourceGraphic"
|
|
172
|
+
flood-opacity="0.498039"
|
|
173
|
+
flood-color="rgb(0,0,0)"
|
|
174
|
+
id="feFlood66" />
|
|
175
|
+
<feGaussianBlur
|
|
176
|
+
result="blur"
|
|
177
|
+
in="SourceGraphic"
|
|
178
|
+
stdDeviation="0.400000"
|
|
179
|
+
id="feGaussianBlur66" />
|
|
180
|
+
<feOffset
|
|
181
|
+
result="offset"
|
|
182
|
+
in="blur"
|
|
183
|
+
dx="0.000000"
|
|
184
|
+
dy="1.000000"
|
|
185
|
+
id="feOffset66" />
|
|
186
|
+
<feComposite
|
|
187
|
+
result="comp1"
|
|
188
|
+
operator="in"
|
|
189
|
+
in="flood"
|
|
190
|
+
in2="offset"
|
|
191
|
+
id="feComposite67" />
|
|
192
|
+
<feComposite
|
|
193
|
+
result="comp2"
|
|
194
|
+
operator="over"
|
|
195
|
+
in="SourceGraphic"
|
|
196
|
+
in2="comp1"
|
|
197
|
+
id="feComposite68" />
|
|
198
|
+
</filter>
|
|
199
|
+
</defs>
|
|
200
|
+
<rect
|
|
201
|
+
style="display:inline;fill:url(#radialGradient14);stroke:none;stroke-width:1.0625"
|
|
202
|
+
id="rect2"
|
|
203
|
+
width="34"
|
|
204
|
+
height="34"
|
|
205
|
+
x="0.21000074"
|
|
206
|
+
y="0" />
|
|
207
|
+
<g
|
|
208
|
+
inkscape:label="Layer 1"
|
|
209
|
+
inkscape:groupmode="layer"
|
|
210
|
+
id="layer1"
|
|
211
|
+
transform="translate(1.21,1)">
|
|
212
|
+
<path
|
|
213
|
+
style="fill:url(#radialGradient4);stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter66)"
|
|
214
|
+
d="M 0,22 V 2 l 8,8 8,-8 0.03029,21 H 21 l -7,7 -7,-7 h 5.030291 L 12,11 8,15 4,11 v 11 z"
|
|
215
|
+
id="path1"
|
|
216
|
+
sodipodi:nodetypes="cccccccccccccc" />
|
|
217
|
+
<path
|
|
218
|
+
style="fill:url(#radialGradient6);stroke:#000000;stroke-width:0.5;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter68)"
|
|
219
|
+
d="m 21,18 -4,4 h 11 l 4,-4 V 14 L 28,10 H 23 L 21,8 23,6 h 5 L 32.029451,2 H 21.05087 L 17,5.8781794 V 10.014726 L 21,14 h 5 l 2,2 -2,2 z"
|
|
220
|
+
id="path2"
|
|
221
|
+
sodipodi:nodetypes="ccccccccccccccccccc" />
|
|
222
|
+
</g>
|
|
223
|
+
</svg>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { getDomainThemes } from './app/config/themes'
|
|
3
|
+
import { buildDarkOverrideCss, getDomainThemes } from './app/config/themes'
|
|
4
4
|
import { createBibleReferencePatterns } from './app/utils/bible-book-names'
|
|
5
5
|
import { runBuildFallbackHooks } from './scripts/renderer-hooks'
|
|
6
6
|
import { withBasePath } from './utils/base-url'
|
|
@@ -59,6 +59,16 @@ export default defineNuxtConfig({
|
|
|
59
59
|
app: {
|
|
60
60
|
baseURL: appBaseURL,
|
|
61
61
|
head: {
|
|
62
|
+
style: [
|
|
63
|
+
{ innerHTML: buildDarkOverrideCss() }
|
|
64
|
+
],
|
|
65
|
+
script: [
|
|
66
|
+
{
|
|
67
|
+
tagPosition: 'head',
|
|
68
|
+
tagPriority: 'critical',
|
|
69
|
+
innerHTML: `(function(){try{var t=localStorage.getItem('theme-preference');if(t!=='light'&&t!=='dark'){t=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';}document.documentElement.setAttribute('data-mdsite-theme',t);}catch(e){document.documentElement.setAttribute('data-mdsite-theme','light');}})();`
|
|
70
|
+
}
|
|
71
|
+
],
|
|
62
72
|
link: [
|
|
63
73
|
{ rel: 'icon', type: 'image/svg+xml', href: withBasePath('/favicon.svg', appBaseURL), sizes: 'any' },
|
|
64
74
|
{ rel: 'icon', type: 'image/x-icon', href: withBasePath('/favicon.ico', appBaseURL), sizes: '32x32' },
|
|
@@ -0,0 +1,123 @@
|
|
|
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, 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
|
+
})
|
|
@@ -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'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { loadMdsiteConfigSync, resolveMdsiteConfigPath } 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,48 +18,80 @@ 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() {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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'
|
|
54
|
+
|
|
55
|
+
const resolvedSource = resolveFaviconSource(contentDir, config.favicon ?? '')
|
|
56
|
+
|
|
57
|
+
if (!resolvedSource) {
|
|
58
|
+
console.error('❌ No favicon source available (configured source missing AND bundled default not found).')
|
|
30
59
|
return false
|
|
31
60
|
}
|
|
32
61
|
|
|
33
|
-
|
|
62
|
+
const { sourcePath } = resolvedSource
|
|
63
|
+
const publicDir = options.outputDir ?? path.resolve(__dirname, '..', 'public')
|
|
64
|
+
await fs.ensureDir(publicDir)
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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}`)
|
|
71
|
+
console.log(` Source: ${sourcePath}`)
|
|
72
|
+
console.log(` Output: ${publicDir}`)
|
|
38
73
|
|
|
39
74
|
try {
|
|
40
75
|
// Copy SVG as-is (for modern browsers)
|
|
41
|
-
const svgTargetPath = path.join(
|
|
42
|
-
await fs.copy(
|
|
76
|
+
const svgTargetPath = path.join(publicDir, 'favicon.svg')
|
|
77
|
+
await fs.copy(sourcePath, svgTargetPath)
|
|
43
78
|
console.log(` ✓ SVG: favicon.svg`)
|
|
44
79
|
|
|
45
80
|
// Generate ICO (32x32 with transparent padding)
|
|
46
|
-
const icoTargetPath = path.join(
|
|
47
|
-
const png32Buffer = await sharp(
|
|
81
|
+
const icoTargetPath = path.join(publicDir, 'favicon.ico')
|
|
82
|
+
const png32Buffer = await sharp(sourcePath)
|
|
48
83
|
.resize(32, 32, {
|
|
49
84
|
fit: 'contain',
|
|
50
85
|
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
51
86
|
})
|
|
52
87
|
.png()
|
|
53
88
|
.toBuffer()
|
|
54
|
-
|
|
55
89
|
await fs.writeFile(icoTargetPath, png32Buffer)
|
|
56
90
|
console.log(` ✓ ICO: favicon.ico`)
|
|
57
91
|
|
|
58
92
|
// Generate Apple Touch Icon (180x180 with transparent padding)
|
|
59
|
-
const appleTouchPath = path.join(
|
|
60
|
-
await sharp(
|
|
93
|
+
const appleTouchPath = path.join(publicDir, 'apple-touch-icon.png')
|
|
94
|
+
await sharp(sourcePath)
|
|
61
95
|
.resize(FAVICON_SIZES.appleTouchIcon, FAVICON_SIZES.appleTouchIcon, {
|
|
62
96
|
fit: 'contain',
|
|
63
97
|
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
@@ -67,8 +101,8 @@ export async function generateFavicons() {
|
|
|
67
101
|
console.log(` ✓ Apple Touch Icon: apple-touch-icon.png`)
|
|
68
102
|
|
|
69
103
|
// Generate PWA Icon 192x192
|
|
70
|
-
const icon192Path = path.join(
|
|
71
|
-
await sharp(
|
|
104
|
+
const icon192Path = path.join(publicDir, 'icon-192.png')
|
|
105
|
+
await sharp(sourcePath)
|
|
72
106
|
.resize(FAVICON_SIZES.pwaIcon192, FAVICON_SIZES.pwaIcon192, {
|
|
73
107
|
fit: 'contain',
|
|
74
108
|
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
@@ -78,8 +112,8 @@ export async function generateFavicons() {
|
|
|
78
112
|
console.log(` ✓ PWA Icon 192: icon-192.png`)
|
|
79
113
|
|
|
80
114
|
// Generate PWA Icon 512x512
|
|
81
|
-
const icon512Path = path.join(
|
|
82
|
-
await sharp(
|
|
115
|
+
const icon512Path = path.join(publicDir, 'icon-512.png')
|
|
116
|
+
await sharp(sourcePath)
|
|
83
117
|
.resize(FAVICON_SIZES.pwaIcon512, FAVICON_SIZES.pwaIcon512, {
|
|
84
118
|
fit: 'contain',
|
|
85
119
|
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
@@ -88,48 +122,14 @@ export async function generateFavicons() {
|
|
|
88
122
|
.toFile(icon512Path)
|
|
89
123
|
console.log(` ✓ PWA Icon 512: icon-512.png`)
|
|
90
124
|
|
|
91
|
-
console.log(`✅ Favicons generated successfully for ${
|
|
125
|
+
console.log(`✅ Favicons generated successfully for ${siteName}\n`)
|
|
92
126
|
return true
|
|
93
127
|
} catch (error) {
|
|
94
|
-
console.error(`❌ Failed to generate favicons for ${
|
|
128
|
+
console.error(`❌ Failed to generate favicons for ${siteName}:`, error)
|
|
95
129
|
return false
|
|
96
130
|
}
|
|
97
131
|
}
|
|
98
132
|
|
|
99
|
-
/**
|
|
100
|
-
* Copy favicon files from content submodule to public directory
|
|
101
|
-
*/
|
|
102
|
-
export async function copyFaviconsToPublic() {
|
|
103
|
-
const projectRoot = path.resolve(__dirname, '..')
|
|
104
|
-
const { contentDir, config } = loadMdsiteConfigSync()
|
|
105
|
-
const faviconDir = path.join(contentDir, 'favicon')
|
|
106
|
-
const publicDir = path.join(projectRoot, 'public')
|
|
107
|
-
|
|
108
|
-
console.log(`📋 Copying ${config.site.name} favicons to public...`)
|
|
109
|
-
|
|
110
|
-
const files = [
|
|
111
|
-
'favicon.svg',
|
|
112
|
-
'favicon.ico',
|
|
113
|
-
'apple-touch-icon.png',
|
|
114
|
-
'icon-192.png',
|
|
115
|
-
'icon-512.png'
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
for (const file of files) {
|
|
119
|
-
const sourcePath = path.join(faviconDir, file)
|
|
120
|
-
const targetPath = path.join(publicDir, file)
|
|
121
|
-
|
|
122
|
-
if (await fs.pathExists(sourcePath)) {
|
|
123
|
-
await fs.copy(sourcePath, targetPath)
|
|
124
|
-
console.log(` ✓ ${file}`)
|
|
125
|
-
} else {
|
|
126
|
-
console.warn(` ⚠ Missing: ${file}`)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log(`✅ Favicon copy complete\n`)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
133
|
/**
|
|
134
134
|
* Generate web manifest for PWA support
|
|
135
135
|
*/
|
|
@@ -183,7 +183,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
183
183
|
; (async () => {
|
|
184
184
|
const success = await generateFavicons()
|
|
185
185
|
if (success) {
|
|
186
|
-
await copyFaviconsToPublic()
|
|
187
186
|
await generateWebManifest(config.site.name)
|
|
188
187
|
} else {
|
|
189
188
|
process.exit(1)
|
|
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
buildContentDataMock,
|
|
7
|
-
copyFaviconsToPublicMock,
|
|
8
7
|
existsSyncMock,
|
|
9
8
|
generateFaviconsMock,
|
|
10
9
|
generateWebManifestMock,
|
|
@@ -20,7 +19,6 @@ const {
|
|
|
20
19
|
writeFileSyncMock,
|
|
21
20
|
} = vi.hoisted(() => ({
|
|
22
21
|
buildContentDataMock: vi.fn(),
|
|
23
|
-
copyFaviconsToPublicMock: vi.fn(),
|
|
24
22
|
existsSyncMock: vi.fn(),
|
|
25
23
|
generateFaviconsMock: vi.fn(),
|
|
26
24
|
generateWebManifestMock: vi.fn(),
|
|
@@ -60,7 +58,6 @@ vi.mock('./generate-indices.js', () => ({
|
|
|
60
58
|
}))
|
|
61
59
|
|
|
62
60
|
vi.mock('./generate-favicons.js', () => ({
|
|
63
|
-
copyFaviconsToPublic: copyFaviconsToPublicMock,
|
|
64
61
|
generateFavicons: generateFaviconsMock,
|
|
65
62
|
generateWebManifest: generateWebManifestMock,
|
|
66
63
|
}))
|
|
@@ -99,7 +96,6 @@ describe('renderer hooks orchestration', () => {
|
|
|
99
96
|
stringifyYamlMock.mockReturnValue('compat-config')
|
|
100
97
|
generateFaviconsMock.mockResolvedValue(true)
|
|
101
98
|
buildContentDataMock.mockResolvedValue(undefined)
|
|
102
|
-
copyFaviconsToPublicMock.mockResolvedValue(undefined)
|
|
103
99
|
generateWebManifestMock.mockResolvedValue(undefined)
|
|
104
100
|
startWatcherMock.mockResolvedValue(undefined)
|
|
105
101
|
syncContentMock.mockResolvedValue(undefined)
|
|
@@ -255,7 +251,6 @@ describe('renderer hooks orchestration', () => {
|
|
|
255
251
|
expect(syncContentMock).toHaveBeenCalledTimes(1)
|
|
256
252
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
257
253
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
258
|
-
expect(copyFaviconsToPublicMock).toHaveBeenCalledTimes(1)
|
|
259
254
|
expect(generateWebManifestMock).toHaveBeenCalledWith('Docs')
|
|
260
255
|
expect(syncContentMock.mock.invocationCallOrder[0]).toBeLessThan(buildContentDataMock.mock.invocationCallOrder[0])
|
|
261
256
|
expect(buildContentDataMock.mock.invocationCallOrder[0]).toBeLessThan(generateFaviconsMock.mock.invocationCallOrder[0])
|
|
@@ -268,7 +263,6 @@ describe('renderer hooks orchestration', () => {
|
|
|
268
263
|
expect(syncContentMock).toHaveBeenCalledTimes(1)
|
|
269
264
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
270
265
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
271
|
-
expect(copyFaviconsToPublicMock).toHaveBeenCalledTimes(1)
|
|
272
266
|
expect(generateWebManifestMock).toHaveBeenCalledWith('Docs')
|
|
273
267
|
expect(startWatcherMock).not.toHaveBeenCalled()
|
|
274
268
|
expect(rmMock).not.toHaveBeenCalled()
|
|
@@ -292,7 +286,6 @@ describe('renderer hooks orchestration', () => {
|
|
|
292
286
|
|
|
293
287
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
294
288
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
295
|
-
expect(copyFaviconsToPublicMock).not.toHaveBeenCalled()
|
|
296
289
|
expect(generateWebManifestMock).not.toHaveBeenCalled()
|
|
297
290
|
})
|
|
298
291
|
|
|
@@ -304,7 +297,6 @@ describe('renderer hooks orchestration', () => {
|
|
|
304
297
|
expect(syncContentMock).toHaveBeenCalledTimes(1)
|
|
305
298
|
expect(buildContentDataMock).toHaveBeenCalledTimes(1)
|
|
306
299
|
expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
|
|
307
|
-
expect(copyFaviconsToPublicMock).not.toHaveBeenCalled()
|
|
308
300
|
expect(generateWebManifestMock).not.toHaveBeenCalled()
|
|
309
301
|
expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
|
|
310
302
|
})
|
|
@@ -3,7 +3,7 @@ import path from 'path'
|
|
|
3
3
|
import YAML from 'yaml'
|
|
4
4
|
|
|
5
5
|
import { buildContentData } from './generate-indices.js'
|
|
6
|
-
import { generateFavicons,
|
|
6
|
+
import { generateFavicons, generateWebManifest } from './generate-favicons.js'
|
|
7
7
|
import { startWatcher, syncContent } from './sync-content.js'
|
|
8
8
|
import { loadMdsiteConfigSync, resolveMdsiteConfigPath } from '../utils/mdsite-config.js'
|
|
9
9
|
|
|
@@ -95,7 +95,6 @@ async function generateFaviconAssets(siteName: string): Promise<void> {
|
|
|
95
95
|
const success = await generateFavicons()
|
|
96
96
|
|
|
97
97
|
if (success) {
|
|
98
|
-
await copyFaviconsToPublic()
|
|
99
98
|
await generateWebManifest(siteName)
|
|
100
99
|
console.log(`✅ Favicons ready for ${siteName}\n`)
|
|
101
100
|
}
|