methanol 0.0.0 → 0.0.1

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 (45) hide show
  1. package/.editorconfig +19 -0
  2. package/.prettierrc +10 -0
  3. package/LICENSE +203 -0
  4. package/banner.txt +6 -0
  5. package/bin/methanol.js +24 -0
  6. package/index.js +22 -0
  7. package/package.json +42 -9
  8. package/src/assets.js +30 -0
  9. package/src/build-system.js +200 -0
  10. package/src/components.js +145 -0
  11. package/src/config.js +355 -0
  12. package/src/dev-server.js +559 -0
  13. package/src/main.js +87 -0
  14. package/src/mdx.js +254 -0
  15. package/src/node-loader.js +88 -0
  16. package/src/pagefind.js +99 -0
  17. package/src/pages.js +638 -0
  18. package/src/preview-server.js +58 -0
  19. package/src/public-assets.js +73 -0
  20. package/src/register-loader.js +29 -0
  21. package/src/rehype-plugins/link-resolve.js +89 -0
  22. package/src/rehype-plugins/methanol-ctx.js +89 -0
  23. package/src/renderer.js +25 -0
  24. package/src/rewind.js +117 -0
  25. package/src/stage-logger.js +59 -0
  26. package/src/state.js +159 -0
  27. package/src/virtual-module/inject.js +30 -0
  28. package/src/virtual-module/loader.js +116 -0
  29. package/src/virtual-module/pagefind.js +108 -0
  30. package/src/vite-plugins.js +173 -0
  31. package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
  32. package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
  33. package/themes/default/components/ThemeSearchBox.client.jsx +287 -0
  34. package/themes/default/components/ThemeSearchBox.static.jsx +41 -0
  35. package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
  36. package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
  37. package/themes/default/components/pre.client.jsx +84 -0
  38. package/themes/default/components/pre.jsx +27 -0
  39. package/themes/default/heading.jsx +35 -0
  40. package/themes/default/index.js +50 -0
  41. package/themes/default/page.jsx +249 -0
  42. package/themes/default/pages/404.mdx +8 -0
  43. package/themes/default/pages/index.mdx +9 -0
  44. package/themes/default/public/logo.png +0 -0
  45. package/themes/default/resources/style.css +1089 -0
