methanol 0.0.20 → 0.0.22

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.
@@ -18,9 +18,11 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
- import '../register-loader.js'
22
21
  import { parentPort, workerData } from 'worker_threads'
22
+ import { mkdir, writeFile, readFile, copyFile } from 'fs/promises'
23
+ import { resolve, join, dirname } from 'path'
23
24
  import { style } from '../logger.js'
25
+ import { scanRenderedHtml, rewriteHtmlContent, resolveManifestEntry } from '../html/worker-html.js'
24
26
 
25
27
  const { mode = 'production', configPath = null, command = 'build', cli: cliOverrides = null } =
26
28
  workerData || {}
@@ -29,6 +31,7 @@ let pages = []
29
31
  let pagesContext = null
30
32
  let components = null
31
33
  let mdxPageIds = new Set()
34
+ const parsedHtmlCache = new Map()
32
35
 
33
36
  const ensureInit = async () => {
34
37
  if (initPromise) return initPromise
@@ -117,6 +120,11 @@ const handleSetPages = async (message) => {
117
120
  await rebuildPagesContext(new Set(excludedRoutes), new Set(excludedDirs))
118
121
  }
119
122
 
123
+ const handleSetPagesLite = async (message) => {
124
+ const { pages: nextPages } = message || {}
125
+ pages = Array.isArray(nextPages) ? nextPages : []
126
+ }
127
+
120
128
  const handleSyncUpdates = async (message) => {
121
129
  const { updates = [], excludedRoutes = null, excludedDirs = null } = message || {}
122
130
  for (const update of updates) {
@@ -168,9 +176,17 @@ const handleCompile = async (message) => {
168
176
  return updates
169
177
  }
170
178
 
179
+ const MAX_PENDING_WRITES = 32
180
+
171
181
  const handleRender = async (message) => {
172
- const { ids = [], stage } = message || {}
173
- const { renderHtml } = await import('../mdx.js')
182
+ const { ids = [], stage, feedIds = [], htmlStageDir = null, writeConcurrency = null } = message || {}
183
+ const { renderHtml, renderPageContent } = await import('../mdx.js')
184
+ const feedSet = new Set(Array.isArray(feedIds) ? feedIds : [])
185
+ const writeLimit =
186
+ typeof writeConcurrency === 'number' && Number.isFinite(writeConcurrency)
187
+ ? Math.max(1, Math.floor(writeConcurrency))
188
+ : MAX_PENDING_WRITES
189
+ const pendingWrites = []
174
190
  let completed = 0
175
191
  for (const id of ids) {
176
192
  const page = pages[id]
@@ -187,7 +203,57 @@ const handleRender = async (message) => {
187
203
  pagesContext,
188
204
  pageMeta: page
189
205
  })
190
- parentPort?.postMessage({ type: 'result', stage, result: { id, html } })
206
+ let outputHtml = html
207
+ let scan = null
208
+ let feedContent = null
209
+ if (feedSet.has(id)) {
210
+ feedContent = await renderPageContent({
211
+ routePath: page.routePath,
212
+ path: page.path,
213
+ components,
214
+ pagesContext,
215
+ pageMeta: page
216
+ })
217
+ }
218
+ let stagePath = null
219
+ if (htmlStageDir) {
220
+ const scanned = await scanRenderedHtml(outputHtml, page.routePath)
221
+ outputHtml = scanned.html
222
+ scan = scanned.scan
223
+ const hasResources =
224
+ scan.scripts.length > 0 || scan.styles.length > 0 || scan.assets.length > 0
225
+ if (hasResources) {
226
+ parsedHtmlCache.set(id, scanned.plan)
227
+ }
228
+ const name = resolveOutputName(page)
229
+ stagePath = resolve(htmlStageDir, `${name}.html`)
230
+ pendingWrites.push(
231
+ (async () => {
232
+ await mkdir(dirname(stagePath), { recursive: true })
233
+ await writeFile(stagePath, outputHtml)
234
+ })()
235
+ )
236
+ if (pendingWrites.length >= writeLimit) {
237
+ const results = await Promise.allSettled(pendingWrites)
238
+ pendingWrites.length = 0
239
+ const failed = results.find((result) => result.status === 'rejected')
240
+ if (failed) {
241
+ throw failed.reason
242
+ }
243
+ }
244
+ }
245
+ page.mdxComponent = null
246
+ parentPort?.postMessage({
247
+ type: 'result',
248
+ stage,
249
+ result: {
250
+ id,
251
+ html: htmlStageDir ? null : outputHtml,
252
+ stagePath,
253
+ feedContent,
254
+ scan
255
+ }
256
+ })
191
257
  } catch (error) {
192
258
  logPageError('MDX render', page, error)
193
259
  throw error
@@ -195,11 +261,55 @@ const handleRender = async (message) => {
195
261
  completed += 1
196
262
  parentPort?.postMessage({ type: 'progress', stage, completed })
197
263
  }
264
+ if (pendingWrites.length) {
265
+ const results = await Promise.allSettled(pendingWrites)
266
+ const failed = results.find((result) => result.status === 'rejected')
267
+ if (failed) {
268
+ throw failed.reason
269
+ }
270
+ }
198
271
  }
199
272
 
200
- const handleRss = async (message) => {
201
- const { ids = [], stage } = message || {}
202
- const { renderPageContent } = await import('../mdx.js')
273
+ const resolveOutputName = (page) => {
274
+ if (!page) return 'index'
275
+ if (page.routePath === '/') return 'index'
276
+ if (page.isIndex && page.dir) {
277
+ return join(page.dir, 'index').replace(/\\/g, '/')
278
+ }
279
+ return page.routePath.slice(1)
280
+ }
281
+
282
+ const handleRewrite = async (message) => {
283
+ const {
284
+ ids = [],
285
+ stage,
286
+ htmlStageDir,
287
+ manifest,
288
+ entryModules = [],
289
+ commonScripts = [],
290
+ commonEntry = null,
291
+ scans = {}
292
+ } = message || {}
293
+ const { state } = await import('../state.js')
294
+ const { resolveBasePrefix } = await import('../config.js')
295
+ const basePrefix = resolveBasePrefix()
296
+ const scriptMap = new Map()
297
+ const styleMap = new Map()
298
+ for (const entry of entryModules) {
299
+ if (!entry?.publicPath || !entry?.manifestKey) continue
300
+ const manifestEntry = resolveManifestEntry(manifest, entry.manifestKey)
301
+ if (!manifestEntry?.file) continue
302
+ if (entry.kind === 'script') {
303
+ scriptMap.set(entry.publicPath, { file: manifestEntry.file, css: manifestEntry.css || null })
304
+ }
305
+ if (entry.kind === 'style') {
306
+ const cssFile = manifestEntry.css?.[0] || (manifestEntry.file.endsWith('.css') ? manifestEntry.file : null)
307
+ if (cssFile) {
308
+ styleMap.set(entry.publicPath, { file: cssFile, css: manifestEntry.css || null })
309
+ }
310
+ }
311
+ }
312
+ const commonSet = new Set(commonScripts || [])
203
313
  let completed = 0
204
314
  for (const id of ids) {
205
315
  const page = pages[id]
@@ -209,16 +319,40 @@ const handleRss = async (message) => {
209
319
  continue
210
320
  }
211
321
  try {
212
- const content = await renderPageContent({
213
- routePath: page.routePath,
214
- path: page.path,
215
- components,
216
- pagesContext,
217
- pageMeta: page
218
- })
219
- parentPort?.postMessage({ type: 'result', stage, result: { id, content } })
322
+ const name = resolveOutputName(page)
323
+ const stagePath = htmlStageDir ? resolve(htmlStageDir, `${name}.html`) : null
324
+ const distPath = resolve(state.DIST_DIR, `${name}.html`)
325
+ await mkdir(dirname(distPath), { recursive: true })
326
+ const scan = scans?.[id] || null
327
+ if (scan && Array.isArray(scan.scripts) && Array.isArray(scan.styles) && Array.isArray(scan.assets)) {
328
+ if (scan.scripts.length === 0 && scan.styles.length === 0 && scan.assets.length === 0) {
329
+ await copyFile(stagePath, distPath)
330
+ completed += 1
331
+ parentPort?.postMessage({ type: 'progress', stage, completed })
332
+ continue
333
+ }
334
+ }
335
+ const plan = parsedHtmlCache.get(id)
336
+ const html = await readFile(stagePath, 'utf-8')
337
+ if (html == null) {
338
+ throw new Error('HTML content not available for rewrite')
339
+ }
340
+ const output = rewriteHtmlContent(
341
+ html,
342
+ plan,
343
+ page.routePath,
344
+ basePrefix,
345
+ manifest,
346
+ scriptMap,
347
+ styleMap,
348
+ commonSet,
349
+ commonEntry
350
+ )
351
+ parsedHtmlCache.delete(id)
352
+ await writeFile(distPath, output)
353
+ parentPort?.postMessage({ type: 'result', stage, result: { id } })
220
354
  } catch (error) {
221
- logPageError('RSS render', page, error)
355
+ logPageError('HTML rewrite', page, error)
222
356
  throw error
223
357
  }
224
358
  completed += 1
@@ -235,6 +369,11 @@ parentPort?.on('message', async (message) => {
235
369
  parentPort?.postMessage({ type: 'done', stage: 'setPages' })
236
370
  return
237
371
  }
372
+ if (type === 'setPagesLite') {
373
+ await handleSetPagesLite(message)
374
+ parentPort?.postMessage({ type: 'done', stage: 'setPagesLite' })
375
+ return
376
+ }
238
377
  if (type === 'sync') {
239
378
  await handleSyncUpdates(message)
240
379
  parentPort?.postMessage({ type: 'done', stage: 'sync' })
@@ -250,8 +389,8 @@ parentPort?.on('message', async (message) => {
250
389
  parentPort?.postMessage({ type: 'done', stage })
251
390
  return
252
391
  }
253
- if (type === 'rss') {
254
- await handleRss(message)
392
+ if (type === 'rewrite') {
393
+ await handleRewrite(message)
255
394
  parentPort?.postMessage({ type: 'done', stage })
256
395
  return
257
396
  }
@@ -0,0 +1,22 @@
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('./build-worker.js')
@@ -0,0 +1,22 @@
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('./mdx-compile-worker.js')
@@ -18,7 +18,6 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
- import '../register-loader.js'
22
21
  import { parentPort, workerData } from 'worker_threads'
23
22
 
24
23
  const { mode = 'production', configPath = null, cli: cliOverrides = null } = workerData || {}
@@ -0,0 +1,5 @@
1
+ # Benchmark Theme
2
+
3
+ Some frameworks cheat at benchmark, so can we.
4
+
5
+ This theme does nothing but rendering the bodies of markdown files.
@@ -0,0 +1,33 @@
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 { fileURLToPath } from 'url'
22
+ import { dirname } from 'path'
23
+ import PAGE_TEMPLATE from './src/page.jsx'
24
+
25
+ const __filename = fileURLToPath(import.meta.url)
26
+ const __dirname = dirname(__filename)
27
+
28
+ export default () => {
29
+ return {
30
+ root: __dirname,
31
+ template: PAGE_TEMPLATE
32
+ }
33
+ }
@@ -0,0 +1,25 @@
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
+ const PAGE_TEMPLATE = ({ PageContent }) => {
22
+ return <PageContent />
23
+ }
24
+
25
+ export default PAGE_TEMPLATE
@@ -128,7 +128,6 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
128
128
  const pagefindOptions = ctx.site.pagefind?.options || null
129
129
  const feedInfo = ctx.site.feed
130
130
  const rssHref = feedInfo?.enabled ? feedInfo.href : null
131
- const feedType = feedInfo?.atom ? 'application/atom+xml' : 'application/rss+xml'
132
131
  const feedLabel = feedInfo?.atom ? 'Atom' : 'RSS'
133
132
 
134
133
  return (
@@ -142,7 +141,6 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
142
141
  {title} | {siteName}
143
142
  </title>
144
143
  <link rel="stylesheet" href="/.methanol_theme_blog/style.css" />
145
- {rssHref ? <link rel="alternate" type={feedType} title={`${siteName} ${feedLabel}`} href={rssHref} /> : null}
146
144
  <ExtraHead />
147
145
  </head>
148
146
  <body>
@@ -118,11 +118,12 @@ const NavTree = ({ nodes, depth }) => {
118
118
  )
119
119
  }
120
120
 
121
- const rootNodes = signal()
121
+ const _rootNodes = signal()
122
+ const rootNodes = signal(_rootNodes, (nodes) => nodes?.map(toSignal))
122
123
  const rootTree = HTMLRenderer.createElement(NavTree, { nodes: rootNodes, depth: 0 })
123
124
 
124
125
  export const renderNavTree = (nodes, path) => {
125
126
  currentPath.value = path
126
- rootNodes.value = nodes.map(toSignal)
127
+ _rootNodes.value = nodes
127
128
  return rootTree
128
129
  }
@@ -63,7 +63,6 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx }) => {
63
63
  const pagefindOptions = ctx.site.pagefind?.options || null
64
64
  const feedInfo = ctx.site.feed
65
65
  const rssHref = feedInfo?.enabled ? feedInfo.href : null
66
- const feedType = feedInfo?.atom ? 'application/atom+xml' : 'application/rss+xml'
67
66
  const feedLabel = feedInfo?.atom ? 'Atom' : 'RSS'
68
67
  const repoBase = ctx.site.repoBase
69
68
  const sourceUrl = pageFrontmatter.sourceURL
@@ -128,7 +127,6 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx }) => {
128
127
  {twitterTitle ? <meta name="twitter:title" content={twitterTitle} /> : null}
129
128
  {twitterDescription ? <meta name="twitter:description" content={twitterDescription} /> : null}
130
129
  {twitterImage ? <meta name="twitter:image" content={twitterImage} /> : null}
131
- {rssHref ? <link rel="alternate" type={feedType} title={`${siteName} ${feedLabel}`} href={rssHref} /> : null}
132
130
  <link
133
131
  rel="preload stylesheet"
134
132
  as="style"