methanol 0.0.11 → 0.0.13

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.
@@ -0,0 +1,156 @@
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 { cpus } from 'os'
22
+ import { Worker } from 'worker_threads'
23
+ import { state, cli } from '../state.js'
24
+
25
+ const BUILD_WORKER_URL = new URL('./build-worker.js', import.meta.url)
26
+ const cliOverrides = {
27
+ CLI_INTERMEDIATE_DIR: cli.CLI_INTERMEDIATE_DIR,
28
+ CLI_EMIT_INTERMEDIATE: cli.CLI_EMIT_INTERMEDIATE,
29
+ CLI_HOST: cli.CLI_HOST,
30
+ CLI_PORT: cli.CLI_PORT,
31
+ CLI_PAGES_DIR: cli.CLI_PAGES_DIR,
32
+ CLI_COMPONENTS_DIR: cli.CLI_COMPONENTS_DIR,
33
+ CLI_ASSETS_DIR: cli.CLI_ASSETS_DIR,
34
+ CLI_OUTPUT_DIR: cli.CLI_OUTPUT_DIR,
35
+ CLI_CONFIG_PATH: cli.CLI_CONFIG_PATH,
36
+ CLI_SITE_NAME: cli.CLI_SITE_NAME,
37
+ CLI_CODE_HIGHLIGHTING: cli.CLI_CODE_HIGHLIGHTING,
38
+ CLI_JOBS: cli.CLI_JOBS,
39
+ CLI_VERBOSE: cli.CLI_VERBOSE,
40
+ CLI_BASE: cli.CLI_BASE,
41
+ CLI_SEARCH: cli.CLI_SEARCH,
42
+ CLI_PWA: cli.CLI_PWA
43
+ }
44
+
45
+ const resolveWorkerCount = (total) => {
46
+ const cpuCount = Math.max(1, cpus()?.length || 1)
47
+ const requested = state.WORKER_JOBS
48
+ if (requested == null || requested <= 0) {
49
+ const items = Math.max(1, Number.isFinite(total) ? total : 1)
50
+ const autoCount = Math.round(Math.log(items))
51
+ return Math.max(1, Math.min(cpuCount, autoCount))
52
+ }
53
+ return Math.max(1, Math.min(cpuCount, Math.floor(requested)))
54
+ }
55
+
56
+ export const createBuildWorkers = (pageCount, options = {}) => {
57
+ const { command = 'build' } = options || {}
58
+ const workerCount = Math.min(resolveWorkerCount(pageCount), pageCount || 1) || 1
59
+ const workers = []
60
+ for (let i = 0; i < workerCount; i += 1) {
61
+ workers.push(
62
+ new Worker(BUILD_WORKER_URL, {
63
+ type: 'module',
64
+ workerData: {
65
+ mode: state.CURRENT_MODE,
66
+ configPath: cli.CLI_CONFIG_PATH,
67
+ command,
68
+ cli: cliOverrides
69
+ }
70
+ })
71
+ )
72
+ }
73
+ const assignments = Array.from({ length: workers.length }, () => [])
74
+ for (let i = 0; i < pageCount; i += 1) {
75
+ assignments[i % workers.length].push(i)
76
+ }
77
+ return { workers, assignments }
78
+ }
79
+
80
+ export const terminateWorkers = async (workers = []) => {
81
+ await Promise.all(workers.map((worker) => worker.terminate().catch(() => null)))
82
+ }
83
+
84
+ export const runWorkerStage = async ({ workers, stage, messages, onProgress, collect }) => {
85
+ return await new Promise((resolve, reject) => {
86
+ let completed = 0
87
+ let doneCount = 0
88
+ const results = []
89
+ const handleFailure = (error) => {
90
+ for (const w of workers) {
91
+ const handler = handlers.get(w)
92
+ if (handler) {
93
+ w.off('message', handler)
94
+ w.off('error', errorHandlers.get(w))
95
+ w.off('exit', exitHandlers.get(w))
96
+ }
97
+ }
98
+ reject(error instanceof Error ? error : new Error(String(error)))
99
+ }
100
+ const handleMessage = (worker, message) => {
101
+ if (!message || message.stage !== stage) return
102
+ if (message.type === 'progress') {
103
+ completed += 1
104
+ if (onProgress) {
105
+ onProgress(completed)
106
+ }
107
+ return
108
+ }
109
+ if (message.type === 'done') {
110
+ if (collect) {
111
+ const data = collect(message)
112
+ if (Array.isArray(data) && data.length) {
113
+ results.push(...data)
114
+ }
115
+ }
116
+ doneCount += 1
117
+ if (doneCount >= workers.length) {
118
+ for (const w of workers) {
119
+ const handler = handlers.get(w)
120
+ if (handler) {
121
+ w.off('message', handler)
122
+ w.off('error', errorHandlers.get(w))
123
+ w.off('exit', exitHandlers.get(w))
124
+ }
125
+ }
126
+ resolve(results)
127
+ }
128
+ return
129
+ }
130
+ if (message.type === 'error') {
131
+ handleFailure(new Error(message.error || 'Worker error'))
132
+ }
133
+ }
134
+ const handlers = new Map()
135
+ const errorHandlers = new Map()
136
+ const exitHandlers = new Map()
137
+ for (const worker of workers) {
138
+ const handler = (message) => handleMessage(worker, message)
139
+ handlers.set(worker, handler)
140
+ worker.on('message', handler)
141
+ const errorHandler = (error) => handleFailure(error)
142
+ const exitHandler = (code) => {
143
+ if (code !== 0) {
144
+ handleFailure(new Error(`Build worker exited with code ${code}`))
145
+ }
146
+ }
147
+ errorHandlers.set(worker, errorHandler)
148
+ exitHandlers.set(worker, exitHandler)
149
+ worker.on('error', errorHandler)
150
+ worker.on('exit', exitHandler)
151
+ }
152
+ for (const entry of messages) {
153
+ entry.worker.postMessage(entry.message)
154
+ }
155
+ })
156
+ }
@@ -0,0 +1,231 @@
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 '../register-loader.js'
22
+ import { parentPort, workerData } from 'worker_threads'
23
+ import { style } from '../logger.js'
24
+
25
+ const { mode = 'production', configPath = null, command = 'build', cli: cliOverrides = null } =
26
+ workerData || {}
27
+ let initPromise = null
28
+ let pages = []
29
+ let pagesContext = null
30
+ let components = null
31
+
32
+ const ensureInit = async () => {
33
+ if (initPromise) return initPromise
34
+ initPromise = (async () => {
35
+ const { loadUserConfig, applyConfig, resolveUserViteConfig } = await import('../config.js')
36
+ const { buildComponentRegistry } = await import('../components.js')
37
+ const { state, cli } = await import('../state.js')
38
+ if (cliOverrides) {
39
+ Object.assign(cli, cliOverrides)
40
+ }
41
+ const config = await loadUserConfig(mode, configPath)
42
+ await applyConfig(config, mode)
43
+ await resolveUserViteConfig(command)
44
+ const themeComponentsDir = state.THEME_COMPONENTS_DIR
45
+ const themeEnv = state.THEME_ENV
46
+ const themeRegistry = themeComponentsDir
47
+ ? await buildComponentRegistry({
48
+ componentsDir: themeComponentsDir,
49
+ register: themeEnv.register
50
+ })
51
+ : { components: {} }
52
+ const themeComponents = {
53
+ ...(themeRegistry.components || {}),
54
+ ...(state.USER_THEME.components || {})
55
+ }
56
+ const registry = await buildComponentRegistry()
57
+ components = {
58
+ ...themeComponents,
59
+ ...(registry.components || {})
60
+ }
61
+ })()
62
+ return initPromise
63
+ }
64
+
65
+ const rebuildPagesContext = async (excludedRoutes, excludedDirs) => {
66
+ const { createPagesContextFromPages } = await import('../pages.js')
67
+ pagesContext = createPagesContextFromPages({
68
+ pages,
69
+ excludedRoutes,
70
+ excludedDirs
71
+ })
72
+ }
73
+
74
+ const refreshMdxCtx = (page) => {
75
+ if (!page?.mdxCtx || !pagesContext) return
76
+ const ctx = page.mdxCtx
77
+ ctx.page = page
78
+ ctx.pages = pagesContext.pages || []
79
+ ctx.pagesByRoute = pagesContext.pagesByRoute || new Map()
80
+ ctx.languages = pagesContext.languages || []
81
+ ctx.language = pagesContext.getLanguageForRoute
82
+ ? pagesContext.getLanguageForRoute(page.routePath)
83
+ : null
84
+ ctx.site = pagesContext.site || null
85
+ ctx.getSiblings = pagesContext.getSiblings
86
+ ? () => pagesContext.getSiblings(page.routePath, page.path)
87
+ : null
88
+ if (page && ctx.getSiblings && page.getSiblings !== ctx.getSiblings) {
89
+ page.getSiblings = ctx.getSiblings
90
+ }
91
+ if (pagesContext.getPagesTree) {
92
+ ctx.pagesTree = pagesContext.getPagesTree(page.routePath)
93
+ } else {
94
+ ctx.pagesTree = pagesContext.pagesTree || []
95
+ }
96
+ }
97
+
98
+ const serializeError = (error) => {
99
+ if (!error) return 'Unknown error'
100
+ if (error.stack) return error.stack
101
+ if (error.message) return error.message
102
+ return String(error)
103
+ }
104
+
105
+ const logPageError = (phase, page, error) => {
106
+ const target = page?.path || page?.routePath || 'unknown file'
107
+ console.error(style.red(`\n\n[methanol] ${phase} error in ${target}`))
108
+ // Error is thrown so wo don't need to print here
109
+ // console.error(error?.stack || error)
110
+ }
111
+
112
+ const handleSetPages = async (message) => {
113
+ const { pages: nextPages, excludedRoutes = [], excludedDirs = [] } = message || {}
114
+ pages = Array.isArray(nextPages) ? nextPages : []
115
+ await rebuildPagesContext(new Set(excludedRoutes), new Set(excludedDirs))
116
+ }
117
+
118
+ const handleSyncUpdates = async (message) => {
119
+ const { updates = [], titles = null, excludedRoutes = null, excludedDirs = null } = message || {}
120
+ if (Array.isArray(titles)) {
121
+ for (let i = 0; i < titles.length; i += 1) {
122
+ const page = pages[i]
123
+ if (!page) continue
124
+ if (titles[i] !== undefined) {
125
+ page.title = titles[i]
126
+ }
127
+ }
128
+ }
129
+ for (const update of updates) {
130
+ const page = pages[update.id]
131
+ if (!page) continue
132
+ if (update.title !== undefined) page.title = update.title
133
+ if (update.toc !== undefined) page.toc = update.toc
134
+ }
135
+ await rebuildPagesContext(
136
+ excludedRoutes ? new Set(excludedRoutes) : pagesContext?.excludedRoutes || new Set(),
137
+ excludedDirs ? new Set(excludedDirs) : pagesContext?.excludedDirs || new Set()
138
+ )
139
+ for (const page of pages) {
140
+ if (!page?.mdxCtx) continue
141
+ refreshMdxCtx(page)
142
+ }
143
+ }
144
+
145
+ const handleCompile = async (message) => {
146
+ const { ids = [], stage } = message || {}
147
+ const { compilePageMdx } = await import('../mdx.js')
148
+ const updates = []
149
+ let completed = 0
150
+ for (const id of ids) {
151
+ const page = pages[id]
152
+ if (!page || page.content == null || page.mdxComponent) {
153
+ completed += 1
154
+ parentPort?.postMessage({ type: 'progress', stage, completed })
155
+ continue
156
+ }
157
+ try {
158
+ await compilePageMdx(page, pagesContext, {
159
+ lazyPagesTree: true,
160
+ refreshPagesTree: false
161
+ })
162
+ updates.push({ id, title: page.title, toc: page.toc || null })
163
+ } catch (error) {
164
+ logPageError('MDX compile', page, error)
165
+ throw error
166
+ }
167
+ completed += 1
168
+ parentPort?.postMessage({ type: 'progress', stage, completed })
169
+ }
170
+ return updates
171
+ }
172
+
173
+ const handleRender = async (message) => {
174
+ const { ids = [], stage } = message || {}
175
+ const { renderHtml } = await import('../mdx.js')
176
+ const results = []
177
+ let completed = 0
178
+ for (const id of ids) {
179
+ const page = pages[id]
180
+ if (!page) {
181
+ completed += 1
182
+ parentPort?.postMessage({ type: 'progress', stage, completed })
183
+ continue
184
+ }
185
+ try {
186
+ const html = await renderHtml({
187
+ routePath: page.routePath,
188
+ path: page.path,
189
+ components,
190
+ pagesContext,
191
+ pageMeta: page
192
+ })
193
+ results.push({ id, html })
194
+ } catch (error) {
195
+ logPageError('MDX render', page, error)
196
+ throw error
197
+ }
198
+ completed += 1
199
+ parentPort?.postMessage({ type: 'progress', stage, completed })
200
+ }
201
+ return results
202
+ }
203
+
204
+ parentPort?.on('message', async (message) => {
205
+ const { type, stage } = message || {}
206
+ try {
207
+ await ensureInit()
208
+ if (type === 'setPages') {
209
+ await handleSetPages(message)
210
+ parentPort?.postMessage({ type: 'done', stage: 'setPages' })
211
+ return
212
+ }
213
+ if (type === 'sync') {
214
+ await handleSyncUpdates(message)
215
+ parentPort?.postMessage({ type: 'done', stage: 'sync' })
216
+ return
217
+ }
218
+ if (type === 'compile') {
219
+ const updates = await handleCompile(message)
220
+ parentPort?.postMessage({ type: 'done', stage, updates })
221
+ return
222
+ }
223
+ if (type === 'render') {
224
+ const results = await handleRender(message)
225
+ parentPort?.postMessage({ type: 'done', stage, results })
226
+ return
227
+ }
228
+ } catch (error) {
229
+ parentPort?.postMessage({ type: 'error', stage, error: serializeError(error) })
230
+ }
231
+ })
@@ -0,0 +1,60 @@
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 '../register-loader.js'
22
+ import { parentPort, workerData } from 'worker_threads'
23
+
24
+ const { mode = 'production', configPath = null, cli: cliOverrides = null } = workerData || {}
25
+ let initPromise = null
26
+ let compileMdxSource = null
27
+
28
+ const ensureInit = async () => {
29
+ if (initPromise) return initPromise
30
+ initPromise = (async () => {
31
+ const { loadUserConfig, applyConfig } = await import('../config.js')
32
+ const { cli } = await import('../state.js')
33
+ if (cliOverrides) {
34
+ Object.assign(cli, cliOverrides)
35
+ }
36
+ const mdx = await import('../mdx.js')
37
+ compileMdxSource = mdx.compileMdxSource
38
+ const config = await loadUserConfig(mode, configPath)
39
+ await applyConfig(config, mode)
40
+ })()
41
+ return initPromise
42
+ }
43
+
44
+ const serializeError = (error) => {
45
+ if (!error) return 'Unknown error'
46
+ if (error.stack) return error.stack
47
+ if (error.message) return error.message
48
+ return String(error)
49
+ }
50
+
51
+ parentPort?.on('message', async (message) => {
52
+ const { id, path, content, frontmatter } = message || {}
53
+ try {
54
+ await ensureInit()
55
+ const result = await compileMdxSource({ content, path, frontmatter })
56
+ parentPort?.postMessage({ id, result })
57
+ } catch (error) {
58
+ parentPort?.postMessage({ id, error: serializeError(error) })
59
+ }
60
+ })
@@ -18,6 +18,6 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
- export default function () {
22
- // render nothing on server side
23
- }
21
+ // Nothing needs to be rendered on the server side for accent switch
22
+ // But we explicitly need to keep a file here to prevent Methanol from
23
+ // rendering the client version as fallback
@@ -18,6 +18,6 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
- export default function () {
22
- // render nothing on server side
23
- }
21
+ // Nothing needs to be rendered on the server side for accent switch
22
+ // But we explicitly need to keep a file here to prevent Methanol from
23
+ // rendering the client version as fallback
@@ -38,7 +38,7 @@ export default function (props, ...children) {
38
38
  }
