methanol 0.0.9 → 0.0.11

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/pages.js CHANGED
@@ -28,7 +28,6 @@ import { compilePageMdx } from './mdx.js'
28
28
  import { createStageLogger } from './stage-logger.js'
29
29
 
30
30
  const isPageFile = (name) => name.endsWith('.mdx') || name.endsWith('.md')
31
- const isInternalPage = (name) => name.startsWith('_') || name.startsWith('.')
32
31
  const isIgnoredEntry = (name) => name.startsWith('.') || name.startsWith('_')
33
32
 
34
33
  const pageMetadataCache = new Map()
@@ -107,17 +106,17 @@ const resolveLanguageForRoute = (languages = [], routePath = '/') => {
107
106
  return best || rootLanguage
108
107
  }
109
108
 
110
- export const routePathFromFile = (filePath, pagesDir = state.PAGES_DIR) => {
111
- if (!filePath.endsWith('.mdx') && !filePath.endsWith('.md')) {
109
+ export const routePathFromFile = (path, pagesDir = state.PAGES_DIR) => {
110
+ if (!path.endsWith('.mdx') && !path.endsWith('.md')) {
112
111
  return null
113
112
  }
114
- const relPath = relative(pagesDir, filePath)
113
+ const relPath = relative(pagesDir, path)
115
114
  if (relPath.startsWith('..')) {
116
115
  return null
117
116
  }
118
117
  const name = relPath.replace(/\.(mdx|md)$/, '')
119
118
  const baseName = name.split(/[\\/]/).pop()
120
- if (isInternalPage(baseName)) {
119
+ if (isIgnoredEntry(baseName)) {
121
120
  return null
122
121
  }
123
122
  const normalized = name.replace(/\\/g, '/')
@@ -147,8 +146,8 @@ const parseFrontmatter = (raw) => {
147
146
  }
148
147
  }
149
148
 
150
- const parsePageMetadata = async (filePath) => {
151
- const raw = await readFile(filePath, 'utf-8')
149
+ const parsePageMetadata = async (path) => {
150
+ const raw = await readFile(path, 'utf-8')
152
151
  const { data: frontmatter, content, matter } = parseFrontmatter(raw)
153
152
  let title = frontmatter.title
154
153
  return {
@@ -198,13 +197,21 @@ const buildPagesTree = (pages, options = {}) => {
198
197
  }
199
198
  return routePath
200
199
  }
200
+ const buildDirRoutePath = (dir) => {
201
+ const localPath = dir ? `/${dir}/` : '/'
202
+ if (!rootPrefix) return normalizeRoutePath(localPath)
203
+ return normalizeRoutePath(`${rootPrefix}${localPath}`)
204
+ }
205
+ const resolveDirFsPath = (dir) => {
206
+ if (!rootDir) return resolve(state.PAGES_DIR, dir)
207
+ return resolve(state.PAGES_DIR, join(rootDir, dir))
208
+ }
201
209
  const currentRouteWithinRoot = resolveRouteWithinRoot(currentRoutePath)
202
210
  const isUnderRoot = (page) => {
203
211
  if (!rootDir) return true
204
212
  return page.routePath === rootPath || (rootPrefix && page.routePath.startsWith(`${rootPrefix}/`))
205
213
  }
206
214
  const treePages = pages
207
- .filter((page) => !page.isInternal)
208
215
  .filter((page) => isUnderRoot(page))
209
216
  .map((page) => {
210
217
  if (!rootDir) return page
@@ -267,10 +274,10 @@ const buildPagesTree = (pages, options = {}) => {
267
274
  const dir = {
268
275
  type: 'directory',
269
276
  name,
270
- path: `/${path}`,
277
+ path: resolveDirFsPath(path),
271
278
  children: [],
272
279
  depth,
273
- routePath: null,
280
+ routePath: buildDirRoutePath(path),
274
281
  routeHref: null,
275
282
  title: null,
276
283
  weight: null,
@@ -291,9 +298,9 @@ const buildPagesTree = (pages, options = {}) => {
291
298
  }
292
299
  for (const page of treePages) {
293
300
  if (page.hidden && !(includeHiddenRoot && page.routePath === rootPath)) {
294
- const isHidden404 = page.routePath === '/404'
301
+ const isHiddenSpecial = page.routePath === '/404' || page.routePath === '/offline'
295
302
  const shouldExposeHidden =
296
- !isHidden404 &&
303
+ !isHiddenSpecial &&
297
304
  page.hiddenByFrontmatter === true &&
298
305
  (
299
306
  page.routePath === currentRoutePath ||
@@ -404,7 +411,7 @@ const walkPages = async function* (dir, basePath = '') {
404
411
  const relativePath = join(basePath, name).replace(/\\/g, '/')
405
412
  const isIndex = name === 'index'
406
413
  const routePath = isIndex ? (basePath ? `/${basePath}/` : '/') : `/${relativePath}`
407
- yield { routePath, filePath: fullPath, isIndex }
414
+ yield { routePath, path: fullPath, isIndex }
408
415
  }
409
416
 
410
417
  for (const { entry, fullPath } of dirs) {
@@ -412,39 +419,45 @@ const walkPages = async function* (dir, basePath = '') {
412
419
  }
413
420
  }
414
421
 
415
- export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
416
- const routePath = routePathFromFile(filePath, pagesDir)
422
+ export const buildPageEntry = async ({ path, pagesDir, source }) => {
423
+ const routePath = routePathFromFile(path, pagesDir)
417
424
  if (!routePath) return null
418
- const relPath = relative(pagesDir, filePath).replace(/\\/g, '/')
425
+ const relPath = relative(pagesDir, path).replace(/\\/g, '/')
419
426
  const name = relPath.replace(/\.(mdx|md)$/, '')
420
427
  const baseName = name.split('/').pop()
421
428
  const dir = name.split('/').slice(0, -1).join('/')
422
429
  const dirName = dir ? dir.split('/').pop() : ''
423
430
  const isIndex = baseName === 'index'
424
431
  const segments = routePath.split('/').filter(Boolean)
425
- const stats = await stat(filePath)
426
- const cached = pageMetadataCache.get(filePath)
432
+ const stats = await stat(path)
433
+ const cached = pageMetadataCache.get(path)
427
434
  let metadata = null
428
435
  if (cached && cached.mtimeMs === stats.mtimeMs) {
429
436
  metadata = cached.metadata
430
437
  } else {
431
- metadata = await parsePageMetadata(filePath)
432
- pageMetadataCache.set(filePath, { mtimeMs: stats.mtimeMs, metadata })
438
+ metadata = await parsePageMetadata(path)
439
+ pageMetadataCache.set(path, { mtimeMs: stats.mtimeMs, metadata })
433
440
  }
434
- const derived = pageDerivedCache.get(filePath)
441
+ const derived = pageDerivedCache.get(path)
435
442
  const exclude = Boolean(metadata.frontmatter?.exclude)
436
443
  const frontmatterHidden = metadata.frontmatter?.hidden
437
444
  const hiddenByFrontmatter = frontmatterHidden === true
438
445
  const isNotFoundPage = routePath === '/404'
439
- const hidden = frontmatterHidden === false
440
- ? false
441
- : frontmatterHidden === true
442
- ? true
443
- : isNotFoundPage || Boolean(metadata.frontmatter?.isRoot)
446
+ const isOfflinePage = routePath === '/offline'
447
+ const isSpecialPage = isNotFoundPage || isOfflinePage
448
+ const isSiteRoot = routePath === '/'
449
+ const frontmatterIsRoot = Boolean(metadata.frontmatter?.isRoot)
450
+ const hidden = isSpecialPage
451
+ ? true
452
+ : frontmatterHidden === false
453
+ ? false
454
+ : frontmatterHidden === true
455
+ ? true
456
+ : frontmatterIsRoot
444
457
  return {
445
458
  routePath,
446
459
  routeHref: withBase(routePath),
447
- filePath,
460
+ path,
448
461
  source,
449
462
  relativePath: relPath,
450
463
  name: baseName,
@@ -452,11 +465,10 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
452
465
  segments,
453
466
  depth: segments.length,
454
467
  isIndex,
455
- isInternal: isInternalPage(baseName),
456
468
  title: metadata.title || derived?.title || (baseName === 'index' ? (dirName || 'Home') : baseName),
457
469
  weight: parseWeight(metadata.frontmatter?.weight),
458
470
  date: parseDate(metadata.frontmatter?.date) || parseDate(stats.mtime),
459
- isRoot: Boolean(metadata.frontmatter?.isRoot),
471
+ isRoot: isSiteRoot || frontmatterIsRoot,
460
472
  hidden,
461
473
  hiddenByFrontmatter,
462
474
  exclude,
@@ -468,9 +480,7 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
468
480
  size: stats.size,
469
481
  createdAt: stats.birthtime?.toISOString?.() || null,
470
482
  updatedAt: stats.mtime?.toISOString?.() || null
471
- },
472
- createdAt: stats.birthtime?.toISOString?.() || null,
473
- updatedAt: stats.mtime?.toISOString?.() || null
483
+ }
474
484
  }
475
485
  }
476
486
 
@@ -481,7 +491,7 @@ const collectPagesFromDir = async (pagesDir, source) => {
481
491
  const pages = []
482
492
  for await (const page of walkPages(pagesDir)) {
483
493
  const entry = await buildPageEntry({
484
- filePath: page.filePath,
494
+ path: page.path,
485
495
  pagesDir,
486
496
  source
487
497
  })
@@ -523,7 +533,12 @@ const collectPages = async () => {
523
533
 
524
534
  const buildIndexFallback = (pages, siteName) => {
525
535
  const visiblePages = pages
526
- .filter((page) => !page.isInternal && page.routePath !== '/')
536
+ .filter(
537
+ (page) =>
538
+ page.routePath !== '/' &&
539
+ page.routePath !== '/404' &&
540
+ page.routePath !== '/offline'
541
+ )
527
542
  .sort((a, b) => a.routePath.localeCompare(b.routePath))
528
543
 
529
544
  const lines = [
@@ -574,7 +589,7 @@ const buildNavSequence = (nodes, pagesByRoute) => {
574
589
  const seen = new Set()
575
590
  const addEntry = (entry) => {
576
591
  if (!entry.routePath) return
577
- const key = entry.filePath || entry.routePath
592
+ const key = entry.path || entry.routePath
578
593
  if (seen.has(key)) return
579
594
  seen.add(key)
580
595
  result.push(entry)
@@ -616,14 +631,13 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
616
631
  pages.unshift({
617
632
  routePath: '/',
618
633
  routeHref: withBase('/'),
619
- filePath: resolve(state.PAGES_DIR, 'index.md'),
634
+ path: resolve(state.PAGES_DIR, 'index.md'),
620
635
  relativePath: 'index.md',
621
636
  name: 'index',
622
637
  dir: '',
623
638
  segments: [],
624
639
  depth: 0,
625
640
  isIndex: true,
626
- isInternal: false,
627
641
  title: state.SITE_NAME || 'Methanol Site',
628
642
  weight: null,
629
643
  date: null,
@@ -648,10 +662,10 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
648
662
  }
649
663
  }
650
664
  const getPageByRoute = (routePath, options = {}) => {
651
- const { filePath } = options || {}
652
- if (filePath) {
665
+ const { path } = options || {}
666
+ if (path) {
653
667
  for (const page of pages) {
654
- if (page.routePath === routePath && page.filePath === filePath) {
668
+ if (page.routePath === routePath && page.path === path) {
655
669
  return page
656
670
  }
657
671
  }
@@ -717,26 +731,26 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
717
731
  pagesTree,
718
732
  getPagesTree,
719
733
  derivedTitleCache: pageDerivedCache,
720
- setDerivedTitle: (filePath, title, toc) => {
721
- if (!filePath) return
722
- pageDerivedCache.set(filePath, { title, toc })
734
+ setDerivedTitle: (path, title, toc) => {
735
+ if (!path) return
736
+ pageDerivedCache.set(path, { title, toc })
723
737
  },
724
- clearDerivedTitle: (filePath) => {
725
- if (!filePath) return
726
- pageDerivedCache.delete(filePath)
738
+ clearDerivedTitle: (path) => {
739
+ if (!path) return
740
+ pageDerivedCache.delete(path)
727
741
  },
728
742
  refreshPagesTree: () => {
729
743
  pagesTreeCache.clear()
730
744
  navSequenceCache.clear()
731
745
  pagesContext.pagesTree = getPagesTree('/')
732
746
  },
733
- getSiblings: (routePath, filePath = null) => {
747
+ getSiblings: (routePath, path = null) => {
734
748
  if (!routePath) return { prev: null, next: null }
735
749
  const sequence = getNavSequence(routePath)
736
750
  if (!sequence.length) return { prev: null, next: null }
737
751
  let index = -1
738
- if (filePath) {
739
- index = sequence.findIndex((entry) => entry.filePath === filePath)
752
+ if (path) {
753
+ index = sequence.findIndex((entry) => entry.path === path)
740
754
  }
741
755
  if (index < 0) {
742
756
  index = sequence.findIndex((entry) => entry.routePath === routePath)
@@ -748,7 +762,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
748
762
  routePath: entry.routePath,
749
763
  routeHref: entry.routeHref,
750
764
  title: entry.title || entry.name || entry.routePath,
751
- filePath: entry.filePath || null
765
+ path: entry.path || null
752
766
  }
753
767
  }
754
768
  return {
@@ -775,7 +789,10 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
775
789
  for (let i = 0; i < pages.length; i++) {
776
790
  const page = pages[i]
777
791
  if (logEnabled) {
778
- stageLogger.update(compileToken, `Compiling MDX [${i + 1}/${totalPages}] ${page.filePath}`)
792
+ stageLogger.update(
793
+ compileToken,
794
+ `Compiling MDX [${i + 1}/${totalPages}] ${page.routePath || page.path}`
795
+ )
779
796
  }
780
797
  await compilePageMdx(page, pagesContext, {
781
798
  lazyPagesTree: true,
@@ -110,7 +110,7 @@ export const preparePublicAssets = async ({ themeDir, userDir, targetDir }) => {
110
110
  }
111
111
  }
112
112
 
113
- export const updateAsset = async ({ type, filePath, themeDir, userDir, targetDir, relPath }) => {
113
+ export const updateAsset = async ({ type, path, themeDir, userDir, targetDir, relPath }) => {
114
114
  const targetPath = resolve(targetDir, relPath)
115
115
 
116
116
  if (type === 'unlink') {
@@ -58,7 +58,7 @@ export function env(parentEnv) {
58
58
  const component = ({ children: childrenProp, ...props }, ...children) => {
59
59
  const id = renderCount++
60
60
  const idStr = id.toString(16)
61
- const script = `$$rwnd(${JSON.stringify(key)},${id},${Object.keys(props).length ? JSON5.stringify(props) : '{}'})`
61
+ const script = `$$rfrm(${JSON.stringify(key)},${id},${Object.keys(props).length ? JSON5.stringify(props) : '{}'})`
62
62
 
63
63
  return (R) => {
64
64
  return [
@@ -76,8 +76,8 @@ const resolvePagesRoot = (filePath) => {
76
76
 
77
77
  export const linkResolve = () => {
78
78
  return (tree, file) => {
79
- const baseDir = file?.path ? dirname(file.path) : file?.cwd || process.cwd()
80
- const pagesRoot = resolvePagesRoot(file?.path || null)
79
+ const baseDir = file.path ? dirname(file.path) : file.cwd || process.cwd()
80
+ const pagesRoot = resolvePagesRoot(file.path || null)
81
81
  visit(tree, (node) => {
82
82
  if (!isElement(node) || node.tagName !== 'a') {
83
83
  return
@@ -21,6 +21,7 @@
21
21
  import { style } from './logger.js'
22
22
 
23
23
  const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now())
24
+ const log = console.log.bind(console)
24
25
 
25
26
  export const createStageLogger = (enabled) => {
26
27
  let lastLength = 0
@@ -28,13 +29,13 @@ export const createStageLogger = (enabled) => {
28
29
  const writeLine = (text, newline) => {
29
30
  if (!process.stdout || !process.stdout.write) {
30
31
  if (newline) {
31
- console.log(text)
32
+ log(text)
32
33
  }
33
34
  return
34
35
  }
35
36
  if (!isTty) {
36
37
  if (newline) {
37
- console.log(text)
38
+ log(text)
38
39
  }
39
40
  return
40
41
  }
package/src/state.js CHANGED
@@ -101,7 +101,7 @@ const withCommonOptions = (y) =>
101
101
  type: 'boolean',
102
102
  default: false
103
103
  })
104
- .option('code-highlighting', {
104
+ .option('highlight', {
105
105
  describe: 'Enable or disable code highlighting',
106
106
  type: 'boolean',
107
107
  coerce: (value) => {
@@ -125,6 +125,14 @@ const withCommonOptions = (y) =>
125
125
  requiresArg: true,
126
126
  nargs: 1
127
127
  })
128
+ .option('search', {
129
+ describe: 'Enable search indexing (pagefind)',
130
+ type: 'boolean'
131
+ })
132
+ .option('pwa', {
133
+ describe: 'Enable PWA support',
134
+ type: 'boolean'
135
+ })
128
136
 
129
137
  const parser = yargs(hideBin(process.argv))
130
138
  .scriptName('methanol')
@@ -152,9 +160,11 @@ export const cli = {
152
160
  CLI_OUTPUT_DIR: argv.output || null,
153
161
  CLI_CONFIG_PATH: argv.config || null,
154
162
  CLI_SITE_NAME: argv['site-name'] || null,
155
- CLI_CODE_HIGHLIGHTING: typeof argv['code-highlighting'] === 'boolean' ? argv['code-highlighting'] : null,
163
+ CLI_CODE_HIGHLIGHTING: typeof argv.highlight === 'boolean' ? argv.highlight : null,
156
164
  CLI_VERBOSE: Boolean(argv.verbose),
157
- CLI_BASE: argv.base || null
165
+ CLI_BASE: argv.base || null,
166
+ CLI_SEARCH: argv.search,
167
+ CLI_PWA: argv.pwa
158
168
  }
159
169
 
160
170
  export const state = {
@@ -183,6 +193,7 @@ export const state = {
183
193
  PAGEFIND_ENABLED: false,
184
194
  PAGEFIND_OPTIONS: null,
185
195
  PAGEFIND_BUILD: null,
196
+ PWA_ENABLED: false,
186
197
  USER_PRE_BUILD_HOOKS: [],
187
198
  USER_POST_BUILD_HOOKS: [],
188
199
  USER_PRE_BUNDLE_HOOKS: [],
@@ -194,6 +205,8 @@ export const state = {
194
205
  STARRY_NIGHT_ENABLED: false,
195
206
  STARRY_NIGHT_OPTIONS: null,
196
207
  GFM_ENABLED: true,
208
+ PWA_ENABLED: false,
209
+ PWA_OPTIONS: null,
197
210
  CURRENT_MODE: 'production',
198
211
  RESOLVED_MDX_CONFIG: undefined,
199
212
  RESOLVED_VITE_CONFIG: undefined
package/src/utils.js CHANGED
@@ -1,9 +1,31 @@
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 NullProtoObj from 'null-prototype-object'
22
+
1
23
  export const cached = (fn) => {
2
24
  let cache = null
3
25
  return () => (cache ?? (cache = fn()))
4
26
  }
5
27
 
6
28
  export const cachedStr = (fn) => {
7
- const cache = Object.create(null)
29
+ const cache = new NullProtoObj()
8
30
  return (key) => (cache[key] ?? (cache[key] = fn(key)))
9
31
  }
@@ -26,7 +26,7 @@ import { normalizePath } from 'vite'
26
26
  import { state } from './state.js'
27
27
  import { resolveBasePrefix } from './config.js'
28
28
  import { genRegistryScript } from './components.js'
29
- import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT } from './assets.js'
29
+ import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT, PWA_INJECT_SCRIPT } from './client/virtual-module/assets.js'
30
30
  import { projectRequire } from './node-loader.js'
31
31
 
32
32
  const require = createRequire(import.meta.url)
@@ -129,9 +129,22 @@ const virtualModuleMap = {
129
129
  get 'registry.js'() {
130
130
  return `export const registry = ${genRegistryScript()}`
131
131
  },
132
- loader: LOADER_SCRIPT,
133
- 'inject.js': INJECT_SCRIPT,
134
- 'pagefind-loader': PAGEFIND_LOADER_SCRIPT
132
+ get loader() {
133
+ return LOADER_SCRIPT()
134
+ },
135
+ get 'inject.js'() {
136
+ return INJECT_SCRIPT()
137
+ },
138
+ get 'pagefind-loader'() {
139
+ return PAGEFIND_LOADER_SCRIPT()
140
+ },
141
+ get 'pwa-inject'() {
142
+ if (state.PWA_ENABLED) {
143
+ return PWA_INJECT_SCRIPT()
144
+ }
145
+
146
+ return ''
147
+ }
135
148
  }
136
149
 
137
150
  const getModuleIdSegment = (id, start) => {