methanol 0.0.21 → 0.0.23

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,10 +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, feedIds = [] } = message || {}
182
+ const { ids = [], stage, feedIds = [], htmlStageDir = null, writeConcurrency = null } = message || {}
173
183
  const { renderHtml, renderPageContent } = await import('../mdx.js')
174
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 = []
175
190
  let completed = 0
176
191
  for (const id of ids) {
177
192
  const page = pages[id]
@@ -188,6 +203,8 @@ const handleRender = async (message) => {
188
203
  pagesContext,
189
204
  pageMeta: page
190
205
  })
206
+ let outputHtml = html
207
+ let scan = null
191
208
  let feedContent = null
192
209
  if (feedSet.has(id)) {
193
210
  feedContent = await renderPageContent({
@@ -198,7 +215,45 @@ const handleRender = async (message) => {
198
215
  pageMeta: page
199
216
  })
200
217
  }
201
- parentPort?.postMessage({ type: 'result', stage, result: { id, html, feedContent } })
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
+ })
202
257
  } catch (error) {
203
258
  logPageError('MDX render', page, error)
204
259
  throw error
@@ -206,6 +261,103 @@ const handleRender = async (message) => {
206
261
  completed += 1
207
262
  parentPort?.postMessage({ type: 'progress', stage, completed })
208
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
+ }
271
+ }
272
+
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 || [])
313
+ let completed = 0
314
+ for (const id of ids) {
315
+ const page = pages[id]
316
+ if (!page) {
317
+ completed += 1
318
+ parentPort?.postMessage({ type: 'progress', stage, completed })
319
+ continue
320
+ }
321
+ try {
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 } })
354
+ } catch (error) {
355
+ logPageError('HTML rewrite', page, error)
356
+ throw error
357
+ }
358
+ completed += 1
359
+ parentPort?.postMessage({ type: 'progress', stage, completed })
360
+ }
209
361
  }
210
362
 
211
363
  parentPort?.on('message', async (message) => {
@@ -217,6 +369,11 @@ parentPort?.on('message', async (message) => {
217
369
  parentPort?.postMessage({ type: 'done', stage: 'setPages' })
218
370
  return
219
371
  }
372
+ if (type === 'setPagesLite') {
373
+ await handleSetPagesLite(message)
374
+ parentPort?.postMessage({ type: 'done', stage: 'setPagesLite' })
375
+ return
376
+ }
220
377
  if (type === 'sync') {
221
378
  await handleSyncUpdates(message)
222
379
  parentPort?.postMessage({ type: 'done', stage: 'sync' })
@@ -232,6 +389,11 @@ parentPort?.on('message', async (message) => {
232
389
  parentPort?.postMessage({ type: 'done', stage })
233
390
  return
234
391
  }
392
+ if (type === 'rewrite') {
393
+ await handleRewrite(message)
394
+ parentPort?.postMessage({ type: 'done', stage })
395
+ return
396
+ }
235
397
  } catch (error) {
236
398
  parentPort?.postMessage({ type: 'error', stage, error: serializeError(error) })
237
399
  }
@@ -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>
@@ -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"