39
39
  }
40
40
 
41
- const Btn = copied.choose(
41
+ const BtnImg = copied.choose(
42
42
  () => (
43
43
  <svg
44
44
  attr:width="14"
@@ -74,7 +74,7 @@ export default function (props, ...children) {
74
74
  return (
75
75
  <div class="code-block-container">
76
76
  <button class="copy-btn" on:click={copy} attr:aria-label="Copy code">
77
- <Btn />
77
+ <BtnImg />
78
78
  </button>
79
79
  <pre {...props} $ref={el}>
80
80
  {...children}
@@ -29,7 +29,7 @@
29
29
  if (savedAccent && savedAccent !== 'default') {
30
30
  document.documentElement.classList.add('accent-' + savedAccent)
31
31
  }
32
- })()
32
+ })
33
33
  ;(function initPrefetch() {
34
34
  const prefetched = new Set()
35
35
  const canPrefetch = (anchor) => {
@@ -58,4 +58,5 @@
58
58
  document.head.appendChild(link)
59
59
  }
60
60
  document.addEventListener('pointerover', onHover, { capture: true, passive: true })
61
- })()
61
+ })
62
+ console.log('theme prep')
@@ -79,11 +79,12 @@ const NavTree = ({ nodes, depth }) => (
79
79
  <li class={isActive.choose('is-active', null)}>
80
80
  <details class="sidebar-collapsible" open={isOpen.choose(true, null)}>
81
81
  <summary class="sb-dir-header">{header}</summary>
82
- <If condition={() => children.value.length}>{() => (
83
- <ul data-depth={depth + 1}>
84
- <NavTree nodes={children} depth={depth + 1} />
85
- </ul>
86
- )}
82
+ <If condition={() => children.value.length}>
83
+ {() => (
84
+ <ul data-depth={depth + 1}>
85
+ <NavTree nodes={children} depth={depth + 1} />
86
+ </ul>
87
+ )}
87
88
  </If>
88
89
  </details>
89
90
  </li>
@@ -22,7 +22,7 @@ import { HTMLRenderer as R, DOCTYPE_HTML } from 'methanol'
22
22
  import { renderToc } from '../components/ThemeToCContainer.static.jsx'
23
23
  import { renderNavTree } from './nav-tree.jsx'
24
24
 
25
- const PAGE_TEMPLATE = async ({ PageContent, ExtraHead, components, ctx }) => {
25
+ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx }) => {
26
26
  const page = ctx.page
27
27
  const pagesByRoute = ctx.pagesByRoute
28
28
  const pages = ctx.pages || []
@@ -122,13 +122,13 @@ const PAGE_TEMPLATE = async ({ PageContent, ExtraHead, components, ctx }) => {
122
122
  {twitterTitle ? <meta name="twitter:title" content={twitterTitle} /> : null}
123
123
  {twitterDescription ? <meta name="twitter:description" content={twitterDescription} /> : null}
124
124
  {twitterImage ? <meta name="twitter:image" content={twitterImage} /> : null}
125
- <ExtraHead />
126
125
  <link
127
126
  rel="preload stylesheet"
128
127
  as="style"
129
128
  href="/.methanol_theme_default/style.css"
130
129
  />
131
- <script src="/theme-prepare.js"></script>
130
+ <script type="module" src="/.methanol_theme_default/theme-prepare.js"></script>
131
+ <ExtraHead />
132
132
  </head>
133
133
  <body>
134
134
  <input type="checkbox" id="nav-toggle" class="nav-toggle" />