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.
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/build-system.js +116 -44
- package/src/config.js +13 -2
- package/src/dev-server.js +155 -24
- package/src/logger.js +15 -3
- package/src/mdx.js +68 -39
- package/src/pages.js +216 -56
- package/src/state.js +18 -14
- package/src/workers/build-pool.js +156 -0
- package/src/workers/build-worker.js +203 -0
- package/src/workers/mdx-compile-worker.js +60 -0
- package/themes/default/src/nav-tree.jsx +6 -5
|
@@ -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
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
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>
|