methanol 0.0.6 → 0.0.8

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
@@ -23,6 +23,7 @@ import { readdir, readFile, stat } from 'fs/promises'
23
23
  import { existsSync } from 'fs'
24
24
  import { resolve, join, relative } from 'path'
25
25
  import { state, cli } from './state.js'
26
+ import { withBase } from './config.js'
26
27
  import { compilePageMdx } from './mdx.js'
27
28
  import { createStageLogger } from './stage-logger.js'
28
29
 
@@ -36,12 +37,11 @@ const pageDerivedCache = new Map()
36
37
  const collectLanguagesFromPages = (pages = []) => {
37
38
  const languages = new Map()
38
39
  for (const page of pages) {
39
- if (!page?.isIndex) continue
40
- const label = page?.frontmatter?.lang
40
+ if (!page.isIndex) continue
41
+ const label = page.frontmatter?.lang
41
42
  if (label == null || label === '') continue
42
43
  const routePath = page.routePath || '/'
43
- const href = page.routeHref || routePath || '/'
44
- const frontmatterCode = page?.frontmatter?.langCode
44
+ const frontmatterCode = page.frontmatter?.langCode
45
45
  const code =
46
46
  typeof frontmatterCode === 'string' && frontmatterCode.trim()
47
47
  ? frontmatterCode.trim()
@@ -50,19 +50,39 @@ const collectLanguagesFromPages = (pages = []) => {
50
50
  : routePath.replace(/^\/+/, '')
51
51
  languages.set(routePath, {
52
52
  routePath,
53
- href,
53
+ routeHref: withBase(routePath),
54
54
  label: String(label),
55
55
  code: code || null
56
56
  })
57
57
  }
58
- return Array.from(languages.values()).sort((a, b) => a.href.localeCompare(b.href))
58
+ return Array.from(languages.values()).sort((a, b) => a.routePath.localeCompare(b.routePath))
59
59
  }
60
60
 
61
61
  const normalizeRoutePath = (value) => {
62
+ if (!value) return '/'
63
+ let normalized = value
64
+ if (!normalized.startsWith('/')) {
65
+ normalized = `/${normalized}`
66
+ }
67
+ normalized = normalized.replace(/\/{2,}/g, '/')
68
+ if (normalized === '/') return '/'
69
+ if (normalized.endsWith('/')) {
70
+ return normalized.replace(/\/+$/, '/')
71
+ }
72
+ return normalized
73
+ }
74
+
75
+ const stripTrailingSlash = (value) => {
62
76
  if (!value || value === '/') return '/'
63
77
  return value.replace(/\/+$/, '')
64
78
  }
65
79
 
80
+ const toRoutePrefix = (value) => {
81
+ const normalized = normalizeRoutePath(value)
82
+ const stripped = stripTrailingSlash(normalized)
83
+ return stripped === '/' ? '' : stripped
84
+ }
85
+
66
86
  const resolveLanguageForRoute = (languages = [], routePath = '/') => {
67
87
  if (!languages.length) return null
68
88
  const normalizedRoute = normalizeRoutePath(routePath)
@@ -70,16 +90,17 @@ const resolveLanguageForRoute = (languages = [], routePath = '/') => {
70
90
  let bestLength = -1
71
91
  let rootLanguage = null
72
92
  for (const lang of languages) {
73
- const base = normalizeRoutePath(lang?.routePath || lang?.href)
93
+ const base = normalizeRoutePath(lang.routePath)
94
+ const basePrefix = toRoutePrefix(base)
74
95
  if (!base) continue
75
96
  if (base === '/') {
76
97
  rootLanguage = lang
77
98
  continue
78
99
  }
79
- if (normalizedRoute === base || normalizedRoute.startsWith(`${base}/`)) {
80
- if (base.length > bestLength) {
100
+ if (normalizedRoute === base || (basePrefix && normalizedRoute.startsWith(`${basePrefix}/`))) {
101
+ if (basePrefix.length > bestLength) {
81
102
  best = lang
82
- bestLength = base.length
103
+ bestLength = basePrefix.length
83
104
  }
84
105
  }
85
106
  }
@@ -104,7 +125,7 @@ export const routePathFromFile = (filePath, pagesDir = state.PAGES_DIR) => {
104
125
  return '/'
105
126
  }
106
127
  if (normalized.endsWith('/index')) {
107
- return `/${normalized.slice(0, -'/index'.length)}`
128
+ return `/${normalized.slice(0, -'/index'.length)}/`
108
129
  }
109
130
  return `/${normalized}`
110
131
  }
@@ -162,7 +183,8 @@ const stripRootPrefix = (value, rootDir) => {
162
183
 
163
184
  const buildPagesTree = (pages, options = {}) => {
164
185
  const rootPath = normalizeRoutePath(options.rootPath || '/')
165
- const rootDir = rootPath === '/' ? '' : rootPath.replace(/^\/+/, '')
186
+ const rootPrefix = toRoutePrefix(rootPath)
187
+ const rootDir = rootPrefix ? rootPrefix.replace(/^\/+/, '') : ''
166
188
  const includeHiddenRoot = Boolean(options.includeHiddenRoot)
167
189
  const currentRoutePath = normalizeRoutePath(options.currentRoutePath || '/')
168
190
  const rootSegments = rootDir ? rootDir.split('/') : []
@@ -170,8 +192,8 @@ const buildPagesTree = (pages, options = {}) => {
170
192
  if (!routePath) return '/'
171
193
  if (!rootDir) return routePath
172
194
  if (routePath === rootPath) return '/'
173
- if (routePath.startsWith(`${rootPath}/`)) {
174
- const stripped = routePath.slice(rootPath.length)
195
+ if (rootPrefix && routePath.startsWith(`${rootPrefix}/`)) {
196
+ const stripped = routePath.slice(rootPrefix.length)
175
197
  return stripped.startsWith('/') ? stripped : `/${stripped}`
176
198
  }
177
199
  return routePath
@@ -179,7 +201,7 @@ const buildPagesTree = (pages, options = {}) => {
179
201
  const currentRouteWithinRoot = resolveRouteWithinRoot(currentRoutePath)
180
202
  const isUnderRoot = (page) => {
181
203
  if (!rootDir) return true
182
- return page.routePath === rootPath || page.routePath.startsWith(`${rootPath}/`)
204
+ return page.routePath === rootPath || (rootPrefix && page.routePath.startsWith(`${rootPrefix}/`))
183
205
  }
184
206
  const treePages = pages
185
207
  .filter((page) => !page.isInternal)
@@ -249,10 +271,10 @@ const buildPagesTree = (pages, options = {}) => {
249
271
  children: [],
250
272
  depth,
251
273
  routePath: null,
274
+ routeHref: null,
252
275
  title: null,
253
276
  weight: null,
254
277
  date: null,
255
- routeHref: null,
256
278
  isRoot: false
257
279
  }
258
280
  dirs.set(path, dir)
@@ -273,7 +295,10 @@ const buildPagesTree = (pages, options = {}) => {
273
295
  const shouldExposeHidden =
274
296
  !isHidden404 &&
275
297
  page.hiddenByFrontmatter === true &&
276
- (page.routePath === currentRoutePath || isUnderExposedHiddenDir(page.dir))
298
+ (
299
+ page.routePath === currentRoutePath ||
300
+ (page.isIndex && page.dir && isUnderExposedHiddenDir(page.dir))
301
+ )
277
302
  if (!shouldExposeHidden) {
278
303
  continue
279
304
  }
@@ -303,7 +328,7 @@ const buildPagesTree = (pages, options = {}) => {
303
328
  if (page.isIndex && page.dir) {
304
329
  const dir = getDirNode(page.dir, page.dir.split('/').pop(), page.depth)
305
330
  dir.routePath = page.routePath
306
- dir.routeHref = page.routeHref || page.routePath
331
+ dir.routeHref = page.routeHref
307
332
  dir.title = page.title
308
333
  dir.weight = page.weight ?? null
309
334
  dir.date = page.date ?? null
@@ -378,9 +403,8 @@ const walkPages = async function* (dir, basePath = '') {
378
403
  const name = entry.replace(/\.(mdx|md)$/, '')
379
404
  const relativePath = join(basePath, name).replace(/\\/g, '/')
380
405
  const isIndex = name === 'index'
381
- const routePath = isIndex ? (basePath ? `/${basePath}` : '/') : `/${relativePath}`
382
- const routeHref = isIndex && basePath ? `/${basePath}/` : routePath
383
- yield { routePath, routeHref, filePath: fullPath, isIndex }
406
+ const routePath = isIndex ? (basePath ? `/${basePath}/` : '/') : `/${relativePath}`
407
+ yield { routePath, filePath: fullPath, isIndex }
384
408
  }
385
409
 
386
410
  for (const { entry, fullPath } of dirs) {
@@ -397,7 +421,6 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
397
421
  const dir = name.split('/').slice(0, -1).join('/')
398
422
  const dirName = dir ? dir.split('/').pop() : ''
399
423
  const isIndex = baseName === 'index'
400
- const routeHref = isIndex && dir ? `/${dir}/` : routePath
401
424
  const segments = routePath.split('/').filter(Boolean)
402
425
  const stats = await stat(filePath)
403
426
  const cached = pageMetadataCache.get(filePath)
@@ -420,7 +443,7 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
420
443
  : isNotFoundPage || Boolean(metadata.frontmatter?.isRoot)
421
444
  return {
422
445
  routePath,
423
- routeHref,
446
+ routeHref: withBase(routePath),
424
447
  filePath,
425
448
  source,
426
449
  relativePath: relPath,
@@ -463,7 +486,6 @@ const collectPagesFromDir = async (pagesDir, source) => {
463
486
  source
464
487
  })
465
488
  if (entry) {
466
- entry.routeHref = page.routeHref || entry.routeHref
467
489
  entry.isIndex = page.isIndex || entry.isIndex
468
490
  pages.push(entry)
469
491
  }
@@ -526,15 +548,22 @@ const buildIndexFallback = (pages, siteName) => {
526
548
  return lines.join('\n')
527
549
  }
528
550
 
529
- const resolveRootPath = (routePath, pagesByRoute, pagesByRouteIndex = null) => {
551
+ const resolveRootPath = (routePath, pagesByRoute) => {
530
552
  const normalized = normalizeRoutePath(routePath || '/')
531
553
  const segments = normalized.split('/').filter(Boolean)
532
- const lookup = pagesByRouteIndex || pagesByRoute
533
554
  for (let i = segments.length; i >= 1; i--) {
534
555
  const candidate = `/${segments.slice(0, i).join('/')}`
535
- const page = lookup.get(candidate)
556
+ const page = pagesByRoute.get(candidate)
536
557
  if (page?.isIndex && page?.isRoot) {
537
- return candidate
558
+ return page.routePath
559
+ }
560
+ const isExact = normalized === candidate
561
+ if (!isExact) {
562
+ const indexCandidate = candidate === '/' ? '/' : `${candidate}/`
563
+ const indexPage = pagesByRoute.get(indexCandidate)
564
+ if (indexPage?.isIndex && indexPage?.isRoot) {
565
+ return indexPage.routePath
566
+ }
538
567
  }
539
568
  }
540
569
  return '/'
@@ -544,7 +573,7 @@ const buildNavSequence = (nodes, pagesByRoute) => {
544
573
  const result = []
545
574
  const seen = new Set()
546
575
  const addEntry = (entry) => {
547
- if (!entry?.routePath) return
576
+ if (!entry.routePath) return
548
577
  const key = entry.filePath || entry.routePath
549
578
  if (seen.has(key)) return
550
579
  seen.add(key)
@@ -584,54 +613,42 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
584
613
  const hasIndex = pages.some((page) => page.routePath === '/')
585
614
  if (!hasIndex) {
586
615
  const content = buildIndexFallback(pages, state.SITE_NAME)
587
- pages = [
588
- {
589
- routePath: '/',
590
- routeHref: '/',
591
- filePath: resolve(state.PAGES_DIR, 'index.md'),
592
- relativePath: 'index.md',
593
- name: 'index',
594
- dir: '',
595
- segments: [],
596
- depth: 0,
597
- isIndex: true,
598
- isInternal: false,
599
- title: state.SITE_NAME || 'Methanol Site',
600
- weight: null,
601
- date: null,
602
- isRoot: false,
603
- hidden: false,
604
- content,
605
- frontmatter: {},
606
- matter: null,
607
- stats: { size: content.length, createdAt: null, updatedAt: null },
608
- createdAt: null,
609
- updatedAt: null
610
- },
611
- ...pages
612
- ]
616
+ pages.unshift({
617
+ routePath: '/',
618
+ routeHref: withBase('/'),
619
+ filePath: resolve(state.PAGES_DIR, 'index.md'),
620
+ relativePath: 'index.md',
621
+ name: 'index',
622
+ dir: '',
623
+ segments: [],
624
+ depth: 0,
625
+ isIndex: true,
626
+ isInternal: false,
627
+ title: state.SITE_NAME || 'Methanol Site',
628
+ weight: null,
629
+ date: null,
630
+ isRoot: false,
631
+ hidden: false,
632
+ content,
633
+ frontmatter: {},
634
+ matter: null,
635
+ stats: { size: content.length, createdAt: null, updatedAt: null },
636
+ createdAt: null,
637
+ updatedAt: null
638
+ })
613
639
  if (excludedRoutes?.has('/')) {
614
640
  excludedRoutes.delete('/')
615
641
  }
616
642
  }
617
643
 
618
644
  const pagesByRoute = new Map()
619
- const pagesByRouteIndex = new Map()
620
645
  for (const page of pages) {
621
- if (page.isIndex) {
622
- pagesByRouteIndex.set(page.routePath, page)
623
- if (!pagesByRoute.has(page.routePath)) {
624
- pagesByRoute.set(page.routePath, page)
625
- }
626
- continue
627
- }
628
- const existing = pagesByRoute.get(page.routePath)
629
- if (!existing || existing.isIndex) {
646
+ if (!pagesByRoute.has(page.routePath)) {
630
647
  pagesByRoute.set(page.routePath, page)
631
648
  }
632
649
  }
633
650
  const getPageByRoute = (routePath, options = {}) => {
634
- const { filePath, preferIndex } = options || {}
651
+ const { filePath } = options || {}
635
652
  if (filePath) {
636
653
  for (const page of pages) {
637
654
  if (page.routePath === routePath && page.filePath === filePath) {
@@ -639,18 +656,12 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
639
656
  }
640
657
  }
641
658
  }
642
- if (preferIndex === true) {
643
- return pagesByRouteIndex.get(routePath) || pagesByRoute.get(routePath) || null
644
- }
645
- if (preferIndex === false) {
646
- return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
647
- }
648
- return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
659
+ return pagesByRoute.get(routePath) || null
649
660
  }
650
661
  const pagesTreeCache = new Map()
651
662
  const navSequenceCache = new Map()
652
663
  const getPagesTree = (routePath) => {
653
- const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
664
+ const rootPath = resolveRootPath(routePath, pagesByRoute)
654
665
  const normalizedRoute = normalizeRoutePath(routePath || '/')
655
666
  const cacheKey = `${rootPath}::${normalizedRoute}`
656
667
  if (pagesTreeCache.has(cacheKey)) {
@@ -665,7 +676,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
665
676
  return tree
666
677
  }
667
678
  const getNavSequence = (routePath) => {
668
- const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
679
+ const rootPath = resolveRootPath(routePath, pagesByRoute)
669
680
  const normalizedRoute = normalizeRoutePath(routePath || '/')
670
681
  const cacheKey = `${rootPath}::${normalizedRoute}`
671
682
  if (navSequenceCache.has(cacheKey)) {
@@ -680,8 +691,10 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
680
691
  const notFound = pagesByRoute.get('/404') || null
681
692
  const languages = collectLanguagesFromPages(pages)
682
693
  const userSite = state.USER_SITE || {}
694
+ const siteBase = state.VITE_BASE ?? userSite.base ?? null
683
695
  const site = {
684
696
  ...userSite,
697
+ base: siteBase,
685
698
  name: state.SITE_NAME,
686
699
  root: state.ROOT_DIR,
687
700
  pagesDir: state.PAGES_DIR,
@@ -700,7 +713,6 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
700
713
  const pagesContext = {
701
714
  pages,
702
715
  pagesByRoute,
703
- pagesByRouteIndex,
704
716
  getPageByRoute,
705
717
  pagesTree,
706
718
  getPagesTree,
@@ -734,7 +746,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
734
746
  if (!entry) return null
735
747
  return {
736
748
  routePath: entry.routePath,
737
- routeHref: entry.routeHref || entry.routePath,
749
+ routeHref: entry.routeHref,
738
750
  title: entry.title || entry.name || entry.routePath,
739
751
  filePath: entry.filePath || null
740
752
  }
@@ -28,8 +28,8 @@ import { methanolPreviewRoutingPlugin } from './vite-plugins.js'
28
28
  export const runVitePreview = async () => {
29
29
  const baseConfig = {
30
30
  configFile: false,
31
+ appType: 'mpa',
31
32
  root: state.PAGES_DIR,
32
- base: '/',
33
33
  build: {
34
34
  outDir: state.DIST_DIR
35
35
  }
@@ -48,7 +48,7 @@ const linkOrCopyFile = async (src, dest) => {
48
48
 
49
49
  try {
50
50
  await ensureDir(dirname(dest))
51
-
51
+
52
52
  if (isWindows) {
53
53
  // Windows: Check for different drives first
54
54
  if (parse(src).root.toLowerCase() !== parse(dest).root.toLowerCase()) {
@@ -85,7 +85,7 @@ const processDir = async (sourceDir, targetDir, accumulated = new Set()) => {
85
85
  const sourcePath = resolve(sourceDir, entry)
86
86
  const targetPath = resolve(targetDir, entry)
87
87
  const stats = await stat(sourcePath)
88
-
88
+
89
89
  if (stats.isDirectory()) {
90
90
  await processDir(sourcePath, targetPath, accumulated)
91
91
  } else {
@@ -104,7 +104,7 @@ export const preparePublicAssets = async ({ themeDir, userDir, targetDir }) => {
104
104
  if (themeDir) {
105
105
  await processDir(themeDir, targetDir)
106
106
  }
107
-
107
+
108
108
  if (userDir) {
109
109
  await processDir(userDir, targetDir)
110
110
  }
@@ -112,7 +112,7 @@ export const preparePublicAssets = async ({ themeDir, userDir, targetDir }) => {
112
112
 
113
113
  export const updateAsset = async ({ type, filePath, themeDir, userDir, targetDir, relPath }) => {
114
114
  const targetPath = resolve(targetDir, relPath)
115
-
115
+
116
116
  if (type === 'unlink') {
117
117
  try {
118
118
  try {
@@ -122,7 +122,7 @@ export const updateAsset = async ({ type, filePath, themeDir, userDir, targetDir
122
122
  await rm(targetPath, { recursive: true, force: true })
123
123
  }
124
124
  }
125
-
125
+
126
126
  if (themeDir) {
127
127
  const themePath = resolve(themeDir, relPath)
128
128
  if (existsSync(themePath)) {
@@ -141,4 +141,4 @@ export const updateAsset = async ({ type, filePath, themeDir, userDir, targetDir
141
141
  return 'updated'
142
142
  }
143
143
  }
144
- }
144
+ }
@@ -27,7 +27,6 @@ import { state } from '../state.js'
27
27
  const extensionRegex = /\.(mdx?|html)$/i
28
28
 
29
29
  const isRelativeHref = (href) => {
30
- if (typeof href !== 'string') return false
31
30
  if (!href) return false
32
31
  if (href.startsWith('#') || href.startsWith('?') || href.startsWith('/')) return false
33
32
  if (href.startsWith('//')) return false
package/src/state.js CHANGED
@@ -148,6 +148,8 @@ export const state = {
148
148
  PROJECT_ROOT,
149
149
  ROOT_DIR: PROJECT_ROOT,
150
150
  SITE_NAME: 'Methanol Site',
151
+ SITE_BASE: null,
152
+ VITE_BASE: null,
151
153
  PAGES_DIR: resolve(PROJECT_ROOT, 'pages'),
152
154
  COMPONENTS_DIR: resolve(PROJECT_ROOT, 'components'),
153
155
  STATIC_DIR: resolve(PROJECT_ROOT, 'public'),
package/src/utils.js ADDED
@@ -0,0 +1,9 @@
1
+ export const cached = (fn) => {
2
+ let cache = null
3
+ return () => (cache ?? (cache = fn()))
4
+ }
5
+
6
+ export const cachedStr = (fn) => {
7
+ const cache = Object.create(null)
8
+ return (key) => (cache[key] ?? (cache[key] = fn(key)))
9
+ }
@@ -21,10 +21,9 @@
21
21
  import { createDOMRenderer } from 'refui/dom'
22
22
  import { defaults } from 'refui/browser'
23
23
  import { lazy } from 'refui'
24
- import { init } from '/.methanol_virtual_module/loader.js'
25
-
26
- const reg = import('/.methanol_virtual_module/registry.js')
24
+ import { init } from 'methanol:loader'
25
+ import { registry } from 'methanol:registry'
27
26
 
28
27
  const R = createDOMRenderer(defaults)
29
28
 
30
- reg.then((m) => init(m.registry, R))
29
+ init(registry, R)
@@ -22,6 +22,27 @@ let pagefindInit = null
22
22
  let pagefindUiInit = null
23
23
  let pagefindUiReady = false
24
24
 
25
+ const resolveBasePrefix = () => {
26
+ let base = import.meta.env?.BASE_URL || '/'
27
+ if (!base || base === '/' || base === './') return ''
28
+ if (base.startsWith('http://') || base.startsWith('https://')) {
29
+ try {
30
+ base = new URL(base).pathname
31
+ } catch {
32
+ return ''
33
+ }
34
+ }
35
+ if (!base.startsWith('/')) return ''
36
+ if (base.endsWith('/')) base = base.slice(0, -1)
37
+ return base
38
+ }
39
+
40
+ const withBase = (path) => {
41
+ const prefix = resolveBasePrefix()
42
+ if (!prefix || path.startsWith(`${prefix}/`)) return path
43
+ return `${prefix}${path}`
44
+ }
45
+
25
46
  const dynamicImport = (path) => {
26
47
  try {
27
48
  const importer = new Function('p', 'return import(p)')
@@ -38,7 +59,7 @@ export const loadPagefind = async () => {
38
59
  resolve(null)
39
60
  return
40
61
  }
41
- dynamicImport('/pagefind/pagefind.js')
62
+ dynamicImport(withBase('/pagefind/pagefind.js'))
42
63
  .then((mod) => {
43
64
  if (!mod) return resolve(null)
44
65
  if (mod.search) return resolve(mod)
@@ -89,7 +110,7 @@ export const loadPagefindUI = async (options = {}) => {
89
110
  return
90
111
  }
91
112
  const script = document.createElement('script')
92
- script.src = '/pagefind/pagefind-ui.js'
113
+ script.src = withBase('/pagefind/pagefind-ui.js')
93
114
  script.async = true
94
115
  script.onload = () => done(initPagefindUI(options))
95
116
  script.onerror = () => done(false)
@@ -24,8 +24,9 @@ import { existsSync } from 'node:fs'
24
24
  import { resolve } from 'node:path'
25
25
  import { normalizePath } from 'vite'
26
26
  import { state } from './state.js'
27
+ import { resolveBasePrefix } from './config.js'
27
28
  import { genRegistryScript } from './components.js'
28
- import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_SCRIPT } from './assets.js'
29
+ import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT } from './assets.js'
29
30
  import { projectRequire } from './node-loader.js'
30
31
 
31
32
  const require = createRequire(import.meta.url)
@@ -60,6 +61,9 @@ export const methanolPreviewRoutingPlugin = (distDir, notFoundPath) => ({
60
61
  cachedHtml = await readFile(notFoundPath, 'utf-8')
61
62
  return cachedHtml
62
63
  }
64
+
65
+ const basePrefix = resolveBasePrefix()
66
+
63
67
  const handler = async (req, res, next) => {
64
68
  if (!req.url || req.method !== 'GET') {
65
69
  return next()
@@ -69,6 +73,13 @@ export const methanolPreviewRoutingPlugin = (distDir, notFoundPath) => ({
69
73
  try {
70
74
  pathname = new URL(req.url, 'http://methanol').pathname
71
75
  pathname = decodeURIComponent(pathname)
76
+ if (basePrefix) {
77
+ if (pathname.startsWith(basePrefix)) {
78
+ pathname = pathname.slice(basePrefix.length)
79
+ } else {
80
+ return next()
81
+ }
82
+ }
72
83
  } catch {}
73
84
  const hasTrailingSlash = pathname.endsWith('/') && pathname !== '/'
74
85
  if (pathname.includes('.') && !pathname.endsWith('.html')) {
@@ -109,20 +120,43 @@ export const methanolPreviewRoutingPlugin = (distDir, notFoundPath) => ({
109
120
 
110
121
  const virtualModulePrefix = '/.methanol_virtual_module/'
111
122
  const resolvedVirtualModulePrefix = '\0' + virtualModulePrefix
123
+ const virtualModuleScheme = 'methanol:'
112
124
 
113
125
  const virtualModuleMap = {
126
+ get registry() {
127
+ return `export const registry = ${genRegistryScript()}`
128
+ },
114
129
  get 'registry.js'() {
115
130
  return `export const registry = ${genRegistryScript()}`
116
131
  },
117
- 'loader.js': LOADER_SCRIPT,
132
+ loader: LOADER_SCRIPT,
118
133
  'inject.js': INJECT_SCRIPT,
119
- 'pagefind.js': PAGEFIND_SCRIPT
134
+ 'pagefind-loader': PAGEFIND_LOADER_SCRIPT
120
135
  }
121
136
 
122
137
  const getModuleIdSegment = (id, start) => {
123
138
  return new URL(id.slice(start), 'http://methanol').pathname.slice(1)
124
139
  }
125
140
 
141
+ const getSchemeModuleKey = (id) => {
142
+ if (!id.startsWith(virtualModuleScheme)) return null
143
+ return id.slice(virtualModuleScheme.length)
144
+ }
145
+
146
+ const resolveVirtualModuleId = (id) => {
147
+ if (id.startsWith(virtualModulePrefix)) {
148
+ return id
149
+ }
150
+ const basePrefix = resolveBasePrefix()
151
+ if (basePrefix) {
152
+ const prefixed = `${basePrefix}${virtualModulePrefix}`
153
+ if (id.startsWith(prefixed)) {
154
+ return id.slice(basePrefix.length)
155
+ }
156
+ }
157
+ return null
158
+ }
159
+
126
160
  export const methanolResolverPlugin = () => {
127
161
  return {
128
162
  name: 'methanol-resolver',
@@ -139,10 +173,16 @@ export const methanolResolverPlugin = () => {
139
173
  return require.resolve(id)
140
174
  }
141
175
 
142
- if (id.startsWith(virtualModulePrefix)) {
143
- const _moduleId = getModuleIdSegment(id, virtualModulePrefix.length)
176
+ const schemeKey = getSchemeModuleKey(id)
177
+ if (schemeKey && Object.prototype.hasOwnProperty.call(virtualModuleMap, schemeKey)) {
178
+ return '\0' + id
179
+ }
180
+
181
+ const virtualId = resolveVirtualModuleId(id)
182
+ if (virtualId) {
183
+ const _moduleId = getModuleIdSegment(virtualId, virtualModulePrefix.length)
144
184
  if (Object.prototype.hasOwnProperty.call(virtualModuleMap, _moduleId)) {
145
- return '\0' + id
185
+ return '\0' + virtualId
146
186
  }
147
187
  }
148
188
 
@@ -164,6 +204,10 @@ export const methanolResolverPlugin = () => {
164
204
  }
165
205
  },
166
206
  load(id) {
207
+ if (id.startsWith('\0' + virtualModuleScheme)) {
208
+ const key = id.slice(1 + virtualModuleScheme.length)
209
+ return virtualModuleMap[key]
210
+ }
167
211
  if (id.startsWith(resolvedVirtualModulePrefix)) {
168
212
  const _moduleId = getModuleIdSegment(id, resolvedVirtualModulePrefix.length)
169
213
  return virtualModuleMap[_moduleId]
@@ -0,0 +1,38 @@
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
+ export default function ButtonGroup({
22
+ children,
23
+ className = '',
24
+ align = 'left', // left, center, right
25
+ ...props
26
+ }) {
27
+ const classes = [
28
+ 'button-group',
29
+ `button-group--${align}`,
30
+ className
31
+ ].filter(Boolean).join(' ')
32
+
33
+ return (
34
+ <div class={classes} {...props}>
35
+ {children}
36
+ </div>
37
+ )
38
+ }