@life-and-dev/mdsite 0.0.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.
Files changed (133) hide show
  1. package/README.md +228 -0
  2. package/bin/mdsite.js +3 -0
  3. package/dist/commands/commands.test.d.ts +1 -0
  4. package/dist/commands/commands.test.js +302 -0
  5. package/dist/commands/commands.test.js.map +1 -0
  6. package/dist/commands/generate.d.ts +1 -0
  7. package/dist/commands/generate.js +20 -0
  8. package/dist/commands/generate.js.map +1 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +16 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/prepare.d.ts +1 -0
  13. package/dist/commands/prepare.js +111 -0
  14. package/dist/commands/prepare.js.map +1 -0
  15. package/dist/commands/prepare.test.d.ts +1 -0
  16. package/dist/commands/prepare.test.js +131 -0
  17. package/dist/commands/prepare.test.js.map +1 -0
  18. package/dist/commands/preview.d.ts +5 -0
  19. package/dist/commands/preview.js +62 -0
  20. package/dist/commands/preview.js.map +1 -0
  21. package/dist/commands/start.d.ts +5 -0
  22. package/dist/commands/start.js +50 -0
  23. package/dist/commands/start.js.map +1 -0
  24. package/dist/commands/stop.d.ts +1 -0
  25. package/dist/commands/stop.js +26 -0
  26. package/dist/commands/stop.js.map +1 -0
  27. package/dist/commands/workflows.test.d.ts +1 -0
  28. package/dist/commands/workflows.test.js +183 -0
  29. package/dist/commands/workflows.test.js.map +1 -0
  30. package/dist/config/default-mdsite-config.d.ts +2 -0
  31. package/dist/config/default-mdsite-config.js +82 -0
  32. package/dist/config/default-mdsite-config.js.map +1 -0
  33. package/dist/config/default-mdsite-config.test.d.ts +1 -0
  34. package/dist/config/default-mdsite-config.test.js +42 -0
  35. package/dist/config/default-mdsite-config.test.js.map +1 -0
  36. package/dist/config/mdsite-config.d.ts +42 -0
  37. package/dist/config/mdsite-config.js +74 -0
  38. package/dist/config/mdsite-config.js.map +1 -0
  39. package/dist/config/mdsite-config.test.d.ts +1 -0
  40. package/dist/config/mdsite-config.test.js +115 -0
  41. package/dist/config/mdsite-config.test.js.map +1 -0
  42. package/dist/config/menu.d.ts +3 -0
  43. package/dist/config/menu.js +58 -0
  44. package/dist/config/menu.js.map +1 -0
  45. package/dist/config/menu.test.d.ts +1 -0
  46. package/dist/config/menu.test.js +53 -0
  47. package/dist/config/menu.test.js.map +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +99 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/index.test.d.ts +1 -0
  52. package/dist/index.test.js +133 -0
  53. package/dist/index.test.js.map +1 -0
  54. package/dist/process/child-process.d.ts +9 -0
  55. package/dist/process/child-process.js +142 -0
  56. package/dist/process/child-process.js.map +1 -0
  57. package/dist/process/child-process.test.d.ts +1 -0
  58. package/dist/process/child-process.test.js +131 -0
  59. package/dist/process/child-process.test.js.map +1 -0
  60. package/dist/process/runtime-state.d.ts +16 -0
  61. package/dist/process/runtime-state.js +42 -0
  62. package/dist/process/runtime-state.js.map +1 -0
  63. package/dist/process/runtime-state.test.d.ts +1 -0
  64. package/dist/process/runtime-state.test.js +57 -0
  65. package/dist/process/runtime-state.test.js.map +1 -0
  66. package/dist/renderer/mdsite-nuxt.d.ts +22 -0
  67. package/dist/renderer/mdsite-nuxt.js +194 -0
  68. package/dist/renderer/mdsite-nuxt.js.map +1 -0
  69. package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
  70. package/dist/renderer/mdsite-nuxt.test.js +226 -0
  71. package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
  72. package/mdsite-nuxt/app/app.vue +7 -0
  73. package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
  74. package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
  75. package/mdsite-nuxt/app/assets/css/print.css +133 -0
  76. package/mdsite-nuxt/app/components/AppBar.vue +138 -0
  77. package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
  78. package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
  79. package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
  80. package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
  81. package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
  82. package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
  83. package/mdsite-nuxt/app/components/TocItem.vue +75 -0
  84. package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
  85. package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
  86. package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
  87. package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
  88. package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
  89. package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
  90. package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
  91. package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
  92. package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
  93. package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
  94. package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
  95. package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
  96. package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
  97. package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
  98. package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
  99. package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
  100. package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
  101. package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
  102. package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
  103. package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
  104. package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
  105. package/mdsite-nuxt/app/config/themes.ts +17 -0
  106. package/mdsite-nuxt/app/layouts/default.vue +376 -0
  107. package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
  108. package/mdsite-nuxt/app/pages/index.vue +54 -0
  109. package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
  110. package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
  111. package/mdsite-nuxt/app/types/content.d.ts +10 -0
  112. package/mdsite-nuxt/app/types/table.ts +57 -0
  113. package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
  114. package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
  115. package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
  116. package/mdsite-nuxt/content.config.ts +18 -0
  117. package/mdsite-nuxt/example.config.yml +67 -0
  118. package/mdsite-nuxt/nuxt.config.ts +247 -0
  119. package/mdsite-nuxt/package-lock.json +15922 -0
  120. package/mdsite-nuxt/package.json +50 -0
  121. package/mdsite-nuxt/playwright.config.js +47 -0
  122. package/mdsite-nuxt/public/robots.txt +2 -0
  123. package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
  124. package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
  125. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
  126. package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
  127. package/mdsite-nuxt/scripts/start.test.ts +164 -0
  128. package/mdsite-nuxt/scripts/start.ts +55 -0
  129. package/mdsite-nuxt/scripts/sync-content.ts +504 -0
  130. package/mdsite-nuxt/tsconfig.json +18 -0
  131. package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
  132. package/mdsite-nuxt/vitest.config.ts +7 -0
  133. package/package.json +36 -0
