methanol 0.0.0 → 0.0.2

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 (48) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +58 -0
  3. package/banner.txt +6 -0
  4. package/bin/methanol.js +24 -0
  5. package/index.js +22 -0
  6. package/package.json +51 -9
  7. package/src/assets.js +30 -0
  8. package/src/build-system.js +200 -0
  9. package/src/components.js +145 -0
  10. package/src/config.js +396 -0
  11. package/src/dev-server.js +632 -0
  12. package/src/main.js +133 -0
  13. package/src/mdx.js +406 -0
  14. package/src/node-loader.js +88 -0
  15. package/src/pagefind.js +107 -0
  16. package/src/pages.js +771 -0
  17. package/src/preview-server.js +58 -0
  18. package/src/public-assets.js +73 -0
  19. package/src/register-loader.js +29 -0
  20. package/src/rehype-plugins/link-resolve.js +116 -0
  21. package/src/rehype-plugins/methanol-ctx.js +89 -0
  22. package/src/renderer.js +25 -0
  23. package/src/rewind.js +117 -0
  24. package/src/stage-logger.js +59 -0
  25. package/src/state.js +179 -0
  26. package/src/virtual-module/inject.js +30 -0
  27. package/src/virtual-module/loader.js +116 -0
  28. package/src/virtual-module/pagefind.js +108 -0
  29. package/src/vite-plugins.js +173 -0
  30. package/themes/default/components/ThemeAccentSwitch.client.jsx +95 -0
  31. package/themes/default/components/ThemeAccentSwitch.static.jsx +23 -0
  32. package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
  33. package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
  34. package/themes/default/components/ThemeSearchBox.client.jsx +324 -0
  35. package/themes/default/components/ThemeSearchBox.static.jsx +40 -0
  36. package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
  37. package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
  38. package/themes/default/components/pre.client.jsx +84 -0
  39. package/themes/default/components/pre.static.jsx +27 -0
  40. package/themes/default/heading.jsx +35 -0
  41. package/themes/default/index.js +41 -0
  42. package/themes/default/page.jsx +303 -0
  43. package/themes/default/pages/404.mdx +8 -0
  44. package/themes/default/pages/index.mdx +31 -0
  45. package/themes/default/public/favicon.png +0 -0
  46. package/themes/default/public/logo.png +0 -0
  47. package/themes/default/sources/prefetch.js +49 -0
  48. package/themes/default/sources/style.css +1660 -0
