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
package/src/main.js ADDED
@@ -0,0 +1,133 @@
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 { loadUserConfig, applyConfig } from './config.js'
22
+ import { runViteDev } from './dev-server.js'
23
+ import { buildHtmlEntries, runViteBuild } from './build-system.js'
24
+ import { runPagefind } from './pagefind.js'
25
+ import { runVitePreview } from './preview-server.js'
26
+ import { cli, state } from './state.js'
27
+ import { HTMLRenderer } from './renderer.js'
28
+ import { readFile } from 'fs/promises'
29
+
30
+ const printBanner = async () => {
31
+ try {
32
+ const pkgUrl = new URL('../package.json', import.meta.url)
33
+ const raw = await readFile(pkgUrl, 'utf-8')
34
+ const pkg = JSON.parse(raw)
35
+ const version = pkg?.version ? `v${pkg.version}` : ''
36
+ const isTty = Boolean(process.stdout && process.stdout.isTTY)
37
+ const label = `Methanol ${version}`.trim()
38
+ if (isTty) {
39
+ const bannerUrl = new URL('../banner.txt', import.meta.url)
40
+ const banner = await readFile(bannerUrl, 'utf-8')
41
+ console.log(banner.trimEnd())
42
+ console.log(`\n\t${label}\n`)
43
+ } else {
44
+ console.log(label)
45
+ }
46
+ } catch {
47
+ console.log('Methanol')
48
+ }
49
+ }
50
+
51
+ const main = async () => {
52
+ await printBanner()
53
+ const command = cli.command
54
+ if (!command) {
55
+ cli.showHelp()
56
+ process.exit(1)
57
+ }
58
+ const normalizedCommand = command === 'preview' ? 'serve' : command
59
+ const isDev = normalizedCommand === 'dev'
60
+ const isPreview = normalizedCommand === 'serve'
61
+ const isBuild = normalizedCommand === 'build'
62
+ const mode = isDev ? 'development' : 'production'
63
+ const config = await loadUserConfig(mode, cli.CLI_CONFIG_PATH)
64
+ await applyConfig(config, mode)
65
+ const hookContext = {
66
+ mode,
67
+ root: state.ROOT_DIR,
68
+ command: normalizedCommand,
69
+ isDev,
70
+ isBuild,
71
+ isPreview,
72
+ HTMLRenderer,
73
+ site: {
74
+ name: state.SITE_NAME,
75
+ root: state.ROOT_DIR,
76
+ pagesDir: state.PAGES_DIR,
77
+ componentsDir: state.COMPONENTS_DIR,
78
+ publicDir: state.STATIC_DIR,
79
+ distDir: state.DIST_DIR,
80
+ mode: state.CURRENT_MODE,
81
+ pagefind: {
82
+ enabled: state.PAGEFIND_ENABLED,
83
+ options: state.PAGEFIND_OPTIONS || null,
84
+ build: state.PAGEFIND_BUILD || null
85
+ }
86
+ },
87
+ data: {}
88
+ }
89
+ const runHooks = async (hooks = [], extra = null) => {
90
+ const context = extra ? { ...hookContext, ...extra } : hookContext
91
+ for (const hook of hooks) {
92
+ await hook(context)
93
+ }
94
+ }
95
+ if (isDev) {
96
+ await runHooks(state.USER_PRE_BUILD_HOOKS)
97
+ await runHooks(state.THEME_PRE_BUILD_HOOKS)
98
+ await runViteDev()
99
+ return
100
+ }
101
+ if (isPreview) {
102
+ await runVitePreview()
103
+ return
104
+ }
105
+ if (isBuild) {
106
+ await runHooks(state.USER_PRE_BUILD_HOOKS)
107
+ await runHooks(state.THEME_PRE_BUILD_HOOKS)
108
+ const { entry, htmlCache, pagesContext } = await buildHtmlEntries()
109
+ await runViteBuild(entry, htmlCache)
110
+ if (state.PAGEFIND_ENABLED) {
111
+ await runPagefind()
112
+ }
113
+ const postBuildContext = pagesContext
114
+ ? {
115
+ pagesContext,
116
+ pages: pagesContext.pages,
117
+ pagesTree: pagesContext.pagesTree,
118
+ pagesByRoute: pagesContext.pagesByRoute,
119
+ site: pagesContext.site
120
+ }
121
+ : null
122
+ await runHooks(state.THEME_POST_BUILD_HOOKS, postBuildContext)
123
+ await runHooks(state.USER_POST_BUILD_HOOKS, postBuildContext)
124
+ return
125
+ }
126
+ cli.showHelp()
127
+ process.exit(1)
128
+ }
129
+
130
+ main().catch((err) => {
131
+ console.error(err)
132
+ process.exit(1)
133
+ })
package/src/mdx.js ADDED
@@ -0,0 +1,406 @@
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 { compile, run } from '@mdx-js/mdx'
22
+ import * as JSXFactory from 'refui/jsx-runtime'
23
+ import * as JSXDevFactory from 'refui/jsx-dev-runtime'
24
+ import rehypeSlug from 'rehype-slug'
25
+ import extractToc from '@stefanprobst/rehype-extract-toc'
26
+ import withTocExport from '@stefanprobst/rehype-extract-toc/mdx'
27
+ import rehypeStarryNight from 'rehype-starry-night'
28
+ import { HTMLRenderer } from './renderer.js'
29
+ import { signal, computed, read, Suspense, nextTick } from 'refui'
30
+ import { createPortal } from 'refui/extras'
31
+ import { pathToFileURL } from 'url'
32
+ import { existsSync } from 'fs'
33
+ import { resolve, dirname, basename, relative } from 'path'
34
+ import { state } from './state.js'
35
+ import { resolveUserMdxConfig } from './config.js'
36
+ import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
37
+ import { linkResolve } from './rehype-plugins/link-resolve.js'
38
+
39
+ // Workaround for Vite: it doesn't support resolving module/virtual modules in script src in dev mode
40
+ const RWND_INJECT = HTMLRenderer.rawHTML`<script type="module" src="/.methanol_virtual_module/inject.js"></script>`
41
+ const RWND_FALLBACK = HTMLRenderer.rawHTML`<script>
42
+ if (!window.$$rwnd) {
43
+ const l = []
44
+ const r = function(k,i,p) {
45
+ l.push([k,i,p,document.currentScript])
46
+ }
47
+ r.$$loaded = l
48
+ window.$$rwnd = r
49
+ }
50
+ </script>`
51
+
52
+ let cachedHeadAssets = null
53
+
54
+ const resolveUserHeadAssets = () => {
55
+ if (cachedHeadAssets) {
56
+ return cachedHeadAssets
57
+ }
58
+ const assets = []
59
+ const pagesDir = state.PAGES_DIR
60
+ if (!pagesDir) return assets
61
+ if (existsSync(resolve(pagesDir, 'style.css'))) {
62
+ assets.push(HTMLRenderer.c('link', { rel: 'stylesheet', href: '/style.css' }))
63
+ }
64
+ if (existsSync(resolve(pagesDir, 'index.js'))) {
65
+ assets.push(HTMLRenderer.c('script', { type: 'module', src: '/index.js' }))
66
+ } else if (existsSync(resolve(pagesDir, 'index.ts'))) {
67
+ assets.push(HTMLRenderer.c('script', { type: 'module', src: '/index.ts' }))
68
+ }
69
+ if (state.CURRENT_MODE === 'production') {
70
+ cachedHeadAssets = assets
71
+ }
72
+ return assets
73
+ }
74
+
75
+ const resolvePageAssetUrl = (page, filePath) => {
76
+ const root = page?.source === 'theme' && state.THEME_PAGES_DIR
77
+ ? state.THEME_PAGES_DIR
78
+ : state.PAGES_DIR
79
+ if (!root) return null
80
+ const relPath = relative(root, filePath).replace(/\\/g, '/')
81
+ if (!relPath || relPath.startsWith('..')) return null
82
+ return `/${relPath}`
83
+ }
84
+
85
+ const resolvePageHeadAssets = (page) => {
86
+ if (!page?.filePath) return []
87
+ const baseDir = dirname(page.filePath)
88
+ const baseName = basename(page.filePath).replace(/\.(mdx|md)$/, '')
89
+ const pagesRoot = state.PAGES_DIR ? resolve(state.PAGES_DIR) : null
90
+ const isRootIndex =
91
+ pagesRoot && baseName === 'index' && resolve(baseDir) === pagesRoot && page.source !== 'theme'
92
+ const isRootStylePage =
93
+ pagesRoot && baseName === 'style' && resolve(baseDir) === pagesRoot && page.source !== 'theme'
94
+ const assets = []
95
+ const cssPath = resolve(baseDir, `${baseName}.css`)
96
+ if (existsSync(cssPath)) {
97
+ if (isRootStylePage) {
98
+ const rootStyle = resolve(pagesRoot, 'style.css')
99
+ if (cssPath === rootStyle) {
100
+ return assets
101
+ }
102
+ }
103
+ const href = resolvePageAssetUrl(page, cssPath)
104
+ if (href) {
105
+ assets.push(HTMLRenderer.c('link', { rel: 'stylesheet', href }))
106
+ }
107
+ }
108
+ const scriptExtensions = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']
109
+ let scriptPath = null
110
+ for (const ext of scriptExtensions) {
111
+ const candidate = resolve(baseDir, `${baseName}${ext}`)
112
+ if (existsSync(candidate)) {
113
+ scriptPath = candidate
114
+ break
115
+ }
116
+ }
117
+ if (scriptPath) {
118
+ if (isRootIndex) {
119
+ const rootIndexJs = resolve(pagesRoot, 'index.js')
120
+ const rootIndexTs = resolve(pagesRoot, 'index.ts')
121
+ if (scriptPath === rootIndexJs || scriptPath === rootIndexTs) {
122
+ return assets
123
+ }
124
+ }
125
+ const src = resolvePageAssetUrl(page, scriptPath)
126
+ if (src) {
127
+ assets.push(HTMLRenderer.c('script', { type: 'module', src }))
128
+ }
129
+ }
130
+ return assets
131
+ }
132
+
133
+ export const buildPageContext = ({
134
+ routePath,
135
+ filePath,
136
+ pageMeta,
137
+ pagesContext,
138
+ lazyPagesTree = false
139
+ }) => {
140
+ const page = pageMeta
141
+ const language = pagesContext?.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
142
+ const getSiblings = pagesContext?.getSiblings
143
+ ? () => pagesContext.getSiblings(routePath, page?.filePath || filePath)
144
+ : null
145
+ if (page && getSiblings && page.getSiblings !== getSiblings) {
146
+ page.getSiblings = getSiblings
147
+ }
148
+ const ctx = {
149
+ routePath,
150
+ filePath,
151
+ page,
152
+ pages: pagesContext?.pages || [],
153
+ pagesByRoute: pagesContext?.pagesByRoute || new Map(),
154
+ languages: pagesContext?.languages || [],
155
+ language,
156
+ site: pagesContext?.site || null,
157
+ getSiblings
158
+ }
159
+ const resolvePagesTree = () =>
160
+ pagesContext?.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext?.pagesTree || []
161
+ if (lazyPagesTree) {
162
+ let cachedTree = null
163
+ let hasTree = false
164
+ Object.defineProperty(ctx, 'pagesTree', {
165
+ enumerable: true,
166
+ get() {
167
+ if (!hasTree) {
168
+ cachedTree = resolvePagesTree()
169
+ hasTree = true
170
+ }
171
+ return cachedTree
172
+ },
173
+ set(value) {
174
+ cachedTree = value
175
+ hasTree = true
176
+ }
177
+ })
178
+ } else {
179
+ ctx.pagesTree = resolvePagesTree()
180
+ }
181
+ return ctx
182
+ }
183
+
184
+ const findTitleFromToc = (toc = []) => {
185
+ let minDepth = Infinity
186
+ const scanDepth = (items) => {
187
+ for (const item of items) {
188
+ if (typeof item?.depth === 'number') {
189
+ minDepth = Math.min(minDepth, item.depth)
190
+ }
191
+ if (item?.children?.length) {
192
+ scanDepth(item.children)
193
+ }
194
+ }
195
+ }
196
+ scanDepth(toc)
197
+ if (!Number.isFinite(minDepth)) return null
198
+ let result = null
199
+ const findFirst = (items) => {
200
+ for (const item of items) {
201
+ if (item?.depth === minDepth && item?.value) {
202
+ result = item.value
203
+ return true
204
+ }
205
+ if (item?.children?.length && findFirst(item.children)) {
206
+ return true
207
+ }
208
+ }
209
+ return false
210
+ }
211
+ findFirst(toc)
212
+ return result
213
+ }
214
+
215
+ let cachedMdxConfig = null
216
+
217
+ const normalizeStarryNightConfig = (value) => {
218
+ if (value == null) return null
219
+ if (typeof value === 'boolean') {
220
+ return { enabled: value, options: null }
221
+ }
222
+ if (typeof value !== 'object') return null
223
+ const { enabled, options, ...rest } = value
224
+ if (enabled === false) return { enabled: false, options: null }
225
+ if (options && typeof options === 'object') {
226
+ return { enabled: true, options: { ...options } }
227
+ }
228
+ if (Object.keys(rest).length) {
229
+ return { enabled: true, options: { ...rest } }
230
+ }
231
+ return { enabled: true, options: null }
232
+ }
233
+
234
+ const resolveStarryNightForPage = (frontmatter) => {
235
+ const base = {
236
+ enabled: state.STARRY_NIGHT_ENABLED === true,
237
+ options: state.STARRY_NIGHT_OPTIONS || null
238
+ }
239
+ if (!frontmatter || !Object.prototype.hasOwnProperty.call(frontmatter, 'starryNight')) {
240
+ return base
241
+ }
242
+ const override = normalizeStarryNightConfig(frontmatter.starryNight)
243
+ if (!override) return base
244
+ if (override.enabled === false) return { enabled: false, options: null }
245
+ const options = override.options != null ? override.options : base.options
246
+ return { enabled: true, options }
247
+ }
248
+
249
+ const resolveBaseMdxConfig = async () => {
250
+ const userMdxConfig = await resolveUserMdxConfig()
251
+ if (cachedMdxConfig) {
252
+ return cachedMdxConfig
253
+ }
254
+ const baseMdxConfig = {
255
+ outputFormat: 'function-body',
256
+ jsxRuntime: 'automatic',
257
+ jsxImportSource: 'refui',
258
+ development: state.CURRENT_MODE !== 'production',
259
+ elementAttributeNameCase: 'html',
260
+ rehypePlugins: [rehypeSlug, extractToc, [withTocExport, { name: 'toc' }]]
261
+ }
262
+ const mdxConfig = { ...baseMdxConfig, ...userMdxConfig }
263
+ const userRehypePlugins = Array.isArray(userMdxConfig.rehypePlugins) ? userMdxConfig.rehypePlugins : []
264
+ mdxConfig.rehypePlugins = [...baseMdxConfig.rehypePlugins, ...userRehypePlugins]
265
+ mdxConfig.rehypePlugins.push(linkResolve)
266
+ mdxConfig.rehypePlugins.push(methanolCtx)
267
+ return (cachedMdxConfig = mdxConfig)
268
+ }
269
+
270
+ const resolveMdxConfigForPage = async (frontmatter) => {
271
+ const baseConfig = await resolveBaseMdxConfig()
272
+ const mdxConfig = {
273
+ ...baseConfig,
274
+ rehypePlugins: [...baseConfig.rehypePlugins]
275
+ }
276
+ const starryNightConfig = resolveStarryNightForPage(frontmatter)
277
+ if (!starryNightConfig.enabled) return mdxConfig
278
+ const plugin = starryNightConfig.options ? [rehypeStarryNight, starryNightConfig.options] : [rehypeStarryNight]
279
+ const insertIndex = mdxConfig.rehypePlugins.indexOf(linkResolve)
280
+ if (insertIndex >= 0) {
281
+ mdxConfig.rehypePlugins.splice(insertIndex, 0, plugin)
282
+ } else {
283
+ mdxConfig.rehypePlugins.push(plugin)
284
+ }
285
+ return mdxConfig
286
+ }
287
+
288
+ export const compileMdx = async ({ content, filePath, ctx }) => {
289
+ const mdxConfig = await resolveMdxConfigForPage(ctx?.page?.frontmatter)
290
+ const runtimeFactory = mdxConfig.development ? JSXDevFactory : JSXFactory
291
+ const compiled = await compile({ value: content, path: filePath }, mdxConfig)
292
+
293
+ return await run(compiled, {
294
+ ...runtimeFactory,
295
+ baseUrl: pathToFileURL(filePath).href,
296
+ ctx,
297
+ rawHTML: HTMLRenderer.rawHTML
298
+ })
299
+ }
300
+
301
+ export const compilePageMdx = async (page, pagesContext, options = {}) => {
302
+ if (!page || page.content == null || page.mdxComponent) return
303
+ const { ctx = null, lazyPagesTree = false, refreshPagesTree = true } = options || {}
304
+ const activeCtx =
305
+ ctx ||
306
+ buildPageContext({
307
+ routePath: page.routePath,
308
+ filePath: page.filePath,
309
+ pageMeta: page,
310
+ pagesContext,
311
+ lazyPagesTree
312
+ })
313
+ const mdxModule = await compileMdx({
314
+ content: page.content,
315
+ filePath: page.filePath,
316
+ ctx: activeCtx
317
+ })
318
+ page.mdxComponent = mdxModule.default
319
+ page.toc = mdxModule.toc
320
+ const shouldUseTocTitle = page.frontmatter?.title == null
321
+ if (shouldUseTocTitle) {
322
+ const nextTitle = findTitleFromToc(page.toc) || page.title
323
+ if (nextTitle !== page.title) {
324
+ page.title = nextTitle
325
+ if (typeof pagesContext?.refreshPagesTree === 'function') {
326
+ pagesContext.refreshPagesTree()
327
+ }
328
+ }
329
+ }
330
+ if (typeof pagesContext?.setDerivedTitle === 'function') {
331
+ pagesContext.setDerivedTitle(page.filePath, shouldUseTocTitle ? page.title : null, page.toc)
332
+ }
333
+ if (ctx && refreshPagesTree && pagesContext?.getPagesTree) {
334
+ ctx.pagesTree = pagesContext.getPagesTree(activeCtx.routePath)
335
+ }
336
+ }
337
+
338
+ export const renderHtml = async ({
339
+ routePath,
340
+ filePath,
341
+ components,
342
+ pagesContext,
343
+ pageMeta: explicitPageMeta = null
344
+ }) => {
345
+ const pageMeta =
346
+ explicitPageMeta ||
347
+ (pagesContext.getPageByRoute
348
+ ? pagesContext.getPageByRoute(routePath, { filePath })
349
+ : pagesContext.pagesByRoute.get(routePath))
350
+
351
+ const ctx = buildPageContext({
352
+ routePath,
353
+ filePath,
354
+ pageMeta,
355
+ pagesContext
356
+ })
357
+ await compilePageMdx(pageMeta, pagesContext, { ctx })
358
+
359
+ const [Head, Outlet] = createPortal()
360
+ const ExtraHead = () => {
361
+ return [
362
+ RWND_INJECT,
363
+ ...resolveUserHeadAssets(),
364
+ ...resolvePageHeadAssets(pageMeta),
365
+ Outlet(),
366
+ RWND_FALLBACK
367
+ ]
368
+ }
369
+
370
+ const mdxComponent = pageMeta?.mdxComponent
371
+
372
+ const Page = ({ components: extraComponents, ...props }, ...children) =>
373
+ mdxComponent({
374
+ children,
375
+ ...props,
376
+ components: {
377
+ ...components,
378
+ ...extraComponents,
379
+ head: Head,
380
+ Head
381
+ }
382
+ })
383
+
384
+ const template = state.USER_THEME.template
385
+
386
+ const renderResult = await new Promise((r) => {
387
+ const result = HTMLRenderer.c(
388
+ Suspense,
389
+ {
390
+ onLoad() {
391
+ nextTick(() => r(result))
392
+ }
393
+ },
394
+ () =>
395
+ template({
396
+ ctx,
397
+ Page,
398
+ ExtraHead,
399
+ HTMLRenderer,
400
+ components
401
+ })
402
+ )
403
+ })
404
+
405
+ return HTMLRenderer.serialize(renderResult)
406
+ }
@@ -0,0 +1,88 @@
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 'node:fs/promises'
22
+ import { dirname, extname, join, resolve as pathResolve, relative } from 'node:path'
23
+ import { fileURLToPath, pathToFileURL } from 'node:url'
24
+ import { createRequire } from 'node:module'
25
+ import { transform } from 'esbuild'
26
+
27
+ const __filename = fileURLToPath(import.meta.url)
28
+ const __dirname = dirname(__filename)
29
+
30
+ const projectRoot = pathResolve('.', '__virtual__.js')
31
+ const projectRootURL = pathToFileURL(projectRoot)
32
+ export const projectRequire = createRequire(projectRootURL)
33
+
34
+ const require = createRequire(import.meta.url)
35
+
36
+ const EXTS = new Set(['.jsx', '.tsx', '.ts', '.mts', '.cts'])
37
+
38
+ export async function load(url, context, nextLoad) {
39
+ if (url.startsWith('node:') || url.startsWith('data:')) {
40
+ return nextLoad(url, context, nextLoad)
41
+ }
42
+
43
+ const pathname = new URL(url).pathname
44
+ const ext = extname(pathname).toLowerCase()
45
+ if (!EXTS.has(ext)) {
46
+ return nextLoad(url, context, nextLoad)
47
+ }
48
+
49
+ const source = await readFile(fileURLToPath(url), 'utf-8')
50
+ const loader = ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'ts'
51
+ const result = await transform(source, {
52
+ loader,
53
+ format: 'esm',
54
+ jsx: 'automatic',
55
+ jsxImportSource: 'refui',
56
+ sourcemap: 'inline',
57
+ sourcefile: fileURLToPath(url)
58
+ })
59
+
60
+ return {
61
+ format: 'module',
62
+ shortCircuit: true,
63
+ source: result.code
64
+ }
65
+ }
66
+
67
+ const startPos = 'methanol'.length
68
+ export async function resolve(specifier, context, nextResolve) {
69
+ if (specifier === 'refui' || specifier.startsWith('refui/')) {
70
+ try {
71
+ // Use user installed rEFui when possible
72
+ return await nextResolve(specifier, { ...context, parentURL: projectRootURL })
73
+ } catch (e) {
74
+ return await nextResolve(specifier, { ...context, parentURL: import.meta.url })
75
+ }
76
+ } else if (specifier === 'methanol' || specifier.startsWith('methanol/')) {
77
+ // Force only one Metnanol instance
78
+ const filePath = require.resolve('..' + specifier.slice(startPos))
79
+ return {
80
+ __proto__: null,
81
+ shortCircuit: true,
82
+ format: 'module',
83
+ url: pathToFileURL(filePath).href
84
+ }
85
+ } else {
86
+ return await nextResolve(specifier, context)
87
+ }
88
+ }