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/src/mdx.js CHANGED
@@ -131,9 +131,7 @@ const resolvePageHeadAssets = (page) => {
131
131
  export const buildPageContext = ({ routePath, path, pageMeta, pagesContext, lazyPagesTree = false }) => {
132
132
  const page = pageMeta
133
133
  const language = pagesContext.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
134
- const getSiblings = pagesContext.getSiblings
135
- ? () => pagesContext.getSiblings(routePath, page.path || path)
136
- : null
134
+ const getSiblings = pagesContext.getSiblings ? () => pagesContext.getSiblings(routePath, page.path || path) : null
137
135
  if (page && getSiblings && page.getSiblings !== getSiblings) {
138
136
  page.getSiblings = getSiblings
139
137
  }
@@ -215,7 +213,7 @@ const normalizeStarryNightConfig = (value) => {
215
213
  }
216
214
  if (typeof value !== 'object') return null
217
215
  const { enabled, options, ...rest } = value
218
- if (enabled === false) return { enabled: false, options: null }
216
+ if (enabled === false) return { enabled: false, options }
219
217
  if (options && typeof options === 'object') {
220
218
  return { enabled: true, options: { ...options } }
221
219
  }
@@ -289,12 +287,17 @@ const resolveMdxConfigForPage = async (frontmatter) => {
289
287
  return mdxConfig
290
288
  }
291
289
 
292
- export const compileMdx = async ({ content, path, ctx }) => {
293
- const mdxConfig = await resolveMdxConfigForPage(ctx.page.frontmatter)
294
- const runtimeFactory = mdxConfig.development ? JSXDevFactory : JSXFactory
290
+ export const compileMdxSource = async ({ content, path, frontmatter }) => {
291
+ const mdxConfig = await resolveMdxConfigForPage(frontmatter)
295
292
  const compiled = await compile({ value: content, path: path }, mdxConfig)
293
+ const code = String(compiled.value ?? compiled)
294
+ return { code, development: Boolean(mdxConfig.development) }
295
+ }
296
296
 
297
- return await run(compiled, {
297
+ export const runMdxSource = async ({ code, path, ctx, development = null }) => {
298
+ const isDev = development == null ? state.CURRENT_MODE !== 'production' : development
299
+ const runtimeFactory = isDev ? JSXDevFactory : JSXFactory
300
+ return await run(code, {
298
301
  ...runtimeFactory,
299
302
  baseUrl: pathToFileURL(path).href,
300
303
  ctx,
@@ -302,9 +305,23 @@ export const compileMdx = async ({ content, path, ctx }) => {
302
305
  })
303
306
  }
304
307
 
308
+ export const compileMdx = async ({ content, path, ctx }) => {
309
+ const result = await compileMdxSource({
310
+ content,
311
+ path,
312
+ frontmatter: ctx?.page?.frontmatter || null
313
+ })
314
+ return await runMdxSource({
315
+ code: result.code,
316
+ path,
317
+ ctx,
318
+ development: result.development
319
+ })
320
+ }
321
+
305
322
  export const compilePageMdx = async (page, pagesContext, options = {}) => {
306
323
  if (!page || page.content == null || page.mdxComponent) return
307
- const { ctx = null, lazyPagesTree = false, refreshPagesTree = true } = options || {}
324
+ const { ctx = null, lazyPagesTree = false, refreshPagesTree = true, compiled = null } = options || {}
308
325
  const activeCtx =
309
326
  ctx ||
310
327
  buildPageContext({
@@ -314,11 +331,18 @@ export const compilePageMdx = async (page, pagesContext, options = {}) => {
314
331
  pagesContext,
315
332
  lazyPagesTree
316
333
  })
317
- const mdxModule = await compileMdx({
318
- content: page.content,
319
- path: page.path,
320
- ctx: activeCtx
321
- })
334
+ const mdxModule = compiled?.code
335
+ ? await runMdxSource({
336
+ code: compiled.code,
337
+ path: page.path,
338
+ ctx: activeCtx,
339
+ development: compiled.development
340
+ })
341
+ : await compileMdx({
342
+ content: page.content,
343
+ path: page.path,
344
+ ctx: activeCtx
345
+ })
322
346
  page.mdxComponent = mdxModule.default
323
347
  page.toc = mdxModule.toc
324
348
  const shouldUseTocTitle = page.frontmatter?.title == null
@@ -346,41 +370,46 @@ export const renderHtml = async ({ routePath, path, components, pagesContext, pa
346
370
  pageMeta,
347
371
  pagesContext
348
372
  })
349
- await compilePageMdx(pageMeta, pagesContext, { ctx })
350
373
 
351
- const [Head, Outlet] = createPortal()
352
- const ExtraHead = () => {
353
- return [
354
- resolveRewindInject(),
355
- ...resolveUserHeadAssets(),
356
- ...resolvePageHeadAssets(pageMeta),
357
- Outlet(),
358
- RWND_FALLBACK
359
- ]
360
- }
374
+ await compilePageMdx(pageMeta, pagesContext, { ctx })
361
375
 
376
+ const template = state.USER_THEME.template
362
377
  const mdxComponent = pageMeta.mdxComponent
363
378
 
364
- const PageContent = ({ components: extraComponents, ...props }, ...children) =>
365
- mdxComponent({
366
- children,
367
- ...props,
368
- components: {
369
- ...components,
370
- ...extraComponents,
371
- head: Head,
372
- Head
373
- }
374
- })
379
+ const renderResult = await new Promise((resolve, reject) => {
380
+ const [Head, Outlet] = createPortal()
381
+ const ExtraHead = () => {
382
+ return [
383
+ resolveRewindInject(),
384
+ ...resolveUserHeadAssets(),
385
+ ...resolvePageHeadAssets(pageMeta),
386
+ Outlet(),
387
+ RWND_FALLBACK
388
+ ]
389
+ }
375
390
 
376
- const template = state.USER_THEME.template
391
+ const PageContent = ({ components: extraComponents, ...props }, ...children) => {
392
+ try {
393
+ return mdxComponent({
394
+ children,
395
+ ...props,
396
+ components: {
397
+ ...components,
398
+ ...extraComponents,
399
+ head: Head,
400
+ Head
401
+ }
402
+ })
403
+ } catch (e) {
404
+ reject(e)
405
+ }
406
+ }
377
407
 
378
- const renderResult = await new Promise((r) => {
379
408
  const result = HTMLRenderer.c(
380
409
  Suspense,
381
410
  {
382
411
  onLoad() {
383
- nextTick(() => r(result))
412
+ nextTick(() => resolve(result))
384
413
  }
385
414
  },
386
415
  () =>
package/src/pages.js CHANGED
@@ -22,9 +22,11 @@ import matter from 'gray-matter'
22
22
  import { readdir, readFile, stat } from 'fs/promises'
23
23
  import { existsSync } from 'fs'
24
24
  import { resolve, join, relative } from 'path'
25
+ import { cpus } from 'os'
26
+ import { Worker } from 'worker_threads'
25
27
  import { state, cli } from './state.js'
26
28
  import { withBase } from './config.js'
27
- import { compilePageMdx } from './mdx.js'
29
+ import { compileMdxSource, compilePageMdx } from './mdx.js'
28
30
  import { createStageLogger } from './stage-logger.js'
29
31
 
30
32
  const isPageFile = (name) => name.endsWith('.mdx') || name.endsWith('.md')
@@ -32,6 +34,137 @@ const isIgnoredEntry = (name) => name.startsWith('.') || name.startsWith('_')
32
34
 
33
35
  const pageMetadataCache = new Map()
34
36
  const pageDerivedCache = new Map()
37
+ const MDX_WORKER_URL = new URL('./workers/mdx-compile-worker.js', import.meta.url)
38
+ const cliOverrides = {
39
+ CLI_INTERMEDIATE_DIR: cli.CLI_INTERMEDIATE_DIR,
40
+ CLI_EMIT_INTERMEDIATE: cli.CLI_EMIT_INTERMEDIATE,
41
+ CLI_HOST: cli.CLI_HOST,
42
+ CLI_PORT: cli.CLI_PORT,
43
+ CLI_PAGES_DIR: cli.CLI_PAGES_DIR,
44
+ CLI_COMPONENTS_DIR: cli.CLI_COMPONENTS_DIR,
45
+ CLI_ASSETS_DIR: cli.CLI_ASSETS_DIR,
46
+ CLI_OUTPUT_DIR: cli.CLI_OUTPUT_DIR,
47
+ CLI_CONFIG_PATH: cli.CLI_CONFIG_PATH,
48
+ CLI_SITE_NAME: cli.CLI_SITE_NAME,
49
+ CLI_CODE_HIGHLIGHTING: cli.CLI_CODE_HIGHLIGHTING,
50
+ CLI_JOBS: cli.CLI_JOBS,
51
+ CLI_VERBOSE: cli.CLI_VERBOSE,
52
+ CLI_BASE: cli.CLI_BASE,
53
+ CLI_SEARCH: cli.CLI_SEARCH,
54
+ CLI_PWA: cli.CLI_PWA
55
+ }
56
+
57
+ const resolveWorkerCount = (total) => {
58
+ const cpuCount = Math.max(1, cpus()?.length || 1)
59
+ const requested = state.WORKER_JOBS
60
+ if (requested == null || requested <= 0) {
61
+ const items = Math.max(1, Number.isFinite(total) ? total : 1)
62
+ const autoCount = Math.round(Math.log(items))
63
+ return Math.max(1, Math.min(cpuCount, autoCount))
64
+ }
65
+ return Math.max(1, Math.min(cpuCount, Math.floor(requested)))
66
+ }
67
+
68
+ const compileMdxSources = async (pages, options = {}) => {
69
+ const targets = pages.filter((page) => page && page.content != null && !page.mdxComponent)
70
+ const results = new Map()
71
+ if (!targets.length) return results
72
+ const { onProgress } = options || {}
73
+ const reportProgress = (page) => {
74
+ if (typeof onProgress === 'function') {
75
+ onProgress(page)
76
+ }
77
+ }
78
+ const workerCount = Math.min(resolveWorkerCount(targets.length), targets.length)
79
+ if (workerCount <= 1) {
80
+ for (const page of targets) {
81
+ const result = await compileMdxSource({
82
+ content: page.content,
83
+ path: page.path,
84
+ frontmatter: page.frontmatter
85
+ })
86
+ results.set(page, result)
87
+ reportProgress(page)
88
+ }
89
+ return results
90
+ }
91
+
92
+ return await new Promise((resolve, reject) => {
93
+ const workers = []
94
+ const pending = new Map()
95
+ let cursor = 0
96
+ let nextId = 0
97
+ let finished = false
98
+
99
+ const finalize = async (error) => {
100
+ if (finished) return
101
+ finished = true
102
+ await Promise.all(workers.map((worker) => worker.terminate().catch(() => null)))
103
+ if (error) {
104
+ reject(error)
105
+ return
106
+ }
107
+ resolve(results)
108
+ }
109
+
110
+ const assign = (worker) => {
111
+ if (cursor >= targets.length) return false
112
+ const page = targets[cursor++]
113
+ const id = nextId++
114
+ pending.set(id, page)
115
+ worker.postMessage({
116
+ id,
117
+ path: page.path,
118
+ content: page.content,
119
+ frontmatter: page.frontmatter
120
+ })
121
+ return true
122
+ }
123
+
124
+ const handleMessage = (worker, message) => {
125
+ if (finished) return
126
+ const { id, result, error } = message || {}
127
+ const page = pending.get(id)
128
+ pending.delete(id)
129
+ if (!page) return
130
+ if (error) {
131
+ void finalize(new Error(error))
132
+ return
133
+ }
134
+ results.set(page, result)
135
+ reportProgress(page)
136
+ assign(worker)
137
+ if (results.size === targets.length && pending.size === 0) {
138
+ void finalize()
139
+ }
140
+ }
141
+
142
+ const handleError = (error) => {
143
+ if (finished) return
144
+ void finalize(error instanceof Error ? error : new Error(String(error)))
145
+ }
146
+
147
+ for (let i = 0; i < workerCount; i += 1) {
148
+ const worker = new Worker(MDX_WORKER_URL, {
149
+ type: 'module',
150
+ workerData: {
151
+ mode: state.CURRENT_MODE,
152
+ configPath: cli.CLI_CONFIG_PATH,
153
+ cli: cliOverrides
154
+ }
155
+ })
156
+ workers.push(worker)
157
+ worker.on('message', (message) => handleMessage(worker, message))
158
+ worker.on('error', handleError)
159
+ worker.on('exit', (code) => {
160
+ if (code !== 0) {
161
+ handleError(new Error(`MDX worker exited with code ${code}`))
162
+ }
163
+ })
164
+ assign(worker)
165
+ }
166
+ })
167
+ }
35
168
 
36
169
  const collectLanguagesFromPages = (pages = []) => {
37
170
  const languages = new Map()
@@ -616,47 +749,12 @@ const buildNavSequence = (nodes, pagesByRoute) => {
616
749
  return result
617
750
  }
618
751
 
619
- export const buildPagesContext = async ({ compileAll = true } = {}) => {
620
- const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build'
621
- const stageLogger = createStageLogger(logEnabled)
622
- const collectToken = stageLogger.start('Collecting pages')
623
- const collected = await collectPages()
624
- stageLogger.end(collectToken)
625
- let pages = collected.pages
626
- const excludedRoutes = collected.excludedRoutes
627
- const excludedDirs = collected.excludedDirs
628
- const hasIndex = pages.some((page) => page.routePath === '/')
629
- if (!hasIndex) {
630
- const content = buildIndexFallback(pages, state.SITE_NAME)
631
- pages.unshift({
632
- routePath: '/',
633
- routeHref: withBase('/'),
634
- path: resolve(state.PAGES_DIR, 'index.md'),
635
- relativePath: 'index.md',
636
- name: 'index',
637
- dir: '',
638
- segments: [],
639
- depth: 0,
640
- isIndex: true,
641
- title: state.SITE_NAME || 'Methanol Site',
642
- weight: null,
643
- date: null,
644
- isRoot: false,
645
- hidden: false,
646
- content,
647
- frontmatter: {},
648
- matter: null,
649
- stats: { size: content.length, createdAt: null, updatedAt: null },
650
- createdAt: null,
651
- updatedAt: null
652
- })
653
- if (excludedRoutes?.has('/')) {
654
- excludedRoutes.delete('/')
655
- }
656
- }
657
-
752
+ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDirs } = {}) => {
753
+ const pageList = Array.isArray(pages) ? pages : []
754
+ const routeExcludes = excludedRoutes || new Set()
755
+ const dirExcludes = excludedDirs || new Set()
658
756
  const pagesByRoute = new Map()
659
- for (const page of pages) {
757
+ for (const page of pageList) {
660
758
  if (!pagesByRoute.has(page.routePath)) {
661
759
  pagesByRoute.set(page.routePath, page)
662
760
  }
@@ -703,7 +801,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
703
801
  }
704
802
  let pagesTree = getPagesTree('/')
705
803
  const notFound = pagesByRoute.get('/404') || null
706
- const languages = collectLanguagesFromPages(pages)
804
+ const languages = collectLanguagesFromPages(pageList)
707
805
  const userSite = state.USER_SITE || {}
708
806
  const siteBase = state.VITE_BASE ?? userSite.base ?? null
709
807
  const site = {
@@ -723,9 +821,9 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
723
821
  },
724
822
  generatedAt: new Date().toISOString()
725
823
  }
726
- const excludedDirPaths = new Set(Array.from(excludedDirs).map((dir) => `/${dir}`))
824
+ const excludedDirPaths = new Set(Array.from(dirExcludes).map((dir) => `/${dir}`))
727
825
  const pagesContext = {
728
- pages,
826
+ pages: pageList,
729
827
  pagesByRoute,
730
828
  getPageByRoute,
731
829
  pagesTree,
@@ -775,34 +873,96 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
775
873
  pagesContext.getLanguageForRoute = (routePath) =>
776
874
  resolveLanguageForRoute(pagesContext.languages, routePath)
777
875
  },
778
- excludedRoutes,
779
- excludedDirs,
876
+ excludedRoutes: routeExcludes,
877
+ excludedDirs: dirExcludes,
780
878
  excludedDirPaths,
781
879
  notFound,
782
880
  languages,
783
881
  getLanguageForRoute: (routePath) => resolveLanguageForRoute(languages, routePath),
784
882
  site
785
883
  }
884
+ return pagesContext
885
+ }
886
+
887
+ export const buildPagesContext = async ({ compileAll = true } = {}) => {
888
+ const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build'
889
+ const stageLogger = createStageLogger(logEnabled)
890
+ const collectToken = stageLogger.start('Collecting pages')
891
+ const collected = await collectPages()
892
+ stageLogger.end(collectToken)
893
+ let pages = collected.pages
894
+ const excludedRoutes = collected.excludedRoutes
895
+ const excludedDirs = collected.excludedDirs
896
+ const hasIndex = pages.some((page) => page.routePath === '/')
897
+ if (!hasIndex) {
898
+ const content = buildIndexFallback(pages, state.SITE_NAME)
899
+ pages.unshift({
900
+ routePath: '/',
901
+ routeHref: withBase('/'),
902
+ path: resolve(state.PAGES_DIR, 'index.md'),
903
+ relativePath: 'index.md',
904
+ name: 'index',
905
+ dir: '',
906
+ segments: [],
907
+ depth: 0,
908
+ isIndex: true,
909
+ title: state.SITE_NAME || 'Methanol Site',
910
+ weight: null,
911
+ date: null,
912
+ isRoot: false,
913
+ hidden: false,
914
+ content,
915
+ frontmatter: {},
916
+ matter: null,
917
+ stats: { size: content.length, createdAt: null, updatedAt: null },
918
+ createdAt: null,
919
+ updatedAt: null
920
+ })
921
+ if (excludedRoutes?.has('/')) {
922
+ excludedRoutes.delete('/')
923
+ }
924
+ }
925
+
926
+ const pagesContext = createPagesContextFromPages({
927
+ pages,
928
+ excludedRoutes,
929
+ excludedDirs
930
+ })
786
931
  if (compileAll) {
787
932
  const compileToken = stageLogger.start('Compiling MDX')
788
- const totalPages = pages.length
789
- for (let i = 0; i < pages.length; i++) {
790
- const page = pages[i]
791
- if (logEnabled) {
933
+ const compileTargets = pages.filter((page) => page && page.content != null && !page.mdxComponent)
934
+ const totalPages = compileTargets.length
935
+ let completed = 0
936
+ const compiledSources = await compileMdxSources(compileTargets, {
937
+ onProgress: (page) => {
938
+ if (!logEnabled) return
939
+ completed += 1
792
940
  stageLogger.update(
793
941
  compileToken,
794
- `Compiling MDX [${i + 1}/${totalPages}] ${page.routePath || page.path}`
942
+ `Compiling MDX [${completed}/${totalPages}] ${page.routePath || page.path}`
795
943
  )
796
944
  }
945
+ })
946
+ stageLogger.end(compileToken)
947
+ const executeToken = stageLogger.start('Running MDX')
948
+ completed = 0
949
+ for (const page of compileTargets) {
950
+ const compiled = compiledSources.get(page) || null
797
951
  await compilePageMdx(page, pagesContext, {
798
952
  lazyPagesTree: true,
799
- refreshPagesTree: false
953
+ refreshPagesTree: false,
954
+ compiled
800
955
  })
956
+ if (logEnabled) {
957
+ completed += 1
958
+ stageLogger.update(
959
+ executeToken,
960
+ `Running MDX [${completed}/${totalPages}] ${page.routePath || page.path}`
961
+ )
962
+ }
801
963
  }
802
- stageLogger.end(compileToken)
803
- pagesTreeCache.clear()
804
- pagesTree = getPagesTree('/')
805
- pagesContext.pagesTree = pagesTree
964
+ stageLogger.end(executeToken)
965
+ pagesContext.refreshPagesTree?.()
806
966
  }
807
967
  return pagesContext
808
968
  }
package/src/state.js CHANGED
@@ -104,14 +104,14 @@ const withCommonOptions = (y) =>
104
104
  .option('highlight', {
105
105
  describe: 'Enable or disable code highlighting',
106
106
  type: 'boolean',
107
- coerce: (value) => {
108
- if (value == null) return null
109
- if (typeof value === 'boolean') return value
110
- const normalized = String(value).trim().toLowerCase()
111
- if (normalized === 'true') return true
112
- if (normalized === 'false') return false
113
- return null
114
- }
107
+ default: undefined
108
+ })
109
+ .option('jobs', {
110
+ alias: 'j',
111
+ describe: 'Worker thread count (0 for auto)',
112
+ type: 'number',
113
+ requiresArg: true,
114
+ nargs: 1
115
115
  })
116
116
  .option('verbose', {
117
117
  alias: 'v',
@@ -126,12 +126,14 @@ const withCommonOptions = (y) =>
126
126
  nargs: 1
127
127
  })
128
128
  .option('search', {
129
- describe: 'Enable search indexing (pagefind)',
130
- type: 'boolean'
129
+ describe: 'Enable or disable search indexing (pagefind)',
130
+ type: 'boolean',
131
+ default: undefined
131
132
  })
132
133
  .option('pwa', {
133
- describe: 'Enable PWA support',
134
- type: 'boolean'
134
+ describe: 'Enable or disable PWA support',
135
+ type: 'boolean',
136
+ default: undefined
135
137
  })
136
138
 
137
139
  const parser = yargs(hideBin(process.argv))
@@ -161,10 +163,11 @@ export const cli = {
161
163
  CLI_CONFIG_PATH: argv.config || null,
162
164
  CLI_SITE_NAME: argv['site-name'] || null,
163
165
  CLI_CODE_HIGHLIGHTING: typeof argv.highlight === 'boolean' ? argv.highlight : null,
166
+ CLI_JOBS: typeof argv.jobs === 'number' && Number.isFinite(argv.jobs) ? argv.jobs : null,
164
167
  CLI_VERBOSE: Boolean(argv.verbose),
165
168
  CLI_BASE: argv.base || null,
166
- CLI_SEARCH: argv.search,
167
- CLI_PWA: argv.pwa
169
+ CLI_SEARCH: typeof argv.search === 'boolean' ? argv.search : undefined,
170
+ CLI_PWA: typeof argv.pwa === 'boolean' ? argv.pwa : undefined
168
171
  }
169
172
 
170
173
  export const state = {
@@ -204,6 +207,7 @@ export const state = {
204
207
  THEME_POST_BUNDLE_HOOKS: [],
205
208
  STARRY_NIGHT_ENABLED: false,
206
209
  STARRY_NIGHT_OPTIONS: null,
210
+ WORKER_JOBS: 0,
207
211
  GFM_ENABLED: true,
208
212
  PWA_ENABLED: false,
209
213
  PWA_OPTIONS: null,