methanol 0.0.9 → 0.0.10

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,17 @@ 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
+ }
201
205
  const currentRouteWithinRoot = resolveRouteWithinRoot(currentRoutePath)
202
206
  const isUnderRoot = (page) => {
203
207
  if (!rootDir) return true
204
208
  return page.routePath === rootPath || (rootPrefix && page.routePath.startsWith(`${rootPrefix}/`))
205
209
  }
206
210
  const treePages = pages
207
- .filter((page) => !page.isInternal)
208
211
  .filter((page) => isUnderRoot(page))
209
212
  .map((page) => {
210
213
  if (!rootDir) return page
@@ -267,10 +270,10 @@ const buildPagesTree = (pages, options = {}) => {
267
270
  const dir = {
268
271
  type: 'directory',
269
272
  name,
270
- path: `/${path}`,
273
+ path: resolve(state.PAGES_DIR, path),
271
274
  children: [],
272
275
  depth,
273
- routePath: null,
276
+ routePath: buildDirRoutePath(path),
274
277
  routeHref: null,
275
278
  title: null,
276
279
  weight: null,
@@ -291,9 +294,9 @@ const buildPagesTree = (pages, options = {}) => {
291
294
  }
292
295
  for (const page of treePages) {
293
296
  if (page.hidden && !(includeHiddenRoot && page.routePath === rootPath)) {
294
- const isHidden404 = page.routePath === '/404'
297
+ const isHiddenSpecial = page.routePath === '/404' || page.routePath === '/offline'
295
298
  const shouldExposeHidden =
296
- !isHidden404 &&
299
+ !isHiddenSpecial &&
297
300
  page.hiddenByFrontmatter === true &&
298
301
  (
299
302
  page.routePath === currentRoutePath ||
@@ -404,7 +407,7 @@ const walkPages = async function* (dir, basePath = '') {
404
407
  const relativePath = join(basePath, name).replace(/\\/g, '/')
405
408
  const isIndex = name === 'index'
406
409
  const routePath = isIndex ? (basePath ? `/${basePath}/` : '/') : `/${relativePath}`
407
- yield { routePath, filePath: fullPath, isIndex }
410
+ yield { routePath, path: fullPath, isIndex }
408
411
  }
409
412
 
410
413
  for (const { entry, fullPath } of dirs) {
@@ -412,39 +415,45 @@ const walkPages = async function* (dir, basePath = '') {
412
415
  }
413
416
  }
414
417
 
415
- export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
416
- const routePath = routePathFromFile(filePath, pagesDir)
418
+ export const buildPageEntry = async ({ path, pagesDir, source }) => {
419
+ const routePath = routePathFromFile(path, pagesDir)
417
420
  if (!routePath) return null
418
- const relPath = relative(pagesDir, filePath).replace(/\\/g, '/')
421
+ const relPath = relative(pagesDir, path).replace(/\\/g, '/')
419
422
  const name = relPath.replace(/\.(mdx|md)$/, '')
420
423
  const baseName = name.split('/').pop()
421
424
  const dir = name.split('/').slice(0, -1).join('/')
422
425
  const dirName = dir ? dir.split('/').pop() : ''
423
426
  const isIndex = baseName === 'index'
424
427
  const segments = routePath.split('/').filter(Boolean)
425
- const stats = await stat(filePath)
426
- const cached = pageMetadataCache.get(filePath)
428
+ const stats = await stat(path)
429
+ const cached = pageMetadataCache.get(path)
427
430
  let metadata = null
428
431
  if (cached && cached.mtimeMs === stats.mtimeMs) {
429
432
  metadata = cached.metadata
430
433
  } else {
431
- metadata = await parsePageMetadata(filePath)
432
- pageMetadataCache.set(filePath, { mtimeMs: stats.mtimeMs, metadata })
434
+ metadata = await parsePageMetadata(path)
435
+ pageMetadataCache.set(path, { mtimeMs: stats.mtimeMs, metadata })
433
436
  }
434
- const derived = pageDerivedCache.get(filePath)
437
+ const derived = pageDerivedCache.get(path)
435
438
  const exclude = Boolean(metadata.frontmatter?.exclude)
436
439
  const frontmatterHidden = metadata.frontmatter?.hidden
437
440
  const hiddenByFrontmatter = frontmatterHidden === true
438
441
  const isNotFoundPage = routePath === '/404'
439
- const hidden = frontmatterHidden === false
440
- ? false
441
- : frontmatterHidden === true
442
- ? true
443
- : isNotFoundPage || Boolean(metadata.frontmatter?.isRoot)
442
+ const isOfflinePage = routePath === '/offline'
443
+ const isSpecialPage = isNotFoundPage || isOfflinePage
444
+ const isSiteRoot = routePath === '/'
445
+ const frontmatterIsRoot = Boolean(metadata.frontmatter?.isRoot)
446
+ const hidden = isSpecialPage
447
+ ? true
448
+ : frontmatterHidden === false
449
+ ? false
450
+ : frontmatterHidden === true
451
+ ? true
452
+ : frontmatterIsRoot
444
453
  return {
445
454
  routePath,
446
455
  routeHref: withBase(routePath),
447
- filePath,
456
+ path,
448
457
  source,
449
458
  relativePath: relPath,
450
459
  name: baseName,
@@ -452,11 +461,10 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
452
461
  segments,
453
462
  depth: segments.length,
454
463
  isIndex,
455
- isInternal: isInternalPage(baseName),
456
464
  title: metadata.title || derived?.title || (baseName === 'index' ? (dirName || 'Home') : baseName),
457
465
  weight: parseWeight(metadata.frontmatter?.weight),
458
466
  date: parseDate(metadata.frontmatter?.date) || parseDate(stats.mtime),
459
- isRoot: Boolean(metadata.frontmatter?.isRoot),
467
+ isRoot: isSiteRoot || frontmatterIsRoot,
460
468
  hidden,
461
469
  hiddenByFrontmatter,
462
470
  exclude,
@@ -468,9 +476,7 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
468
476
  size: stats.size,
469
477
  createdAt: stats.birthtime?.toISOString?.() || null,
470
478
  updatedAt: stats.mtime?.toISOString?.() || null
471
- },
472
- createdAt: stats.birthtime?.toISOString?.() || null,
473
- updatedAt: stats.mtime?.toISOString?.() || null
479
+ }
474
480
  }
475
481
  }
476
482
 
@@ -481,7 +487,7 @@ const collectPagesFromDir = async (pagesDir, source) => {
481
487
  const pages = []
482
488
  for await (const page of walkPages(pagesDir)) {
483
489
  const entry = await buildPageEntry({
484
- filePath: page.filePath,
490
+ path: page.path,
485
491
  pagesDir,
486
492
  source
487
493
  })
@@ -523,7 +529,12 @@ const collectPages = async () => {
523
529
 
524
530
  const buildIndexFallback = (pages, siteName) => {
525
531
  const visiblePages = pages
526
- .filter((page) => !page.isInternal && page.routePath !== '/')
532
+ .filter(
533
+ (page) =>
534
+ page.routePath !== '/' &&
535
+ page.routePath !== '/404' &&
536
+ page.routePath !== '/offline'
537
+ )
527
538
  .sort((a, b) => a.routePath.localeCompare(b.routePath))
528
539
 
529
540
  const lines = [
@@ -574,7 +585,7 @@ const buildNavSequence = (nodes, pagesByRoute) => {
574
585
  const seen = new Set()
575
586
  const addEntry = (entry) => {
576
587
  if (!entry.routePath) return
577
- const key = entry.filePath || entry.routePath
588
+ const key = entry.path || entry.routePath
578
589
  if (seen.has(key)) return
579
590
  seen.add(key)
580
591
  result.push(entry)
@@ -616,14 +627,13 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
616
627
  pages.unshift({
617
628
  routePath: '/',
618
629
  routeHref: withBase('/'),
619
- filePath: resolve(state.PAGES_DIR, 'index.md'),
630
+ path: resolve(state.PAGES_DIR, 'index.md'),
620
631
  relativePath: 'index.md',
621
632
  name: 'index',
622
633
  dir: '',
623
634
  segments: [],
624
635
  depth: 0,
625
636
  isIndex: true,
626
- isInternal: false,
627
637
  title: state.SITE_NAME || 'Methanol Site',
628
638
  weight: null,
629
639
  date: null,
@@ -648,10 +658,10 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
648
658
  }
649
659
  }
650
660
  const getPageByRoute = (routePath, options = {}) => {
651
- const { filePath } = options || {}
652
- if (filePath) {
661
+ const { path } = options || {}
662
+ if (path) {
653
663
  for (const page of pages) {
654
- if (page.routePath === routePath && page.filePath === filePath) {
664
+ if (page.routePath === routePath && page.path === path) {
655
665
  return page
656
666
  }
657
667
  }
@@ -717,26 +727,26 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
717
727
  pagesTree,
718
728
  getPagesTree,
719
729
  derivedTitleCache: pageDerivedCache,
720
- setDerivedTitle: (filePath, title, toc) => {
721
- if (!filePath) return
722
- pageDerivedCache.set(filePath, { title, toc })
730
+ setDerivedTitle: (path, title, toc) => {
731
+ if (!path) return
732
+ pageDerivedCache.set(path, { title, toc })
723
733
  },
724
- clearDerivedTitle: (filePath) => {
725
- if (!filePath) return
726
- pageDerivedCache.delete(filePath)
734
+ clearDerivedTitle: (path) => {
735
+ if (!path) return
736
+ pageDerivedCache.delete(path)
727
737
  },
728
738
  refreshPagesTree: () => {
729
739
  pagesTreeCache.clear()
730
740
  navSequenceCache.clear()
731
741
  pagesContext.pagesTree = getPagesTree('/')
732
742
  },
733
- getSiblings: (routePath, filePath = null) => {
743
+ getSiblings: (routePath, path = null) => {
734
744
  if (!routePath) return { prev: null, next: null }
735
745
  const sequence = getNavSequence(routePath)
736
746
  if (!sequence.length) return { prev: null, next: null }
737
747
  let index = -1
738
- if (filePath) {
739
- index = sequence.findIndex((entry) => entry.filePath === filePath)
748
+ if (path) {
749
+ index = sequence.findIndex((entry) => entry.path === path)
740
750
  }
741
751
  if (index < 0) {
742
752
  index = sequence.findIndex((entry) => entry.routePath === routePath)
@@ -748,7 +758,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
748
758
  routePath: entry.routePath,
749
759
  routeHref: entry.routeHref,
750
760
  title: entry.title || entry.name || entry.routePath,
751
- filePath: entry.filePath || null
761
+ path: entry.path || null
752
762
  }
753
763
  }
754
764
  return {
@@ -775,7 +785,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
775
785
  for (let i = 0; i < pages.length; i++) {
776
786
  const page = pages[i]
777
787
  if (logEnabled) {
778
- stageLogger.update(compileToken, `Compiling MDX [${i + 1}/${totalPages}] ${page.filePath}`)
788
+ stageLogger.update(compileToken, `Compiling MDX [${i + 1}/${totalPages}] ${page.path}`)
779
789
  }
780
790
  await compilePageMdx(page, pagesContext, {
781
791
  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) => {