package/src/mdx.js ADDED
@@ -0,0 +1,254 @@
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 { HTMLRenderer } from './renderer.js'
28
+ import { signal, computed, read, Suspense, nextTick } from 'refui'
29
+ import { createPortal } from 'refui/extras'
30
+ import { pathToFileURL } from 'url'
31
+ import { existsSync } from 'fs'
32
+ import { resolve } from 'path'
33
+ import { state } from './state.js'
34
+ import { resolveUserMdxConfig } from './config.js'
35
+ import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
36
+ import { linkResolve } from './rehype-plugins/link-resolve.js'
37
+
38
+ // Workaround for Vite: it doesn't support resolving module/virtual modules in script src in dev mode
39
+ const RWND_INJECT = HTMLRenderer.rawHTML`<script type="module" src="/.methanol_virtual_module/inject.js"></script>`
40
+ const RWND_FALLBACK = HTMLRenderer.rawHTML`<script>
41
+ if (!window.$$rwnd) {
42
+ const l = []
43
+ const r = function(k,i,p) {
44
+ l.push([k,i,p,document.currentScript])
45
+ }
46
+ r.$$loaded = l
47
+ window.$$rwnd = r
48
+ }
49
+ </script>`
50
+
51
+ let cachedHeadAssets = null
52
+
53
+ const resolveUserHeadAssets = () => {
54
+ if (cachedHeadAssets) {
55
+ return cachedHeadAssets
56
+ }
57
+ const assets = []
58
+ const pagesDir = state.PAGES_DIR
59
+ if (!pagesDir) return assets
60
+ if (existsSync(resolve(pagesDir, 'style.css'))) {
61
+ assets.push(HTMLRenderer.c('link', { rel: 'stylesheet', href: '/style.css' }))
62
+ }
63
+ if (existsSync(resolve(pagesDir, 'index.js'))) {
64
+ assets.push(HTMLRenderer.c('script', { type: 'module', src: '/index.js' }))
65
+ } else if (existsSync(resolve(pagesDir, 'index.ts'))) {
66
+ assets.push(HTMLRenderer.c('script', { type: 'module', src: '/index.ts' }))
67
+ }
68
+ if (state.CURRENT_MODE === 'production') {
69
+ cachedHeadAssets = assets
70
+ }
71
+ return assets
72
+ }
73
+
74
+ export const buildPageContext = ({ routePath, filePath, pageMeta, pagesContext }) => {
75
+ const page = pageMeta
76
+ const pagesTree = pagesContext?.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext?.pagesTree || []
77
+ const language = pagesContext?.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
78
+ return {
79
+ routePath,
80
+ filePath,
81
+ page,
82
+ pages: pagesContext?.pages || [],
83
+ pagesTree,
84
+ pagesByRoute: pagesContext?.pagesByRoute || new Map(),
85
+ languages: pagesContext?.languages || [],
86
+ language,
87
+ site: pagesContext?.site || null
88
+ }
89
+ }
90
+
91
+ const findTitleFromToc = (toc = []) => {
92
+ let minDepth = Infinity
93
+ const scanDepth = (items) => {
94
+ for (const item of items) {
95
+ if (typeof item?.depth === 'number') {
96
+ minDepth = Math.min(minDepth, item.depth)
97
+ }
98
+ if (item?.children?.length) {
99
+ scanDepth(item.children)
100
+ }
101
+ }
102
+ }
103
+ scanDepth(toc)
104
+ if (!Number.isFinite(minDepth)) return null
105
+ let result = null
106
+ const findFirst = (items) => {
107
+ for (const item of items) {
108
+ if (item?.depth === minDepth && item?.value) {
109
+ result = item.value
110
+ return true
111
+ }
112
+ if (item?.children?.length && findFirst(item.children)) {
113
+ return true
114
+ }
115
+ }
116
+ return false
117
+ }
118
+ findFirst(toc)
119
+ return result
120
+ }
121
+
122
+ let cachedMdxConfig = null
123
+
124
+ const resolveMdxConfig = async () => {
125
+ const userMdxConfig = await resolveUserMdxConfig()
126
+ if (cachedMdxConfig) {
127
+ return cachedMdxConfig
128
+ }
129
+ const baseMdxConfig = {
130
+ outputFormat: 'function-body',
131
+ jsxRuntime: 'automatic',
132
+ jsxImportSource: 'refui',
133
+ development: state.CURRENT_MODE !== 'production',
134
+ elementAttributeNameCase: 'html',
135
+ rehypePlugins: [rehypeSlug, extractToc, [withTocExport, { name: 'toc' }]]
136
+ }
137
+ const mdxConfig = { ...baseMdxConfig, ...userMdxConfig }
138
+ if (userMdxConfig.rehypePlugins.length) {
139
+ mdxConfig.rehypePlugins = [...baseMdxConfig.rehypePlugins, ...userMdxConfig.rehypePlugins]
140
+ }
141
+ mdxConfig.rehypePlugins.push(linkResolve)
142
+ mdxConfig.rehypePlugins.push(methanolCtx)
143
+ return (cachedMdxConfig = mdxConfig)
144
+ }
145
+
146
+ export const compileMdx = async ({ content, filePath, ctx }) => {
147
+ const mdxConfig = await resolveMdxConfig()
148
+ const runtimeFactory = mdxConfig.development ? JSXDevFactory : JSXFactory
149
+ const compiled = await compile({ value: content, path: filePath }, mdxConfig)
150
+
151
+ return await run(compiled, {
152
+ ...runtimeFactory,
153
+ baseUrl: pathToFileURL(filePath).href,
154
+ ctx,
155
+ rawHTML: HTMLRenderer.rawHTML
156
+ })
157
+ }
158
+
159
+ export const compilePageMdx = async (page, pagesContext) => {
160
+ if (!page || page.content == null || page.mdxComponent) return
161
+ const mdxModule = await compileMdx({
162
+ content: page.content,
163
+ filePath: page.filePath,
164
+ ctx: buildPageContext({
165
+ routePath: page.routePath,
166
+ filePath: page.filePath,
167
+ pageMeta: page,
168
+ pagesContext
169
+ })
170
+ })
171
+ page.mdxComponent = mdxModule.default
172
+ page.toc = mdxModule.toc
173
+ const shouldUseTocTitle = page.frontmatter?.title == null
174
+ if (shouldUseTocTitle) {
175
+ const nextTitle = findTitleFromToc(page.toc) || page.title
176
+ if (nextTitle !== page.title) {
177
+ page.title = nextTitle
178
+ if (typeof pagesContext?.refreshPagesTree === 'function') {
179
+ pagesContext.refreshPagesTree()
180
+ }
181
+ }
182
+ }
183
+ if (typeof pagesContext?.setDerivedTitle === 'function') {
184
+ pagesContext.setDerivedTitle(
185
+ page.filePath,
186
+ shouldUseTocTitle ? page.title : null,
187
+ page.toc
188
+ )
189
+ }
190
+ }
191
+
192
+ export const renderHtml = async ({
193
+ routePath,
194
+ filePath,
195
+ components,
196
+ pagesContext,
197
+ pageMeta: explicitPageMeta = null
198
+ }) => {
199
+ const pageMeta =
200
+ explicitPageMeta ||
201
+ (pagesContext.getPageByRoute
202
+ ? pagesContext.getPageByRoute(routePath, { filePath })
203
+ : pagesContext.pagesByRoute.get(routePath))
204
+
205
+ const ctx = buildPageContext({
206
+ routePath,
207
+ filePath,
208
+ pageMeta,
209
+ pagesContext
210
+ })
211
+
212
+ const [Head, Outlet] = createPortal()
213
+ const ExtraHead = () => {
214
+ return [RWND_INJECT, ...resolveUserHeadAssets(), Outlet(), RWND_FALLBACK]
215
+ }
216
+
217
+ await compilePageMdx(pageMeta, pagesContext)
218
+ const mdxComponent = pageMeta?.mdxComponent
219
+
220
+ const Page = ({ components: extraComponents, ...props }, ...children) =>
221
+ mdxComponent({
222
+ children,
223
+ ...props,
224
+ components: {
225
+ ...components,
226
+ ...extraComponents,
227
+ head: Head,
228
+ Head
229
+ }
230
+ })
231
+
232
+ const template = state.USER_THEME.template
233
+
234
+ const renderResult = await new Promise((r) => {
235
+ const result = HTMLRenderer.c(
236
+ Suspense,
237
+ {
238
+ onLoad() {
239
+ nextTick(() => r(result))
240
+ }
241
+ },
242
+ () =>
243
+ template({
244
+ ctx,
245
+ Page,
246
+ ExtraHead,
247
+ HTMLRenderer,
248
+ components
249
+ })
250
+ )
251
+ })
252
+
253
+ return HTMLRenderer.serialize(renderResult)
254
+ }
@@ -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
+ }
@@ -0,0 +1,99 @@
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 { access } from 'fs/promises'
22
+ import { constants } from 'fs'
23
+ import { join } from 'path'
24
+ import { spawn } from 'child_process'
25
+ import { state } from './state.js'
26
+
27
+ const resolvePagefindBin = async () => {
28
+ const binName = process.platform === 'win32' ? 'pagefind.cmd' : 'pagefind'
29
+ const candidates = [
30
+ join(state.PROJECT_ROOT, 'node_modules', '.bin', binName),
31
+ join(state.ROOT_DIR, 'node_modules', '.bin', binName)
32
+ ]
33
+ for (const candidate of candidates) {
34
+ try {
35
+ await access(candidate, constants.X_OK)
36
+ return candidate
37
+ } catch {}
38
+ }
39
+ return null
40
+ }
41
+
42
+ const toKebabCase = (value) =>
43
+ String(value)
44
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
45
+ .replace(/_/g, '-')
46
+ .toLowerCase()
47
+
48
+ const buildArgsFromOptions = (options) => {
49
+ if (!options) return []
50
+ if (Array.isArray(options)) {
51
+ return options.map(String)
52
+ }
53
+ if (typeof options !== 'object') return []
54
+ const args = []
55
+ for (const [rawKey, rawValue] of Object.entries(options)) {
56
+ if (!rawKey) continue
57
+ const key = String(rawKey)
58
+ const normalized = toKebabCase(key.replace(/^--/, ''))
59
+ if (normalized === 'site' || normalized === 'site-dir' || normalized === 'source') {
60
+ continue
61
+ }
62
+ const flag = key.startsWith('--') ? key : `--${normalized}`
63
+ if (rawValue === true) {
64
+ args.push(flag)
65
+ } else if (rawValue === false || rawValue == null) {
66
+ continue
67
+ } else {
68
+ args.push(flag, String(rawValue))
69
+ }
70
+ }
71
+ return args
72
+ }
73
+
74
+ const runCommand = (command, args, options) =>
75
+ new Promise((resolve) => {
76
+ const child = spawn(command, args, {
77
+ stdio: 'inherit',
78
+ ...options
79
+ })
80
+ child.on('close', (code) => resolve(code === 0))
81
+ child.on('error', () => resolve(false))
82
+ })
83
+
84
+ export const runPagefind = async () => {
85
+ const bin = await resolvePagefindBin()
86
+ if (!bin) {
87
+ console.log('Pagefind not found; skipping search indexing.')
88
+ return false
89
+ }
90
+ console.log('Running Pagefind search indexing...')
91
+ const extraArgs = buildArgsFromOptions(state.PAGEFIND_BUILD_OPTIONS)
92
+ const ok = await runCommand(bin, ['--site', state.DIST_DIR, ...extraArgs], {
93
+ cwd: state.PROJECT_ROOT
94
+ })
95
+ if (!ok) {
96
+ console.warn('Pagefind failed to build search index.')
97
+ }
98
+ return ok
99
+ }