@@ -0,0 +1,277 @@
1
+ import path from 'node:path'
2
+
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ const {
6
+ buildContentDataMock,
7
+ copyFaviconsToPublicMock,
8
+ existsSyncMock,
9
+ generateFaviconsMock,
10
+ generateWebManifestMock,
11
+ loadMdsiteConfigSyncMock,
12
+ parseYamlMock,
13
+ readFileSyncMock,
14
+ resolveMdsiteConfigPathMock,
15
+ rmMock,
16
+ startWatcherMock,
17
+ statSyncMock,
18
+ stringifyYamlMock,
19
+ syncContentMock,
20
+ writeFileSyncMock,
21
+ } = vi.hoisted(() => ({
22
+ buildContentDataMock: vi.fn(),
23
+ copyFaviconsToPublicMock: vi.fn(),
24
+ existsSyncMock: vi.fn(),
25
+ generateFaviconsMock: vi.fn(),
26
+ generateWebManifestMock: vi.fn(),
27
+ loadMdsiteConfigSyncMock: vi.fn(),
28
+ parseYamlMock: vi.fn(),
29
+ readFileSyncMock: vi.fn(),
30
+ resolveMdsiteConfigPathMock: vi.fn(),
31
+ rmMock: vi.fn(),
32
+ startWatcherMock: vi.fn(),
33
+ statSyncMock: vi.fn(),
34
+ stringifyYamlMock: vi.fn(),
35
+ syncContentMock: vi.fn(),
36
+ writeFileSyncMock: vi.fn(),
37
+ }))
38
+
39
+ vi.mock('fs', () => ({
40
+ default: {
41
+ existsSync: existsSyncMock,
42
+ statSync: statSyncMock,
43
+ readFileSync: readFileSyncMock,
44
+ writeFileSync: writeFileSyncMock,
45
+ promises: {
46
+ rm: rmMock,
47
+ },
48
+ },
49
+ }))
50
+
51
+ vi.mock('yaml', () => ({
52
+ default: {
53
+ parse: parseYamlMock,
54
+ stringify: stringifyYamlMock,
55
+ },
56
+ }))
57
+
58
+ vi.mock('./generate-indices.js', () => ({
59
+ buildContentData: buildContentDataMock,
60
+ }))
61
+
62
+ vi.mock('./generate-favicons.js', () => ({
63
+ copyFaviconsToPublic: copyFaviconsToPublicMock,
64
+ generateFavicons: generateFaviconsMock,
65
+ generateWebManifest: generateWebManifestMock,
66
+ }))
67
+
68
+ vi.mock('./sync-content.js', () => ({
69
+ startWatcher: startWatcherMock,
70
+ syncContent: syncContentMock,
71
+ }))
72
+
73
+ vi.mock('../utils/mdsite-config.js', () => ({
74
+ loadMdsiteConfigSync: loadMdsiteConfigSyncMock,
75
+ resolveMdsiteConfigPath: resolveMdsiteConfigPathMock,
76
+ }))
77
+
78
+ import {
79
+ prepareRendererRuntime,
80
+ runBuildFallbackHooks,
81
+ runSetupHooks,
82
+ } from './renderer-hooks.js'
83
+
84
+ describe('renderer hooks orchestration', () => {
85
+ const originalEnv = { ...process.env }
86
+
87
+ beforeEach(() => {
88
+ vi.clearAllMocks()
89
+ process.env = { ...originalEnv }
90
+
91
+ resolveMdsiteConfigPathMock.mockReturnValue('/renderer/mdsite.yml')
92
+ loadMdsiteConfigSyncMock.mockReturnValue({
93
+ config: { site: { name: 'Docs' } },
94
+ configPath: '/renderer/mdsite.yml',
95
+ contentDir: '/renderer/docs',
96
+ })
97
+ existsSyncMock.mockImplementation((target: string) => target === '/renderer/docs')
98
+ statSyncMock.mockReturnValue({ isFile: () => true })
99
+ stringifyYamlMock.mockReturnValue('compat-config')
100
+ generateFaviconsMock.mockResolvedValue(true)
101
+ buildContentDataMock.mockResolvedValue(undefined)
102
+ copyFaviconsToPublicMock.mockResolvedValue(undefined)
103
+ generateWebManifestMock.mockResolvedValue(undefined)
104
+ startWatcherMock.mockResolvedValue(undefined)
105
+ syncContentMock.mockResolvedValue(undefined)
106
+ rmMock.mockResolvedValue(undefined)
107
+ })
108
+
109
+ it('prepares runtime from the resolved mdsite config and exports stable env values', () => {
110
+ const runtime = prepareRendererRuntime('/renderer', {
111
+ configPath: '/renderer/mdsite.yml',
112
+ contentPath: '/input/docs',
113
+ })
114
+
115
+ expect(resolveMdsiteConfigPathMock).toHaveBeenCalledWith({
116
+ configPath: '/renderer/mdsite.yml',
117
+ contentPath: '/input/docs',
118
+ })
119
+ expect(loadMdsiteConfigSyncMock).toHaveBeenCalledWith({
120
+ configPath: '/renderer/mdsite.yml',
121
+ contentPath: '/input/docs',
122
+ })
123
+ expect(runtime).toEqual({
124
+ config: { site: { name: 'Docs' } },
125
+ configPath: '/renderer/mdsite.yml',
126
+ contentDir: '/renderer/docs',
127
+ rootDir: '/renderer',
128
+ })
129
+ expect(process.env.NUXT_CONTENT_PATH).toBe('/renderer/docs')
130
+ expect(process.env.CONTENT_DIR).toBe('/renderer/docs')
131
+ expect(process.env.MDSITE_CONFIG_PATH).toBe('/renderer/mdsite.yml')
132
+ })
133
+
134
+ it('falls back to the legacy compatibility config when no explicit mdsite config resolves', () => {
135
+ resolveMdsiteConfigPathMock.mockReturnValue(undefined)
136
+ existsSyncMock.mockImplementation((target: string) => (
137
+ target === '/renderer/content.config.yml' || target === path.join('/renderer', 'legacy-docs')
138
+ ))
139
+ parseYamlMock.mockReturnValue({
140
+ content: {
141
+ path: 'legacy-docs',
142
+ },
143
+ siteName: 'Legacy Docs',
144
+ })
145
+ loadMdsiteConfigSyncMock.mockReturnValue({
146
+ config: { site: { name: 'Legacy Docs' } },
147
+ configPath: '/renderer/.mdsite-compat.yml',
148
+ contentDir: path.join('/renderer', 'legacy-docs'),
149
+ })
150
+
151
+ const runtime = prepareRendererRuntime('/renderer')
152
+
153
+ expect(readFileSyncMock).toHaveBeenCalledWith('/renderer/content.config.yml', 'utf8')
154
+ expect(writeFileSyncMock).toHaveBeenCalledWith('/renderer/.mdsite-compat.yml', 'compat-config', 'utf8')
155
+ expect(loadMdsiteConfigSyncMock).toHaveBeenCalledWith({
156
+ configPath: '/renderer/.mdsite-compat.yml',
157
+ contentPath: path.join('/renderer', 'legacy-docs'),
158
+ })
159
+ expect(runtime.configPath).toBe('/renderer/.mdsite-compat.yml')
160
+ expect(process.env.MDSITE_CONFIG_PATH).toBe('/renderer/.mdsite-compat.yml')
161
+ expect(process.env.NUXT_CONTENT_PATH).toBe(path.join('/renderer', 'legacy-docs'))
162
+ })
163
+
164
+ it('exits when no renderer config can be resolved', () => {
165
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
166
+ const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number) => {
167
+ throw new Error(`process.exit(${code ?? 0})`)
168
+ })
169
+
170
+ resolveMdsiteConfigPathMock.mockReturnValue(undefined)
171
+ existsSyncMock.mockReturnValue(false)
172
+
173
+ expect(() => prepareRendererRuntime('/renderer')).toThrow('process.exit(1)')
174
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml configuration found'))
175
+
176
+ consoleErrorSpy.mockRestore()
177
+ processExitSpy.mockRestore()
178
+ })
179
+
180
+ it('exits when the resolved content directory does not exist', () => {
181
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
182
+ const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number) => {
183
+ throw new Error(`process.exit(${code ?? 0})`)
184
+ })
185
+
186
+ existsSyncMock.mockReturnValue(false)
187
+
188
+ expect(() => prepareRendererRuntime('/renderer')).toThrow('process.exit(1)')
189
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Content directory not found'))
190
+
191
+ consoleErrorSpy.mockRestore()
192
+ processExitSpy.mockRestore()
193
+ })
194
+
195
+ it('runs the dev wrapper contract and clears .data for uncached dev startup', async () => {
196
+ const runtime = await runSetupHooks('dev', '/renderer')
197
+
198
+ expect(rmMock).toHaveBeenCalledWith(path.join('/renderer', '.data'), {
199
+ force: true,
200
+ recursive: true,
201
+ })
202
+ expect(startWatcherMock).toHaveBeenCalledTimes(1)
203
+ expect(syncContentMock).not.toHaveBeenCalled()
204
+ expect(buildContentDataMock).not.toHaveBeenCalled()
205
+ expect(generateFaviconsMock).not.toHaveBeenCalled()
206
+ expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
207
+ expect(runtime.contentDir).toBe('/renderer/docs')
208
+ })
209
+
210
+ it('keeps cached dev startup on the same wrapper contract without clearing .data', async () => {
211
+ await runSetupHooks('dev', '/renderer', { cached: true })
212
+
213
+ expect(rmMock).not.toHaveBeenCalled()
214
+ expect(startWatcherMock).toHaveBeenCalledTimes(1)
215
+ expect(syncContentMock).not.toHaveBeenCalled()
216
+ })
217
+
218
+ it('runs non-dev setup hooks in orchestration order and publishes favicon assets on success', async () => {
219
+ await runSetupHooks('generate', '/renderer', { configPath: '/renderer/mdsite.yml' })
220
+
221
+ expect(syncContentMock).toHaveBeenCalledTimes(1)
222
+ expect(buildContentDataMock).toHaveBeenCalledTimes(1)
223
+ expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
224
+ expect(copyFaviconsToPublicMock).toHaveBeenCalledTimes(1)
225
+ expect(generateWebManifestMock).toHaveBeenCalledWith('Docs')
226
+ expect(syncContentMock.mock.invocationCallOrder[0]).toBeLessThan(buildContentDataMock.mock.invocationCallOrder[0])
227
+ expect(buildContentDataMock.mock.invocationCallOrder[0]).toBeLessThan(generateFaviconsMock.mock.invocationCallOrder[0])
228
+ expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
229
+ })
230
+
231
+ it('runs setup mode through the same non-dev orchestration path as build and generate', async () => {
232
+ await runSetupHooks('setup', '/renderer', { configPath: '/renderer/mdsite.yml' })
233
+
234
+ expect(syncContentMock).toHaveBeenCalledTimes(1)
235
+ expect(buildContentDataMock).toHaveBeenCalledTimes(1)
236
+ expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
237
+ expect(copyFaviconsToPublicMock).toHaveBeenCalledTimes(1)
238
+ expect(generateWebManifestMock).toHaveBeenCalledWith('Docs')
239
+ expect(startWatcherMock).not.toHaveBeenCalled()
240
+ expect(rmMock).not.toHaveBeenCalled()
241
+ expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
242
+ })
243
+
244
+ it('marks orchestration complete for build mode on the same shared non-dev path', async () => {
245
+ await runSetupHooks('build', '/renderer', { configPath: '/renderer/mdsite.yml' })
246
+
247
+ expect(syncContentMock).toHaveBeenCalledTimes(1)
248
+ expect(buildContentDataMock).toHaveBeenCalledTimes(1)
249
+ expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
250
+ expect(startWatcherMock).not.toHaveBeenCalled()
251
+ expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
252
+ })
253
+
254
+ it('runs build fallback hooks without publishing favicon assets when generation reports no output', async () => {
255
+ generateFaviconsMock.mockResolvedValue(false)
256
+
257
+ await runBuildFallbackHooks('Docs')
258
+
259
+ expect(buildContentDataMock).toHaveBeenCalledTimes(1)
260
+ expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
261
+ expect(copyFaviconsToPublicMock).not.toHaveBeenCalled()
262
+ expect(generateWebManifestMock).not.toHaveBeenCalled()
263
+ })
264
+
265
+ it('keeps non-dev orchestration error-safe when favicon generation reports no output', async () => {
266
+ generateFaviconsMock.mockResolvedValue(false)
267
+
268
+ await runSetupHooks('generate', '/renderer', { configPath: '/renderer/mdsite.yml' })
269
+
270
+ expect(syncContentMock).toHaveBeenCalledTimes(1)
271
+ expect(buildContentDataMock).toHaveBeenCalledTimes(1)
272
+ expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
273
+ expect(copyFaviconsToPublicMock).not.toHaveBeenCalled()
274
+ expect(generateWebManifestMock).not.toHaveBeenCalled()
275
+ expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
276
+ })
277
+ })
@@ -0,0 +1,141 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import YAML from 'yaml'
4
+
5
+ import { buildContentData } from './generate-indices.js'
6
+ import { generateFavicons, copyFaviconsToPublic, generateWebManifest } from './generate-favicons.js'
7
+ import { startWatcher, syncContent } from './sync-content.js'
8
+ import { loadMdsiteConfigSync, resolveMdsiteConfigPath } from '../utils/mdsite-config.js'
9
+
10
+ export interface RendererRuntime {
11
+ config: ReturnType<typeof loadMdsiteConfigSync>['config']
12
+ configPath: string
13
+ contentDir: string
14
+ rootDir: string
15
+ }
16
+
17
+ export function prepareRendererRuntime(rootDir: string, options: {
18
+ configPath?: string
19
+ contentPath?: string
20
+ } = {}): RendererRuntime {
21
+ let configPath = resolveMdsiteConfigPath({
22
+ configPath: options.configPath,
23
+ contentPath: options.contentPath ?? process.env.NUXT_CONTENT_PATH
24
+ })
25
+
26
+ if (!configPath) {
27
+ configPath = ensureLegacyCompatibilityConfig(rootDir)
28
+ }
29
+
30
+ if (!configPath) {
31
+ console.error('❌ No mdsite.yml configuration found. Set MDSITE_CONFIG_PATH or pass a mdsite.yml path.')
32
+ process.exit(1)
33
+ }
34
+
35
+ const { config, contentDir } = loadMdsiteConfigSync({
36
+ configPath,
37
+ contentPath: options.contentPath ?? process.env.NUXT_CONTENT_PATH
38
+ })
39
+
40
+ process.env.NUXT_CONTENT_PATH = contentDir
41
+ process.env.CONTENT_DIR = contentDir
42
+ process.env.MDSITE_CONFIG_PATH = configPath
43
+
44
+ console.log(`🚀 Preparing site: ${config.site.name}`)
45
+ console.log(`📂 Content path: ${contentDir}`)
46
+ console.log(`📑 Config file: ${configPath}`)
47
+
48
+ if (!fs.existsSync(contentDir)) {
49
+ console.error(`❌ Content directory not found: ${contentDir}`)
50
+ process.exit(1)
51
+ }
52
+
53
+ return {
54
+ config,
55
+ configPath,
56
+ contentDir,
57
+ rootDir
58
+ }
59
+ }
60
+
61
+ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev', rootDir: string, options: {
62
+ cached?: boolean
63
+ configPath?: string
64
+ contentPath?: string
65
+ } = {}): Promise<RendererRuntime> {
66
+ const runtime = prepareRendererRuntime(rootDir, options)
67
+
68
+ if (mode === 'dev') {
69
+ if (!options.cached) {
70
+ await fs.promises.rm(path.join(rootDir, '.data'), { recursive: true, force: true })
71
+ }
72
+
73
+ process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
74
+ await startWatcher()
75
+ return runtime
76
+ }
77
+
78
+ await syncContent()
79
+ console.log(`\n🔨 Generating navigation and search index...`)
80
+ await buildContentData()
81
+ await generateFaviconAssets(runtime.config.site.name)
82
+ process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
83
+
84
+ return runtime
85
+ }
86
+
87
+ export async function runBuildFallbackHooks(siteName: string): Promise<void> {
88
+ console.log(`\n🔨 Generating navigation and search index...`)
89
+ await buildContentData()
90
+ await generateFaviconAssets(siteName)
91
+ }
92
+
93
+ async function generateFaviconAssets(siteName: string): Promise<void> {
94
+ console.log(`\n🎨 Generating favicons for build...`)
95
+ const success = await generateFavicons()
96
+
97
+ if (success) {
98
+ await copyFaviconsToPublic()
99
+ await generateWebManifest(siteName)
100
+ console.log(`✅ Favicons ready for ${siteName}\n`)
101
+ }
102
+ }
103
+
104
+ function ensureLegacyCompatibilityConfig(rootDir: string): string | undefined {
105
+ const legacyConfigPath = path.join(rootDir, 'content.config.yml')
106
+
107
+ if (!fs.existsSync(legacyConfigPath) || !fs.statSync(legacyConfigPath).isFile()) {
108
+ return undefined
109
+ }
110
+
111
+ const legacyConfig = YAML.parse(fs.readFileSync(legacyConfigPath, 'utf8')) ?? {}
112
+ const contentDir = path.resolve(rootDir, legacyConfig.content?.path || legacyConfig.content?.git?.path || 'docs')
113
+ const compatibilityConfigPath = path.join(rootDir, '.mdsite-compat.yml')
114
+ const compatibilityConfig = {
115
+ favicon: '',
116
+ features: {
117
+ bibleTooltips: legacyConfig.features?.bibleTooltips ?? true,
118
+ sourceEdit: legacyConfig.features?.sourceEdit ?? true
119
+ },
120
+ menu: [],
121
+ server: {
122
+ output: '.output',
123
+ path: '.mdsite',
124
+ repo: legacyConfig.content?.git?.repo || ''
125
+ },
126
+ site: {
127
+ canonical: legacyConfig.site?.canonical || '',
128
+ name: legacyConfig.site?.name || path.basename(contentDir) || 'Site'
129
+ },
130
+ themes: legacyConfig.themes || {}
131
+ }
132
+
133
+ fs.writeFileSync(compatibilityConfigPath, YAML.stringify(compatibilityConfig), 'utf8')
134
+ process.env.NUXT_CONTENT_PATH = contentDir
135
+ process.env.CONTENT_DIR = contentDir
136
+ process.env.MDSITE_CONFIG_PATH = compatibilityConfigPath
137
+
138
+ console.warn(`⚠️ Using legacy compatibility fallback from ${legacyConfigPath}`)
139
+
140
+ return compatibilityConfigPath
141
+ }
@@ -0,0 +1,164 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ const {
7
+ onMock,
8
+ prepareRendererRuntimeMock,
9
+ runSetupHooksMock,
10
+ spawnMock,
11
+ } = vi.hoisted(() => ({
12
+ onMock: vi.fn(),
13
+ prepareRendererRuntimeMock: vi.fn(),
14
+ runSetupHooksMock: vi.fn(),
15
+ spawnMock: vi.fn(),
16
+ }))
17
+
18
+ vi.mock('child_process', () => ({
19
+ spawn: spawnMock,
20
+ }))
21
+
22
+ vi.mock('./renderer-hooks.js', () => ({
23
+ prepareRendererRuntime: prepareRendererRuntimeMock,
24
+ runSetupHooks: runSetupHooksMock,
25
+ }))
26
+
27
+ describe('renderer start wrapper', () => {
28
+ const originalArgv = [...process.argv]
29
+ const originalEnv = { ...process.env }
30
+ const rootDir = path.resolve(process.cwd())
31
+
32
+ beforeEach(() => {
33
+ vi.clearAllMocks()
34
+ vi.resetModules()
35
+
36
+ process.argv = originalArgv.slice(0, 2)
37
+ process.env = { ...originalEnv }
38
+
39
+ runSetupHooksMock.mockResolvedValue(undefined)
40
+ prepareRendererRuntimeMock.mockReturnValue(undefined)
41
+ spawnMock.mockReturnValue({ on: onMock })
42
+ })
43
+
44
+ async function importStartWithArgs(args: string[]) {
45
+ process.argv = ['node', 'scripts/start.ts', ...args]
46
+ await import('./start.ts')
47
+ }
48
+
49
+ it('wraps default dev startup through runSetupHooks before spawning nuxt dev', async () => {
50
+ await importStartWithArgs([])
51
+
52
+ expect(runSetupHooksMock).toHaveBeenCalledWith('dev', rootDir, {
53
+ cached: false,
54
+ configPath: undefined,
55
+ })
56
+ expect(prepareRendererRuntimeMock).not.toHaveBeenCalled()
57
+ expect(spawnMock).toHaveBeenCalledWith('npx', ['nuxt', 'dev'], {
58
+ cwd: rootDir,
59
+ env: process.env,
60
+ stdio: 'inherit',
61
+ })
62
+ })
63
+
64
+ it('keeps cached dev startup on the same wrapper contract while switching the nuxt command', async () => {
65
+ await importStartWithArgs(['--cached'])
66
+
67
+ expect(runSetupHooksMock).toHaveBeenCalledWith('dev', rootDir, {
68
+ cached: true,
69
+ configPath: undefined,
70
+ })
71
+ expect(spawnMock).toHaveBeenCalledWith('npx', ['nuxt', 'dev:cached'], {
72
+ cwd: rootDir,
73
+ env: process.env,
74
+ stdio: 'inherit',
75
+ })
76
+ })
77
+
78
+ it('routes generate through setup hooks before spawning nuxt generate', async () => {
79
+ await importStartWithArgs(['--generate', 'config/site.yml'])
80
+
81
+ expect(runSetupHooksMock).toHaveBeenCalledWith('generate', rootDir, {
82
+ configPath: 'config/site.yml',
83
+ })
84
+ expect(spawnMock).toHaveBeenCalledWith('npx', ['nuxt', 'generate'], {
85
+ cwd: rootDir,
86
+ env: process.env,
87
+ stdio: 'inherit',
88
+ })
89
+ })
90
+
91
+ it('routes build through setup hooks before spawning nuxt build', async () => {
92
+ await importStartWithArgs(['--build', 'config/site.yml'])
93
+
94
+ expect(runSetupHooksMock).toHaveBeenCalledWith('build', rootDir, {
95
+ configPath: 'config/site.yml',
96
+ })
97
+ expect(prepareRendererRuntimeMock).not.toHaveBeenCalled()
98
+ expect(spawnMock).toHaveBeenCalledWith('npx', ['nuxt', 'build'], {
99
+ cwd: rootDir,
100
+ env: process.env,
101
+ stdio: 'inherit',
102
+ })
103
+ })
104
+
105
+ it('routes preview through the renderer runtime wrapper before spawning nuxt preview', async () => {
106
+ await importStartWithArgs(['--preview', 'config/site.yml'])
107
+
108
+ expect(prepareRendererRuntimeMock).toHaveBeenCalledWith(rootDir, {
109
+ configPath: 'config/site.yml',
110
+ })
111
+ expect(runSetupHooksMock).not.toHaveBeenCalled()
112
+ expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
113
+ expect(spawnMock).toHaveBeenCalledWith('npx', ['nuxt', 'preview'], {
114
+ cwd: rootDir,
115
+ env: process.env,
116
+ stdio: 'inherit',
117
+ })
118
+ })
119
+
120
+ it('supports the explicit prepare hook without spawning nuxt', async () => {
121
+ const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number) => {
122
+ throw new Error(`process.exit(${code ?? 0})`)
123
+ })
124
+
125
+ await expect(importStartWithArgs(['--prepare', 'config/site.yml'])).rejects.toThrow('process.exit(0)')
126
+
127
+ expect(runSetupHooksMock).toHaveBeenCalledWith('setup', rootDir, {
128
+ configPath: 'config/site.yml',
129
+ })
130
+ expect(spawnMock).not.toHaveBeenCalled()
131
+
132
+ processExitSpy.mockRestore()
133
+ })
134
+
135
+ it('exposes stable CLI-facing script names in package.json', () => {
136
+ const packageJson = JSON.parse(readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
137
+
138
+ expect(packageJson.scripts).toMatchObject({
139
+ build: 'tsx scripts/start.ts --build',
140
+ 'build:renderer': 'npm run build',
141
+ dev: 'tsx scripts/start.ts',
142
+ 'dev:cached': 'tsx scripts/start.ts --cached',
143
+ generate: 'tsx scripts/start.ts --generate',
144
+ 'prepare:renderer': 'tsx scripts/start.ts --prepare',
145
+ preview: 'tsx scripts/start.ts --preview',
146
+ setup: 'npm run prepare:renderer',
147
+ 'sync-content': 'tsx scripts/sync-content.ts --sync',
148
+ 'generate-indices': 'tsx scripts/generate-indices.ts',
149
+ 'generate-favicons': 'npm run favicon',
150
+ })
151
+ })
152
+
153
+ it('exposes normalized backend hook script aliases in package.json', () => {
154
+ const packageJson = JSON.parse(readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
155
+
156
+ expect(packageJson.scripts).toMatchObject({
157
+ 'build:renderer': 'npm run build',
158
+ 'prepare:renderer': 'tsx scripts/start.ts --prepare',
159
+ 'prepare:nuxt': 'nuxt prepare',
160
+ setup: 'npm run prepare:renderer',
161
+ start: 'tsx scripts/start.ts',
162
+ })
163
+ })
164
+ })
@@ -0,0 +1,55 @@
1
+ import { spawn } from 'child_process'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ import { prepareRendererRuntime, runSetupHooks } from './renderer-hooks.js'
6
+
7
+ const __filename = fileURLToPath(import.meta.url)
8
+ const __dirname = path.dirname(__filename)
9
+ const rootDir = path.resolve(__dirname, '..')
10
+
11
+
12
+ // Parse arguments
13
+ const args = process.argv.slice(2)
14
+ const isBuild = args.includes('--build')
15
+ const isGenerate = args.includes('--generate')
16
+ const isCached = args.includes('--cached')
17
+ const isPreview = args.includes('--preview')
18
+ const isSetup = args.includes('--setup') || args.includes('--prepare')
19
+ const configArg = args.find(arg => !arg.startsWith('--') && /\.ya?ml$/i.test(arg))
20
+
21
+ // Determine the command to run
22
+ let nuxtCommand = 'dev'
23
+ if (isBuild) nuxtCommand = 'build'
24
+ if (isGenerate) nuxtCommand = 'generate'
25
+ if (isPreview) nuxtCommand = 'preview'
26
+ if (isCached && !isBuild && !isGenerate) nuxtCommand = 'dev:cached'
27
+
28
+ if (isSetup) {
29
+ await runSetupHooks('setup', rootDir, { configPath: configArg })
30
+ process.exit(0)
31
+ }
32
+
33
+ if (nuxtCommand.startsWith('dev')) {
34
+ await runSetupHooks('dev', rootDir, {
35
+ cached: isCached,
36
+ configPath: configArg
37
+ })
38
+ } else if (nuxtCommand === 'build' || nuxtCommand === 'generate') {
39
+ await runSetupHooks(nuxtCommand, rootDir, { configPath: configArg })
40
+ } else {
41
+ prepareRendererRuntime(rootDir, { configPath: configArg })
42
+ process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
43
+ }
44
+
45
+ console.log(`✨ Starting Nuxt ${nuxtCommand.toUpperCase()}...`)
46
+
47
+ const nuxtProcess = spawn('npx', ['nuxt', nuxtCommand], {
48
+ cwd: rootDir,
49
+ stdio: 'inherit',
50
+ env: process.env
51
+ })
52
+
53
+ nuxtProcess.on('close', (code) => {
54
+ process.exit(code ?? 0)
55
+ })