@@ -0,0 +1,145 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import { readdir, stat } from 'fs/promises'
22
+ import { existsSync } from 'fs'
23
+ import { join, extname, basename } from 'path'
24
+ import { pathToFileURL } from 'url'
25
+ import { env } from './rewind.js'
26
+ import { state } from './state.js'
27
+
28
+ const normalizeComponentName = (value) => basename(value)
29
+ const isInternalComponentName = (name) => {
30
+ const normalized = normalizeComponentName(name)
31
+ return normalized.startsWith('_') || normalized.startsWith('.')
32
+ }
33
+ const isIgnoredEntry = (name) => name.startsWith('.')
34
+
35
+ const COMPONENT_NAME_PATTERN = /^[^.]+(?:\.(client|static))?\.(jsx?|tsx?)$/i
36
+
37
+ export const isComponentFile = (name) =>
38
+ /\.(jsx?|tsx?)$/i.test(normalizeComponentName(name)) &&
39
+ !isInternalComponentName(name) &&
40
+ COMPONENT_NAME_PATTERN.test(normalizeComponentName(name))
41
+ export const isClientComponent = (name) => /\.client\.(jsx?|tsx?)$/i.test(normalizeComponentName(name))
42
+ export const COMPONENT_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
43
+
44
+ let componentImportNonce = Date.now()
45
+ export const bumpComponentImportNonce = () => {
46
+ componentImportNonce = Date.now()
47
+ return componentImportNonce
48
+ }
49
+
50
+ export const rewindEnv = env()
51
+ export const client = rewindEnv.client
52
+ export const invalidateRegistryEntry = rewindEnv.invalidate
53
+ export const genRegistryScript = rewindEnv.genRegistryScript
54
+
55
+ const resolveComponentExport = (componentPath, ext) => {
56
+ const staticCandidate = `${componentPath}.static${ext}`
57
+ const clientCandidate = `${componentPath}.client${ext}`
58
+ const genericCandidate = `${componentPath}${ext}`
59
+ const ret = {}
60
+ if (existsSync(staticCandidate)) {
61
+ ret.staticPath = staticCandidate
62
+ }
63
+ if (existsSync(clientCandidate)) {
64
+ ret.clientPath = clientCandidate
65
+ }
66
+ if (!ret.staticPath) {
67
+ if (existsSync(genericCandidate)) {
68
+ ret.staticPath = genericCandidate
69
+ } else if (existsSync(clientCandidate)) {
70
+ ret.staticPath = clientCandidate
71
+ }
72
+ }
73
+ return ret
74
+ }
75
+
76
+ export const buildComponentEntry = async ({ dir, exportName, ext, client: clientFn = client }) => {
77
+ const info = resolveComponentExport(join(dir, exportName), ext)
78
+ if (!info.staticPath) {
79
+ return { component: null, hasClient: false, staticPath: null, clientPath: null }
80
+ }
81
+
82
+ let component = (await import(`${pathToFileURL(info.staticPath).href}?t=${componentImportNonce}`)).default
83
+
84
+ if (!component) {
85
+ return { component: null, hasClient: false, staticPath: null, clientPath: null }
86
+ }
87
+
88
+ if (info.clientPath) {
89
+ component = clientFn({ ...info, staticComponent: component, exportName })
90
+ }
91
+
92
+ return {
93
+ component,
94
+ hasClient: Boolean(info.clientPath),
95
+ staticPath: info.staticPath,
96
+ clientPath: info.clientPath || null
97
+ }
98
+ }
99
+
100
+ export const buildComponentRegistry = async ({ componentsDir = state.COMPONENTS_DIR, client: clientFn = client } = {}) => {
101
+ const components = {}
102
+ const sources = new Map()
103
+
104
+ if (!componentsDir || !existsSync(componentsDir)) {
105
+ return { components, sources }
106
+ }
107
+
108
+ const walk = async (dir) => {
109
+ const entries = await readdir(dir)
110
+ for (const entry of entries) {
111
+ if (isIgnoredEntry(entry)) {
112
+ continue
113
+ }
114
+ const fullPath = join(dir, entry)
115
+ const stats = await stat(fullPath)
116
+ if (stats.isDirectory()) {
117
+ await walk(fullPath)
118
+ continue
119
+ }
120
+ if (!isComponentFile(entry)) {
121
+ continue
122
+ }
123
+
124
+ const exportName = entry.split('.')[0]
125
+ if (sources.has(exportName)) {
126
+ continue
127
+ }
128
+
129
+ const { component, staticPath } = await buildComponentEntry({
130
+ dir,
131
+ exportName,
132
+ ext: extname(entry),
133
+ client: clientFn
134
+ })
135
+ if (!component) continue
136
+ components[exportName] = component
137
+ if (staticPath) {
138
+ sources.set(exportName, staticPath)
139
+ }
140
+ }
141
+ }
142
+
143
+ await walk(componentsDir)
144
+ return { components, sources }
145
+ }
package/src/config.js ADDED
@@ -0,0 +1,396 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import { readFile } from 'fs/promises'
22
+ import { existsSync } from 'fs'
23
+ import { resolve, isAbsolute, extname, basename } from 'path'
24
+ import { pathToFileURL } from 'url'
25
+ import { mergeConfig } from 'vite'
26
+ import { cli, state } from './state.js'
27
+ import { HTMLRenderer } from './renderer.js'
28
+ import { rewindEnv } from './components.js'
29
+ import { env as createEnv } from './rewind.js'
30
+ import defaultTheme from '../themes/default/index.js'
31
+
32
+ const CONFIG_FILENAMES = [
33
+ 'methanol.config.js',
34
+ 'methanol.config.mjs',
35
+ 'methanol.config.cjs',
36
+ 'methanol.config.ts',
37
+ 'methanol.config.jsx',
38
+ 'methanol.config.mts',
39
+ 'methanol.config.cts',
40
+ 'methanol.config.tsx'
41
+ ]
42
+
43
+ const resolveRootPath = (value) => {
44
+ if (!value) {
45
+ return state.PROJECT_ROOT
46
+ }
47
+ return isAbsolute(value) ? value : resolve(state.PROJECT_ROOT, value)
48
+ }
49
+
50
+ const resolveFromRoot = (root, value, fallback) => {
51
+ if (!value) {
52
+ return resolve(root, fallback)
53
+ }
54
+ return isAbsolute(value) ? value : resolve(root, value)
55
+ }
56
+
57
+ const resolveOptionalPath = (root, value, fallback) => {
58
+ if (value === false) {
59
+ return false
60
+ }
61
+ return resolveFromRoot(root, value, fallback)
62
+ }
63
+
64
+ const resolveThemeComponentDir = (root, value) => {
65
+ if (value == null) return null
66
+ if (value === false) return false
67
+ return isAbsolute(value) ? value : resolve(root, value)
68
+ }
69
+
70
+ const resolveThemePagesDir = (root, value) => {
71
+ if (value == null) return null
72
+ if (value === false) return false
73
+ return isAbsolute(value) ? value : resolve(root, value)
74
+ }
75
+
76
+ const resolveThemePublicDir = (root, value) => {
77
+ if (value == null) return null
78
+ if (value === false) return false
79
+ return isAbsolute(value) ? value : resolve(root, value)
80
+ }
81
+
82
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key)
83
+ const normalizeSources = (value, root) => {
84
+ if (!value) return []
85
+ const entries = []
86
+ const addEntry = (find, replacement) => {
87
+ if (!find || !replacement) return
88
+ let resolvedReplacement = replacement
89
+ if (typeof replacement === 'string' && !isAbsolute(replacement)) {
90
+ resolvedReplacement = resolve(root, replacement)
91
+ }
92
+ entries.push({ find, replacement: resolvedReplacement })
93
+ }
94
+ if (Array.isArray(value)) {
95
+ for (const entry of value) {
96
+ if (!entry) continue
97
+ if (typeof entry === 'object') {
98
+ addEntry(entry.find, entry.replacement)
99
+ }
100
+ }
101
+ return entries
102
+ }
103
+ if (typeof value === 'object') {
104
+ for (const [find, replacement] of Object.entries(value)) {
105
+ addEntry(find, replacement)
106
+ }
107
+ }
108
+ return entries
109
+ }
110
+
111
+ const resolvePagefindEnabled = (config) => {
112
+ if (config?.pagefind == null) return false
113
+ if (typeof config.pagefind === 'boolean') return config.pagefind
114
+ if (typeof config.pagefind === 'object') {
115
+ if (hasOwn(config.pagefind, 'enabled')) {
116
+ return config.pagefind.enabled !== false
117
+ }
118
+ }
119
+ return true
120
+ }
121
+
122
+ const resolvePagefindOptions = (config) => {
123
+ const value = config?.pagefind
124
+ if (!value || typeof value !== 'object') return null
125
+ const { enabled, options, build, buildOptions, ...rest } = value
126
+ if (options && typeof options === 'object') {
127
+ return { ...options }
128
+ }
129
+ if (Object.keys(rest).length) {
130
+ return { ...rest }
131
+ }
132
+ return null
133
+ }
134
+
135
+ const resolvePagefindBuild = (config) => {
136
+ const value = config?.pagefind
137
+ if (!value || typeof value !== 'object') return null
138
+ const build = value.build
139
+ if (build && typeof build === 'object') {
140
+ return { ...build }
141
+ }
142
+ return null
143
+ }
144
+
145
+ const resolveStarryNightConfig = (value) => {
146
+ if (value == null) return { enabled: false, options: null }
147
+ if (typeof value === 'boolean') {
148
+ return { enabled: value, options: null }
149
+ }
150
+ if (typeof value !== 'object') {
151
+ return { enabled: false, options: null }
152
+ }
153
+ const { enabled, options, ...rest } = value
154
+ if (enabled === false) return { enabled: false, options: null }
155
+ if (options && typeof options === 'object') {
156
+ return { enabled: true, options: { ...options } }
157
+ }
158
+ if (Object.keys(rest).length) {
159
+ return { enabled: true, options: { ...rest } }
160
+ }
161
+ return { enabled: true, options: null }
162
+ }
163
+
164
+ const normalizeHooks = (value) => {
165
+ if (!value) return []
166
+ if (typeof value === 'function') return [value]
167
+ if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'function')
168
+ return []
169
+ }
170
+
171
+ const loadConfigModule = async (filePath) => {
172
+ return import(`${pathToFileURL(filePath).href}?t=${Date.now()}`)
173
+ }
174
+
175
+ const resolveConfigPath = (value) => {
176
+ if (!value) return null
177
+ return isAbsolute(value) ? value : resolve(state.PROJECT_ROOT, value)
178
+ }
179
+
180
+ const buildConfigContext = (mode) => ({
181
+ mode,
182
+ root: state.PROJECT_ROOT,
183
+ HTMLRenderer
184
+ })
185
+
186
+ export const loadUserConfig = async (mode, configPath = null) => {
187
+ if (configPath) {
188
+ const filePath = resolveConfigPath(configPath)
189
+ if (!filePath || !existsSync(filePath)) {
190
+ throw new Error(`Config file not found: ${configPath}`)
191
+ }
192
+ const mod = await loadConfigModule(filePath)
193
+ const config = mod.default ?? mod
194
+ if (typeof config !== 'function') {
195
+ throw new Error(`Config must export a function: ${filePath}`)
196
+ }
197
+ return (await config(buildConfigContext(mode))) || {}
198
+ }
199
+ for (const name of CONFIG_FILENAMES) {
200
+ const filePath = resolve(state.PROJECT_ROOT, name)
201
+ if (!existsSync(filePath)) {
202
+ continue
203
+ }
204
+ const mod = await loadConfigModule(filePath)
205
+ const config = mod.default ?? mod
206
+ if (typeof config !== 'function') {
207
+ throw new Error(`Config must export a function: ${filePath}`)
208
+ }
209
+ return (await config(buildConfigContext(mode))) || {}
210
+ }
211
+ return {}
212
+ }
213
+
214
+ export const applyConfig = async (config, mode) => {
215
+ const root = resolveRootPath(config.root)
216
+ state.ROOT_DIR = root
217
+ const configSiteName = cli.CLI_SITE_NAME ?? config.site?.name ?? null
218
+ state.SITE_NAME = configSiteName || basename(root) || 'Methanol Site'
219
+ if (mode) {
220
+ state.CURRENT_MODE = mode
221
+ }
222
+ // config.paths / config.dirs are intentionally ignored (deprecated)
223
+
224
+ const pagesDirValue = cli.CLI_PAGES_DIR || config.pagesDir
225
+ const componentsDirValue = cli.CLI_COMPONENTS_DIR || config.componentsDir
226
+ const distDirValue = cli.CLI_OUTPUT_DIR || config.distDir
227
+ const publicDirValue = cli.CLI_ASSETS_DIR ?? config.publicDir
228
+
229
+ const resolvePagesFallback = () => {
230
+ const pagesPath = resolveFromRoot(root, 'pages', 'pages')
231
+ if (existsSync(pagesPath)) return pagesPath
232
+ const docsPath = resolveFromRoot(root, 'docs', 'docs')
233
+ if (existsSync(docsPath)) return docsPath
234
+ return pagesPath
235
+ }
236
+ state.PAGES_DIR = pagesDirValue
237
+ ? resolveFromRoot(root, pagesDirValue, 'pages')
238
+ : resolvePagesFallback()
239
+ state.COMPONENTS_DIR = resolveFromRoot(root, componentsDirValue, 'components')
240
+ state.STATIC_DIR = resolveOptionalPath(root, publicDirValue, 'public')
241
+ state.BUILD_DIR = resolveFromRoot(root, config.buildDir, 'build')
242
+ state.DIST_DIR = resolveFromRoot(root, distDirValue, 'dist')
243
+
244
+ const userSpecifiedPagesDir = cli.CLI_PAGES_DIR != null || hasOwn(config, 'pagesDir')
245
+ if (userSpecifiedPagesDir && !existsSync(state.PAGES_DIR)) {
246
+ throw new Error(`Pages directory not found: ${state.PAGES_DIR}`)
247
+ }
248
+ const userSpecifiedComponentsDir = cli.CLI_COMPONENTS_DIR != null || hasOwn(config, 'componentsDir')
249
+ if (userSpecifiedComponentsDir && !existsSync(state.COMPONENTS_DIR)) {
250
+ throw new Error(`Components directory not found: ${state.COMPONENTS_DIR}`)
251
+ }
252
+ const userSpecifiedPublicDir = cli.CLI_ASSETS_DIR != null || hasOwn(config, 'publicDir')
253
+ if (userSpecifiedPublicDir && state.STATIC_DIR !== false && !existsSync(state.STATIC_DIR)) {
254
+ state.STATIC_DIR = resolveFromRoot(root, publicDirValue, 'public')
255
+ }
256
+ state.USER_PUBLIC_OVERRIDE = userSpecifiedPublicDir
257
+
258
+ state.VIRTUAL_HTML_OUTPUT_ROOT = state.PAGES_DIR
259
+
260
+ state.USER_THEME = config.theme || await defaultTheme()
261
+ if (!state.USER_THEME?.root && !config.theme?.root) {
262
+ throw new Error('Theme root is required.')
263
+ }
264
+ if (config.theme?.root) {
265
+ state.USER_THEME.root = resolveFromRoot(root, config.theme.root)
266
+ }
267
+ const themeEnv = state.USER_THEME.env || createEnv()
268
+ state.THEME_ENV = themeEnv
269
+ rewindEnv.setParent(themeEnv)
270
+ const themeRoot = state.USER_THEME.root || root
271
+ const themeComponentDirValue = hasOwn(state.USER_THEME, 'componentsDir')
272
+ ? state.USER_THEME.componentsDir
273
+ : './components'
274
+ state.THEME_COMPONENTS_DIR = resolveThemeComponentDir(themeRoot, themeComponentDirValue)
275
+ const themePagesDirValue = hasOwn(state.USER_THEME, 'pagesDir')
276
+ ? state.USER_THEME.pagesDir
277
+ : './pages'
278
+ state.THEME_PAGES_DIR = resolveThemePagesDir(themeRoot, themePagesDirValue)
279
+ const themePublicDirValue = hasOwn(state.USER_THEME, 'publicDir')
280
+ ? state.USER_THEME.publicDir
281
+ : './public'
282
+ state.THEME_PUBLIC_DIR = resolveThemePublicDir(themeRoot, themePublicDirValue)
283
+ if (hasOwn(state.USER_THEME, 'componentsDir') && state.THEME_COMPONENTS_DIR && !existsSync(state.THEME_COMPONENTS_DIR)) {
284
+ throw new Error(`Theme components directory not found: ${state.THEME_COMPONENTS_DIR}`)
285
+ }
286
+ if (hasOwn(state.USER_THEME, 'pagesDir') && state.THEME_PAGES_DIR && !existsSync(state.THEME_PAGES_DIR)) {
287
+ throw new Error(`Theme pages directory not found: ${state.THEME_PAGES_DIR}`)
288
+ }
289
+ if (hasOwn(state.USER_THEME, 'publicDir') && state.THEME_PUBLIC_DIR && !existsSync(state.THEME_PUBLIC_DIR)) {
290
+ throw new Error(`Theme public directory not found: ${state.THEME_PUBLIC_DIR}`)
291
+ }
292
+ if (
293
+ state.STATIC_DIR !== false &&
294
+ !userSpecifiedPublicDir &&
295
+ !existsSync(state.STATIC_DIR) &&
296
+ state.THEME_PUBLIC_DIR &&
297
+ existsSync(state.THEME_PUBLIC_DIR)
298
+ ) {
299
+ state.STATIC_DIR = state.THEME_PUBLIC_DIR
300
+ }
301
+ state.SOURCES = normalizeSources(state.USER_THEME.sources, themeRoot)
302
+ state.USER_VITE_CONFIG = config.vite || null
303
+ state.USER_MDX_CONFIG = config.mdx || null
304
+ state.RESOLVED_MDX_CONFIG = undefined
305
+ state.RESOLVED_VITE_CONFIG = undefined
306
+ state.PAGEFIND_ENABLED = resolvePagefindEnabled(config)
307
+ state.PAGEFIND_OPTIONS = resolvePagefindOptions(config)
308
+ state.PAGEFIND_BUILD = resolvePagefindBuild(config)
309
+ state.USER_PRE_BUILD_HOOKS = normalizeHooks(config.preBuild)
310
+ state.USER_POST_BUILD_HOOKS = normalizeHooks(config.postBuild)
311
+ state.THEME_PRE_BUILD_HOOKS = normalizeHooks(state.USER_THEME?.preBuild)
312
+ state.THEME_POST_BUILD_HOOKS = normalizeHooks(state.USER_THEME?.postBuild)
313
+ const starryNight = resolveStarryNightConfig(config.starryNight)
314
+ const cliCodeHighlighting = cli.CLI_CODE_HIGHLIGHTING
315
+ if (cliCodeHighlighting != null) {
316
+ state.STARRY_NIGHT_ENABLED = cliCodeHighlighting === true
317
+ state.STARRY_NIGHT_OPTIONS = cliCodeHighlighting === true ? starryNight.options : null
318
+ } else {
319
+ state.STARRY_NIGHT_ENABLED = starryNight.enabled
320
+ state.STARRY_NIGHT_OPTIONS = starryNight.enabled ? starryNight.options : null
321
+ }
322
+
323
+ if (cli.CLI_INTERMEDIATE_DIR) {
324
+ state.INTERMEDIATE_DIR = resolveFromRoot(root, cli.CLI_INTERMEDIATE_DIR, 'build')
325
+ } else if (config.intermediateDir) {
326
+ state.INTERMEDIATE_DIR = resolveFromRoot(root, config.intermediateDir, 'build')
327
+ } else if (cli.CLI_EMIT_INTERMEDIATE || config.emitIntermediate) {
328
+ state.INTERMEDIATE_DIR = state.BUILD_DIR
329
+ } else {
330
+ state.INTERMEDIATE_DIR = null
331
+ }
332
+ }
333
+
334
+ export const resolveUserMdxConfig = async () => {
335
+ if (state.RESOLVED_MDX_CONFIG !== undefined) {
336
+ return state.RESOLVED_MDX_CONFIG
337
+ }
338
+ const resolveConfig = async (config) => {
339
+ if (!config) return {}
340
+ if (typeof config === 'function') {
341
+ return (
342
+ (await config({
343
+ mode: state.CURRENT_MODE,
344
+ root: state.ROOT_DIR
345
+ })) || {}
346
+ )
347
+ }
348
+ return config || {}
349
+ }
350
+ const themeConfig = await resolveConfig(state.USER_THEME.mdx)
351
+ const userConfig = await resolveConfig(state.USER_MDX_CONFIG)
352
+ const merged = { ...themeConfig, ...userConfig }
353
+ const themePlugins = themeConfig?.rehypePlugins
354
+ const userPlugins = userConfig?.rehypePlugins
355
+ if (themePlugins || userPlugins) {
356
+ const normalize = (value) => (Array.isArray(value) ? value : value ? [value] : [])
357
+ merged.rehypePlugins = [...normalize(themePlugins), ...normalize(userPlugins)]
358
+ } else {
359
+ merged.rehypePlugins = []
360
+ }
361
+ state.RESOLVED_MDX_CONFIG = merged
362
+ return state.RESOLVED_MDX_CONFIG
363
+ }
364
+
365
+ export const resolveUserViteConfig = async (command) => {
366
+ if (state.RESOLVED_VITE_CONFIG !== undefined) {
367
+ return state.RESOLVED_VITE_CONFIG
368
+ }
369
+ const resolveConfig = async (config) => {
370
+ if (!config) return null
371
+ if (typeof config === 'function') {
372
+ const isPreview = command === 'preview'
373
+ return (
374
+ (await config({
375
+ mode: state.CURRENT_MODE,
376
+ root: state.ROOT_DIR,
377
+ command: isPreview ? 'serve' : command,
378
+ isPreview
379
+ })) || null
380
+ )
381
+ }
382
+ return config || null
383
+ }
384
+ const themeConfig = await resolveConfig(state.USER_THEME.vite)
385
+ const userConfig = await resolveConfig(state.USER_VITE_CONFIG)
386
+ if (!themeConfig && !userConfig) {
387
+ state.RESOLVED_VITE_CONFIG = null
388
+ return null
389
+ }
390
+ state.RESOLVED_VITE_CONFIG = themeConfig
391
+ ? userConfig
392
+ ? mergeConfig(themeConfig, userConfig)
393
+ : themeConfig
394
+ : userConfig
395
+ return state.RESOLVED_VITE_CONFIG
396
+ }