methanol 0.0.11 → 0.0.12

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,203 @@
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
+ client: themeEnv.client
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 serializeError = (error) => {
75
+ if (!error) return 'Unknown error'
76
+ if (error.stack) return error.stack
77
+ if (error.message) return error.message
78
+ return String(error)
79
+ }
80
+
81
+ const logPageError = (phase, page, error) => {
82
+ const target = page?.path || page?.routePath || 'unknown file'
83
+ console.error(style.red(`\n\n[methanol] ${phase} error in ${target}`))
84
+ // Error is thrown so wo don't need to print here
85
+ // console.error(error?.stack || error)
86
+ }
87
+
88
+ const handleSetPages = async (message) => {
89
+ const { pages: nextPages, excludedRoutes = [], excludedDirs = [] } = message || {}
90
+ pages = Array.isArray(nextPages) ? nextPages : []
91
+ await rebuildPagesContext(new Set(excludedRoutes), new Set(excludedDirs))
92
+ }
93
+
94
+ const handleSyncUpdates = async (message) => {
95
+ const { updates = [], titles = null, excludedRoutes = null, excludedDirs = null } = message || {}
96
+ if (Array.isArray(titles)) {
97
+ for (let i = 0; i < titles.length; i += 1) {
98
+ const page = pages[i]
99
+ if (!page) continue
100
+ if (titles[i] !== undefined) {
101
+ page.title = titles[i]
102
+ }
103
+ }
104
+ }
105
+ for (const update of updates) {
106
+ const page = pages[update.id]
107
+ if (!page) continue
108
+ if (update.title !== undefined) page.title = update.title
109
+ if (update.toc !== undefined) page.toc = update.toc
110
+ }
111
+ await rebuildPagesContext(
112
+ excludedRoutes ? new Set(excludedRoutes) : pagesContext?.excludedRoutes || new Set(),
113
+ excludedDirs ? new Set(excludedDirs) : pagesContext?.excludedDirs || new Set()
114
+ )
115
+ }
116
+
117
+ const handleCompile = async (message) => {
118
+ const { ids = [], stage } = message || {}
119
+ const { compilePageMdx } = await import('../mdx.js')
120
+ const updates = []
121
+ let completed = 0
122
+ for (const id of ids) {
123
+ const page = pages[id]
124
+ if (!page || page.content == null || page.mdxComponent) {
125
+ completed += 1
126
+ parentPort?.postMessage({ type: 'progress', stage, completed })
127
+ continue
128
+ }
129
+ try {
130
+ await compilePageMdx(page, pagesContext, {
131
+ lazyPagesTree: true,
132
+ refreshPagesTree: false
133
+ })
134
+ updates.push({ id, title: page.title, toc: page.toc || null })
135
+ } catch (error) {
136
+ logPageError('MDX compile', page, error)
137
+ throw error
138
+ }
139
+ completed += 1
140
+ parentPort?.postMessage({ type: 'progress', stage, completed })
141
+ }
142
+ return updates
143
+ }
144
+
145
+ const handleRender = async (message) => {
146
+ const { ids = [], stage } = message || {}
147
+ const { renderHtml } = await import('../mdx.js')
148
+ const results = []
149
+ let completed = 0
150
+ for (const id of ids) {
151
+ const page = pages[id]
152
+ if (!page) {
153
+ completed += 1
154
+ parentPort?.postMessage({ type: 'progress', stage, completed })
155
+ continue
156
+ }
157
+ try {
158
+ const html = await renderHtml({
159
+ routePath: page.routePath,
160
+ path: page.path,
161
+ components,
162
+ pagesContext,
163
+ pageMeta: page
164
+ })
165
+ results.push({ id, html })
166
+ } catch (error) {
167
+ logPageError('MDX render', page, error)
168
+ throw error
169
+ }
170
+ completed += 1
171
+ parentPort?.postMessage({ type: 'progress', stage, completed })
172
+ }
173
+ return results
174
+ }
175
+
176
+ parentPort?.on('message', async (message) => {
177
+ const { type, stage } = message || {}
178
+ try {
179
+ await ensureInit()
180
+ if (type === 'setPages') {
181
+ await handleSetPages(message)
182
+ parentPort?.postMessage({ type: 'done', stage: 'setPages' })
183
+ return
184
+ }
185
+ if (type === 'sync') {
186
+ await handleSyncUpdates(message)
187
+ parentPort?.postMessage({ type: 'done', stage: 'sync' })
188
+ return
189
+ }
190
+ if (type === 'compile') {
191
+ const updates = await handleCompile(message)
192
+ parentPort?.postMessage({ type: 'done', stage, updates })
193
+ return
194
+ }
195
+ if (type === 'render') {
196
+ const results = await handleRender(message)
197
+ parentPort?.postMessage({ type: 'done', stage, results })
198
+ return
199
+ }
200
+ } catch (error) {
201
+ parentPort?.postMessage({ type: 'error', stage, error: serializeError(error) })
202
+ }
203
+ })
@@ -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
+ })
@@ -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>