このコミットが含まれているのは:
@@ -0,0 +1,190 @@
|
||||
import { readFile, readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { normalizePath as normalisePath } from 'vite'
|
||||
import { parse as parseYAML } from 'yaml'
|
||||
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
type Language = { currentEdition: string; publishedEditions: string[] }
|
||||
type Article = {
|
||||
identifier: string
|
||||
canonicalLanguage: string
|
||||
languages: Record<string, Language> }
|
||||
type Text = { title: string; body: string }
|
||||
type SearchEntry = Text & { identifier: string; edition: string }
|
||||
type Change = {
|
||||
identifier: string
|
||||
edition: string
|
||||
title: string
|
||||
pred: string | null
|
||||
change: 'new' | 'update'
|
||||
date: string }
|
||||
type RawArticle = {
|
||||
identifier: string
|
||||
canonical_language: string
|
||||
languages: Record<string, { current_edition: string }> }
|
||||
|
||||
const prefix = 'virtual:help/'
|
||||
|
||||
|
||||
const compareEditions = (a: string, b: string): number => {
|
||||
const [dateA, revisionA = '1'] = a.split ('.')
|
||||
const [dateB, revisionB = '1'] = b.split ('.')
|
||||
return dateA.localeCompare (dateB) || Number (revisionA) - Number (revisionB)
|
||||
}
|
||||
|
||||
|
||||
const validateEdition = (edition: string): void => {
|
||||
const [date, revision = '1'] = edition.split ('.')
|
||||
if (!(/^\d{4}-\d{2}-\d{2}(?:\.[1-9]\d*)?$/.test (edition))
|
||||
|| new Date (date).toISOString ().slice (0, 10) !== date
|
||||
|| !(Number.isSafeInteger (Number (revision))))
|
||||
throw new Error (`Invalid Help edition: ${ edition }`)
|
||||
}
|
||||
|
||||
|
||||
const generateHelpAssets = async (root: string) => {
|
||||
const modules = new Map<string, string> ()
|
||||
const files: string[] = [root]
|
||||
const articles: Record<string, Article> = Object.create (null)
|
||||
const search: Record<string, SearchEntry[]> = Object.create (null)
|
||||
const changes: Record<string, Change[]> = Object.create (null)
|
||||
const titles: Record<string, Record<string, string[]>> = Object.create (null)
|
||||
const components: string[] = []
|
||||
const sources: string[] = []
|
||||
const texts: string[] = []
|
||||
const directories = (await readdir (root, { withFileTypes: true }))
|
||||
.filter (entry => entry.isDirectory () && !(entry.name.startsWith ('.')))
|
||||
.sort ((a, b) => a.name.localeCompare (b.name))
|
||||
|
||||
for (const directory of directories)
|
||||
{
|
||||
const articlePath = path.join (root, directory.name, 'article.yml')
|
||||
files.push (articlePath)
|
||||
const raw = parseYAML (await readFile (articlePath, 'utf8')) as RawArticle | null
|
||||
if (raw == null || raw.identifier !== directory.name
|
||||
|| typeof raw.canonical_language !== 'string'
|
||||
|| raw.languages == null || typeof raw.languages !== 'object'
|
||||
|| !(Object.hasOwn (raw.languages, raw.canonical_language)))
|
||||
throw new Error (`Invalid Help metadata: ${ articlePath }`)
|
||||
|
||||
const article: Article = {
|
||||
identifier: raw.identifier,
|
||||
canonicalLanguage: raw.canonical_language,
|
||||
languages: Object.create (null) }
|
||||
articles[raw.identifier] = article
|
||||
|
||||
for (const [language, metadata] of Object.entries (raw.languages))
|
||||
{
|
||||
if (!(/^[a-zA-Z0-9-]+$/.test (language))
|
||||
|| typeof metadata?.current_edition !== 'string')
|
||||
throw new Error (`Invalid Help language: ${ articlePath }/${ language }`)
|
||||
|
||||
const currentEdition = metadata.current_edition
|
||||
validateEdition (currentEdition)
|
||||
const editionRoot = path.join (root, directory.name, 'editions', language)
|
||||
files.push (editionRoot)
|
||||
const editions = (await readdir (editionRoot)).filter (name => name.endsWith ('.mdx'))
|
||||
.map (name => name.slice (0, -4))
|
||||
editions.forEach (validateEdition)
|
||||
if (!(editions.includes (currentEdition)))
|
||||
throw new Error (`Missing current Help edition: ${ editionRoot }/${ currentEdition }`)
|
||||
|
||||
const publishedEditions = editions
|
||||
.filter (edition => compareEditions (edition, currentEdition) <= 0)
|
||||
.sort (compareEditions)
|
||||
article.languages[language] = { currentEdition, publishedEditions }
|
||||
search[language] ??= []
|
||||
changes[language] ??= []
|
||||
titles[language] ??= Object.create (null)
|
||||
|
||||
for (const [index, edition] of publishedEditions.entries ())
|
||||
{
|
||||
const sourcePath = path.join (editionRoot, `${ edition }.mdx`)
|
||||
files.push (sourcePath)
|
||||
const mdx = (await readFile (sourcePath, 'utf8')).replace (/^\uFEFF/, '')
|
||||
const heading = mdx.match (/^# +([^\r\n]+)(?:\r?\n|$)/)
|
||||
if (heading == null || !(heading[1].trim ()))
|
||||
throw new Error (`Missing leading Help H1: ${ sourcePath }`)
|
||||
|
||||
const text = { title: heading[1].trim (), body: mdx.slice (heading[0].length) }
|
||||
const key = JSON.stringify (`${ raw.identifier }/${ language }/${ edition }`)
|
||||
const textId = `${ prefix }text/${ raw.identifier }/${ language }/${ edition }`
|
||||
const componentId = JSON.stringify (normalisePath (sourcePath))
|
||||
const sourceId = JSON.stringify (normalisePath (sourcePath) + '?raw')
|
||||
modules.set (textId, `export default ${ JSON.stringify (text) }`)
|
||||
texts.push (`${ key }: () => import(${ JSON.stringify (textId) })`)
|
||||
components.push (`${ key }: () => import(${ componentId })`)
|
||||
sources.push (`${ key }: () => import(${ sourceId })`)
|
||||
|
||||
if (edition === currentEdition)
|
||||
search[language].push ({ identifier: raw.identifier, edition, ...text })
|
||||
changes[language].push ({ identifier: raw.identifier, edition, title: text.title,
|
||||
pred: publishedEditions[index - 1] ?? null,
|
||||
change: index === 0 ? 'new' : 'update', date: edition.split ('.')[0] })
|
||||
const title = text.title.trim ().normalize ('NFKC').toLowerCase ()
|
||||
const identifiers = titles[language][title] ??= []
|
||||
if (!(identifiers.includes (raw.identifier)))
|
||||
identifiers.push (raw.identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys (articles).length === 0)
|
||||
throw new Error (`No Help articles found: ${ root }`)
|
||||
|
||||
Object.values (changes).forEach (rows => rows.sort ((a, b) =>
|
||||
compareEditions (b.edition, a.edition) || a.identifier.localeCompare (b.identifier)))
|
||||
for (const [name, data] of Object.entries ({ articles, search, changes, titles }))
|
||||
{
|
||||
modules.set (`${ prefix }${ name }`,
|
||||
`export default JSON.parse(${ JSON.stringify (JSON.stringify (data)) })`)
|
||||
}
|
||||
modules.set (`${ prefix }loaders`, [
|
||||
`export const components = { ${ components.join (',\n') } };`,
|
||||
`export const sources = { ${ sources.join (',\n') } };`,
|
||||
`export const texts = { ${ texts.join (',\n') } };`].join ('\n'))
|
||||
return { modules, files }
|
||||
}
|
||||
|
||||
|
||||
export const helpAssets = (): Plugin => {
|
||||
const state: { root: string; data?: ReturnType<typeof generateHelpAssets> } = { root: '' }
|
||||
const data = () => state.data ??= generateHelpAssets (state.root)
|
||||
|
||||
return {
|
||||
name: 'help-assets',
|
||||
configResolved (config) {
|
||||
state.root = path.resolve (config.root, 'help')
|
||||
},
|
||||
async buildStart () {
|
||||
state.data = generateHelpAssets (state.root)
|
||||
const generated = await data ()
|
||||
generated.files.forEach (file => this.addWatchFile (file))
|
||||
},
|
||||
resolveId (id) {
|
||||
return id.startsWith (prefix) ? `\0${ id }` : null
|
||||
},
|
||||
async load (id) {
|
||||
if (!(id.startsWith (`\0${ prefix }`)))
|
||||
return null
|
||||
const source = (await data ()).modules.get (id.slice (1))
|
||||
if (source == null)
|
||||
throw new Error (`Unknown Help asset: ${ id.slice (1) }`)
|
||||
return source
|
||||
},
|
||||
configureServer (server) {
|
||||
server.watcher.add (state.root)
|
||||
server.watcher.on ('all', (_event, file) => {
|
||||
if (!(file.startsWith (state.root + path.sep)) || !(/\.(mdx|yml)$/.test (file)))
|
||||
return
|
||||
state.data = undefined
|
||||
for (const module of server.moduleGraph.idToModuleMap.values ())
|
||||
{
|
||||
if (module.id?.startsWith (`\0${ prefix }`))
|
||||
server.moduleGraph.invalidateModule (module)
|
||||
}
|
||||
server.ws.send ({ type: 'full-reload' })
|
||||
})
|
||||
} }
|
||||
}
|
||||
@@ -219,18 +219,19 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="top-nav-root px-3 flex justify-between items-center w-full">
|
||||
<div className="flex items-center gap-2 h-12">
|
||||
<nav className="top-nav-root px-3 flex flex-wrap justify-between items-center w-full">
|
||||
<div className="flex shrink-0 items-center gap-2 h-12">
|
||||
<PrefetchLink
|
||||
to="/posts"
|
||||
className="top-nav-brand-link mx-4 text-xl font-bold"
|
||||
className="top-nav-brand-link shrink-0 whitespace-nowrap mx-2 text-base
|
||||
xl:mx-4 xl:text-xl font-bold"
|
||||
onClick={() => {
|
||||
scroll (0, 0)
|
||||
}}>
|
||||
ぼざクリ タグ広場
|
||||
</PrefetchLink>
|
||||
|
||||
<div ref={navRef} className="relative hidden md:flex h-12 items-center">
|
||||
<div ref={navRef} className="relative hidden xl:flex shrink-0 h-12 items-center">
|
||||
<div aria-hidden
|
||||
className={cn ('absolute inset-y-0 h-12',
|
||||
'top-nav-highlight',
|
||||
@@ -242,6 +243,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
{visibleMenu.map ((item, i) => (
|
||||
<motion.div
|
||||
key={item.to}
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
{...(animationsOff
|
||||
? { }
|
||||
: { layoutId: `menu-${ item.name }` })}
|
||||
@@ -271,6 +273,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
measure (-1)
|
||||
}}
|
||||
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center px-5',
|
||||
'shrink-0 whitespace-nowrap',
|
||||
(openItemIdx < 0 || moreVsbl) && 'font-bold')}>
|
||||
その他 »
|
||||
</PrefetchLink>
|
||||
@@ -281,7 +284,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="top-nav-mobile-toggle md:hidden ml-auto border-0 bg-transparent pr-4"
|
||||
className="top-nav-mobile-toggle xl:hidden shrink-0 whitespace-nowrap
|
||||
ml-auto border-0 bg-transparent pr-4"
|
||||
onClick={() => {
|
||||
setMenuOpen (!(menuOpen))
|
||||
}}>
|
||||
@@ -294,7 +298,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
key="submenu-shell"
|
||||
{...(animationsOff ? { }
|
||||
: { layout: true })}
|
||||
className="relative z-20 hidden md:block overflow-hidden top-nav-submenu"
|
||||
className="relative z-20 hidden xl:block shrink-0 overflow-hidden top-nav-submenu"
|
||||
animate={{ height: submenuHeight }}
|
||||
onMouseLeave={() => {
|
||||
if (moreVsbl)
|
||||
@@ -308,7 +312,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
? (
|
||||
moreMenu.map ((item, i) => (
|
||||
<div key={i} className="relative h-[40px]">
|
||||
<div className="absolute inset-0 flex items-center px-3">
|
||||
<div className="absolute inset-0 flex items-center px-3
|
||||
overflow-x-auto whitespace-nowrap">
|
||||
<motion.div
|
||||
transition={topNavTransition}
|
||||
{...(animationsOff
|
||||
@@ -318,7 +323,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
: { initial: { x: 40, y: -40, opacity: 0 },
|
||||
animate: { x: 0, y: 0, opacity: 1 },
|
||||
exit: { x: 40, y: -40, opacity: 0 } }))}
|
||||
className="z-10 h-full flex items-center px-3 font-bold w-28">
|
||||
className="z-10 h-full flex shrink-0 items-center px-3 font-bold w-28">
|
||||
<h2>{item.name}</h2>
|
||||
</motion.div>
|
||||
{item.subMenu
|
||||
@@ -366,7 +371,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
<div className="relative h-[40px]">
|
||||
{animationsOff
|
||||
? (
|
||||
<div className="absolute inset-0 flex items-center px-3">
|
||||
<div className="absolute inset-0 flex items-center px-3
|
||||
overflow-x-auto whitespace-nowrap">
|
||||
{(visibleMenu[activeIdx]?.subMenu ?? [])
|
||||
.filter (item => item.visible ?? true)
|
||||
.map ((item, i) => (
|
||||
@@ -395,7 +401,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }),
|
||||
centre: { y: 0, opacity: 1 },
|
||||
exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }}
|
||||
className="absolute inset-0 flex items-center px-3"
|
||||
className="absolute inset-0 flex items-center px-3
|
||||
overflow-x-auto whitespace-nowrap"
|
||||
initial="enter"
|
||||
animate="centre"
|
||||
exit="exit"
|
||||
@@ -435,7 +442,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
? (
|
||||
menuOpen && (
|
||||
<div
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col md:hidden',
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col xl:hidden',
|
||||
'items-start')}>
|
||||
<Separator/>
|
||||
{visibleMenu.map ((item, i) => (
|
||||
@@ -479,9 +486,6 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
</Fragment>))}
|
||||
<PrefetchLink
|
||||
to="/more"
|
||||
ref={(el: (HTMLAnchorElement | null)) => {
|
||||
itemsRef.current[visibleMenu.length] = el
|
||||
}}
|
||||
className={cn ('top-nav-mobile-row w-full min-h-[40px] flex items-center pl-8',
|
||||
((openItemIdx < 0)
|
||||
&& 'top-nav-mobile-active font-bold'))}>
|
||||
@@ -495,7 +499,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
{menuOpen && (
|
||||
<motion.div
|
||||
key="spmenu"
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col md:hidden',
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col xl:hidden',
|
||||
'items-start')}
|
||||
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
|
||||
height: 0 },
|
||||
@@ -561,9 +565,6 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
</Fragment>))}
|
||||
<PrefetchLink
|
||||
to="/more"
|
||||
ref={(el: (HTMLAnchorElement | null)) => {
|
||||
itemsRef.current[visibleMenu.length] = el
|
||||
}}
|
||||
className={cn ('top-nav-mobile-row w-full min-h-[40px] flex items-center pl-8',
|
||||
((openItemIdx < 0)
|
||||
&& 'top-nav-mobile-active font-bold'))}>
|
||||
|
||||
@@ -15,8 +15,8 @@ const TopNavUser: FC<Props> = ({ user, sp }) => {
|
||||
return
|
||||
|
||||
const className = cn ((sp
|
||||
? 'md:hidden w-full min-h-[40px] flex items-center pl-8'
|
||||
: 'hidden md:block ml-auto pr-4'),
|
||||
? 'xl:hidden w-full min-h-[40px] flex items-center pl-8'
|
||||
: 'hidden xl:block shrink-0 max-w-full break-words ml-auto pr-4'),
|
||||
'font-bold text-red-600 hover:text-red-400',
|
||||
'dark:text-yellow-400 dark:hover:text-yellow-200')
|
||||
|
||||
@@ -30,4 +30,4 @@ const TopNavUser: FC<Props> = ({ user, sp }) => {
|
||||
</>)
|
||||
}
|
||||
|
||||
export default TopNavUser
|
||||
export default TopNavUser
|
||||
|
||||
@@ -197,7 +197,7 @@ body
|
||||
background: var(--top-nav-root-bg-mobile);
|
||||
}
|
||||
|
||||
@media (min-width: 768px)
|
||||
@media (min-width: 1280px)
|
||||
{
|
||||
.top-nav-root
|
||||
{
|
||||
|
||||
+36
-109
@@ -1,15 +1,12 @@
|
||||
import { parse as parseYAML } from 'yaml'
|
||||
|
||||
import type { ComponentType } from 'react'
|
||||
|
||||
type ArticleLanguage = { currentEdition: string }
|
||||
type ArticleLanguage = { currentEdition: string; publishedEditions: string[] }
|
||||
|
||||
type ArticleMeta = {
|
||||
export type ArticleMeta = {
|
||||
identifier: string
|
||||
canonicalLanguage: string
|
||||
languages: Record<string, ArticleLanguage> }
|
||||
|
||||
type MDXModule = { default: ComponentType }
|
||||
|
||||
export type HelpEditionText = {
|
||||
title: string
|
||||
body: string }
|
||||
@@ -21,7 +18,11 @@ export type HelpSearchEntry = HelpEditionText & {
|
||||
export type HelpEditionContent = HelpEditionText & { content: ComponentType }
|
||||
|
||||
export type HelpEditionSource = HelpEditionText & { source: string }
|
||||
export type HelpChange = HelpSearchEntry & { pred: string | null }
|
||||
export type HelpChange = Pick<HelpSearchEntry, 'identifier' | 'edition' | 'title'> & {
|
||||
pred: string | null
|
||||
change: 'new' | 'update'
|
||||
date: string }
|
||||
export type HelpTitleIndex = Record<string, string[]>
|
||||
export type HelpDiffLine = {
|
||||
type: 'added' | 'removed' | 'unchanged'
|
||||
content: string }
|
||||
@@ -30,70 +31,26 @@ export type HelpDiff = {
|
||||
to: HelpEditionSource
|
||||
lines: HelpDiffLine[] }
|
||||
|
||||
const articleFiles = import.meta.glob<string> ('../../help/*/article.yml',
|
||||
{ query: '?raw',
|
||||
import: 'default',
|
||||
eager: true })
|
||||
|
||||
const editionFiles = import.meta.glob<MDXModule> ('../../help/*/editions/*/*.mdx')
|
||||
|
||||
const rawEditionFiles = import.meta.glob<string> ('../../help/*/editions/*/*.mdx',
|
||||
{ query: '?raw',
|
||||
import: 'default' })
|
||||
|
||||
|
||||
const cameliseArticle = (
|
||||
value: { identifier: string
|
||||
languages: Record<string, { ['current_edition']: string }> },
|
||||
): ArticleMeta => (
|
||||
{ identifier: value.identifier,
|
||||
languages: Object.fromEntries (
|
||||
Object.entries (value.languages).map (([language, data]) => [
|
||||
language, { currentEdition: data['current_edition'] }])) })
|
||||
|
||||
|
||||
const compareEditions = (a: string, b: string): number => {
|
||||
const [dateA, revisionA = '1'] = a.split ('.')
|
||||
const [dateB, revisionB = '1'] = b.split ('.')
|
||||
|
||||
return dateA.localeCompare (dateB) || Number (revisionA) - Number (revisionB)
|
||||
export const getHelpArticle = async (identifier: string): Promise<ArticleMeta | null> => {
|
||||
const { default: articles } = await import ('virtual:help/articles')
|
||||
return Object.hasOwn (articles, identifier) ? articles[identifier] : null
|
||||
}
|
||||
|
||||
|
||||
export const getHelpArticle = (identifier: string): ArticleMeta | null => {
|
||||
const path = `../../help/${ identifier }/article.yml`
|
||||
const source = articleFiles[path]
|
||||
|
||||
if (!(source))
|
||||
return null
|
||||
|
||||
return cameliseArticle (parseYAML (source))
|
||||
}
|
||||
|
||||
|
||||
export const getHelpEditions = (
|
||||
export const getHelpEditions = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
): string[] => {
|
||||
const prefix = `../../help/${ identifier }/editions/${ language }/`
|
||||
|
||||
return (Object.keys (editionFiles)
|
||||
.filter (path => path.startsWith (prefix))
|
||||
.map (path => path.slice (prefix.length, -'.mdx'.length))
|
||||
.sort (compareEditions))
|
||||
): Promise<string[]> => {
|
||||
const article = await getHelpArticle (identifier)
|
||||
return article?.languages[language]?.publishedEditions ?? []
|
||||
}
|
||||
|
||||
|
||||
export const getHelpPublishedEditions = (
|
||||
export const getHelpPublishedEditions = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
): string[] => {
|
||||
const currentEdition = getHelpArticle (identifier)?.languages[language]?.currentEdition
|
||||
return currentEdition == null
|
||||
? []
|
||||
: getHelpEditions (identifier, language)
|
||||
.filter (edition => compareEditions (edition, currentEdition) <= 0)
|
||||
}
|
||||
): Promise<string[]> => await getHelpEditions (identifier, language)
|
||||
|
||||
|
||||
export const getHelpEdition = async (
|
||||
@@ -101,8 +58,8 @@ export const getHelpEdition = async (
|
||||
language: string,
|
||||
edition: string,
|
||||
): Promise<ComponentType | null> => {
|
||||
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
|
||||
const loader = editionFiles[path]
|
||||
const { components } = await import ('virtual:help/loaders')
|
||||
const loader = components[`${ identifier }/${ language }/${ edition }`]
|
||||
|
||||
if (!(loader))
|
||||
return null
|
||||
@@ -116,22 +73,15 @@ export const getHelpEditionSource = async (
|
||||
language: string,
|
||||
edition: string,
|
||||
): Promise<HelpEditionSource | null> => {
|
||||
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
|
||||
const loader = rawEditionFiles[path]
|
||||
const { sources } = await import ('virtual:help/loaders')
|
||||
const loader = sources[`${ identifier }/${ language }/${ edition }`]
|
||||
|
||||
if (!(loader))
|
||||
return null
|
||||
|
||||
const source = await loader ()
|
||||
const mdx = source.replace (/^\uFEFF/, '')
|
||||
const heading = mdx.match (/^# +([^\r\n]+)(?:\r?\n|$)/)
|
||||
|
||||
if (!(heading))
|
||||
throw new Error (`Help MDX の先頭に H1 がありません: ${ path }`)
|
||||
|
||||
return { source,
|
||||
title: heading[1].trim (),
|
||||
body: mdx.slice (heading[0].length) }
|
||||
const [source, text] = await Promise.all ([
|
||||
loader (), getHelpEditionText (identifier, language, edition)])
|
||||
return text == null ? null : { source: source.default, ...text }
|
||||
}
|
||||
|
||||
|
||||
@@ -140,31 +90,17 @@ export const getHelpEditionText = async (
|
||||
language: string,
|
||||
edition: string,
|
||||
): Promise<HelpEditionText | null> => {
|
||||
const content = await getHelpEditionSource (identifier, language, edition)
|
||||
return content == null ? null : { title: content.title, body: content.body }
|
||||
const { texts } = await import ('virtual:help/loaders')
|
||||
const loader = texts[`${ identifier }/${ language }/${ edition }`]
|
||||
return loader == null ? null : (await loader ()).default
|
||||
}
|
||||
|
||||
|
||||
export const getHelpSearchEntries = async (
|
||||
language: string,
|
||||
): Promise<HelpSearchEntry[]> => {
|
||||
const entries = await Promise.all (
|
||||
Object.values (articleFiles).map (async source => {
|
||||
const article = cameliseArticle (parseYAML (source))
|
||||
const edition = article.languages[language]?.currentEdition
|
||||
|
||||
if (!(edition))
|
||||
return null
|
||||
|
||||
const content = await getHelpEditionText (article.identifier, language, edition)
|
||||
|
||||
if (!(content))
|
||||
throw new Error (`Help edition が見つかりません: ${ article.identifier }/${ edition }`)
|
||||
|
||||
return { identifier: article.identifier, edition, ...content }
|
||||
}))
|
||||
|
||||
return entries.filter ((entry): entry is HelpSearchEntry => entry != null)
|
||||
const { default: search } = await import ('virtual:help/search')
|
||||
return search[language] ?? []
|
||||
}
|
||||
|
||||
|
||||
@@ -182,23 +118,14 @@ export const getHelpEditionContent = async (
|
||||
|
||||
|
||||
export const getHelpChanges = async (language: string): Promise<HelpChange[]> => {
|
||||
const articles = Object.values (articleFiles).map (source =>
|
||||
cameliseArticle (parseYAML (source)))
|
||||
const changes = await Promise.all (articles.flatMap (article => {
|
||||
const editions = getHelpPublishedEditions (article.identifier, language)
|
||||
return editions.map (async (edition, index) => {
|
||||
const text = await getHelpEditionText (article.identifier, language, edition)
|
||||
if (text == null)
|
||||
throw new Error (`Help edition が見つかりません: ${ article.identifier }/${ edition }`)
|
||||
const { default: changes } = await import ('virtual:help/changes')
|
||||
return changes[language] ?? []
|
||||
}
|
||||
|
||||
return { identifier: article.identifier,
|
||||
edition, pred: editions[index - 1] ?? null, ...text }
|
||||
})
|
||||
}))
|
||||
|
||||
return changes.sort ((a, b) =>
|
||||
compareEditions (b.edition, a.edition)
|
||||
|| a.identifier.localeCompare (b.identifier))
|
||||
export const getHelpTitleIndex = async (language: string): Promise<HelpTitleIndex> => {
|
||||
const { default: titles } = await import ('virtual:help/titles')
|
||||
return titles[language] ?? { }
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +179,7 @@ export const getHelpDiff = async (
|
||||
from: string,
|
||||
to: string,
|
||||
): Promise<HelpDiff | null> => {
|
||||
const editions = getHelpPublishedEditions (identifier, language)
|
||||
const editions = await getHelpPublishedEditions (identifier, language)
|
||||
if (!(editions.includes (from)) || !(editions.includes (to)))
|
||||
return null
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ export const wikiKeys = {
|
||||
export const helpKeys = {
|
||||
root: ['help'] as const,
|
||||
index: (language: string) => ['help', 'index', language] as const,
|
||||
titles: (language: string) => ['help', 'titles', language] as const,
|
||||
article: (identifier: string) => ['help', 'article', identifier] as const,
|
||||
edition: (identifier: string, language: string, edition: string) =>
|
||||
['help', 'edition', identifier, language, edition] as const,
|
||||
|
||||
@@ -1,75 +1,119 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { getHelpChanges } from '@/lib/help'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
|
||||
import { dateString } from '@/lib/utils'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
|
||||
const HelpChangesPage: FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams ()
|
||||
const identifier = searchParams.get ('identifier') ?? ''
|
||||
const pageValue = searchParams.get ('page') ?? '1'
|
||||
const pageNumber = Number (pageValue)
|
||||
const requestedPage =
|
||||
(/^[1-9]\d*$/.test (pageValue) && Number.isSafeInteger (pageNumber))
|
||||
? pageNumber : 1
|
||||
const limit = LIST_LIMIT_OPTIONS.find (
|
||||
value => String (value) === searchParams.get ('limit')) ?? 20
|
||||
const { data: changes = [], isLoading, error } = useQuery ({
|
||||
queryKey: helpKeys.changes ('ja'),
|
||||
queryFn: () => getHelpChanges ('ja'),
|
||||
staleTime: Infinity })
|
||||
|
||||
const results = useMemo (() => identifier
|
||||
? changes.filter (entry => entry.identifier === identifier)
|
||||
: changes, [changes, identifier])
|
||||
const totalPages = Math.ceil (results.length / limit)
|
||||
const page = requestedPage <= Math.max (1, totalPages) ? requestedPage : 1
|
||||
const pageResults = results.slice ((page - 1) * limit, page * limit)
|
||||
|
||||
const handleLimit = (value: string) => {
|
||||
const next = new URLSearchParams (searchParams)
|
||||
next.set ('limit', value)
|
||||
next.set ('page', '1')
|
||||
setSearchParams (next)
|
||||
}
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<MainArea className="min-w-0 max-w-full">
|
||||
<Helmet>
|
||||
<title>{`ヘルプ 変更履歴 | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageTitle>変更履歴</PageTitle>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>変更履歴</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<select value={limit} onChange={ev => handleLimit (ev.target.value)}
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground">
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>{value}</option>))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isLoading
|
||||
? <p className="mt-4">読み込み中...</p>
|
||||
: (error
|
||||
? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
|
||||
: (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-auto w-full whitespace-nowrap border-collapse">
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left">差分</th>
|
||||
<th className="p-2 text-left">タイトル</th>
|
||||
<th className="p-2 text-left">版</th>
|
||||
<th className="p-2 text-left">変更</th>
|
||||
<th className="p-2 text-left">更新日時</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{changes.map (entry => {
|
||||
const path = `/help/${ encodeURIComponent (entry.identifier) }`
|
||||
const diffQuery = new URLSearchParams ({
|
||||
from: entry.pred ?? '', to: entry.edition })
|
||||
const updatedAt = `${ entry.edition.split ('.')[0] }T00:00:00`
|
||||
<div className="mt-4">
|
||||
<p className="mb-2 text-sm" aria-live="polite">{results.length} 件</p>
|
||||
<div className="min-w-0 max-w-full overflow-x-auto">
|
||||
<table className="table-auto w-full min-w-[900px] whitespace-nowrap
|
||||
border-collapse">
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left">差分</th>
|
||||
<th className="p-2 text-left">タイトル</th>
|
||||
<th className="p-2 text-left">版</th>
|
||||
<th className="p-2 text-left">変更</th>
|
||||
<th className="p-2 text-left">更新日時</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pageResults.map (entry => {
|
||||
const path = `/help/${ encodeURIComponent (entry.identifier) }`
|
||||
const diffQuery = new URLSearchParams ({
|
||||
from: entry.pred ?? '', to: entry.edition })
|
||||
const diffTo = `${ path }/diff?${ diffQuery.toString () }`
|
||||
const updatedAt = `${ entry.date }T00:00:00`
|
||||
|
||||
return (
|
||||
<tr key={`${ entry.identifier }/${ entry.edition }`}
|
||||
className="even:bg-gray-100 text-gray-900
|
||||
dark:even:bg-gray-700 dark:text-gray-100">
|
||||
<td className="p-2">
|
||||
{entry.pred != null && (
|
||||
<PrefetchLink to={`${ path }/diff?${ diffQuery.toString () }`}>
|
||||
差分
|
||||
</PrefetchLink>)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<PrefetchLink to={`${ path }?edition=${ entry.edition }`}>
|
||||
{entry.title}
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2">{entry.edition}</td>
|
||||
<td className="p-2">{entry.pred == null ? '新規' : '更新'}</td>
|
||||
<td className="p-2">{dateString (updatedAt, 'hour')}</td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
return (
|
||||
<tr key={`${ entry.identifier }/${ entry.edition }`}
|
||||
className="even:bg-gray-100 text-gray-900
|
||||
dark:even:bg-gray-700 dark:text-gray-100">
|
||||
<td className="p-2">
|
||||
{entry.pred != null && (
|
||||
<PrefetchLink to={diffTo}>
|
||||
差分
|
||||
</PrefetchLink>)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<PrefetchLink to={`${ path }?edition=${ entry.edition }`}>
|
||||
{entry.title}
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2">{entry.edition}</td>
|
||||
<td className="p-2">{entry.change === 'new' ? '新規' : '更新'}</td>
|
||||
<td className="p-2">{dateString (updatedAt, 'hour')}</td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{totalPages > 0 && <Pagination page={page} totalPages={totalPages}/>}
|
||||
</div>))}
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { getHelpArticle, getHelpEditionContent, getHelpEditions } from '@/lib/help'
|
||||
import { getHelpArticle, getHelpEditionContent } from '@/lib/help'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import NotFound from '@/pages/NotFound'
|
||||
|
||||
@@ -32,8 +31,7 @@ const HelpDetailPage: FC = () => {
|
||||
|
||||
const edition = requestedEdition ?? languageMeta?.currentEdition
|
||||
|
||||
const editions = useMemo (() => getHelpEditions (identifier, language),
|
||||
[identifier, language])
|
||||
const editions = languageMeta?.publishedEditions ?? []
|
||||
|
||||
const index = edition ? editions.indexOf (edition) : -1
|
||||
const pred = index > 0 ? editions[index - 1] : undefined
|
||||
|
||||
@@ -37,7 +37,7 @@ const HelpDiffPage: FC = () => {
|
||||
const path = `/help/${ encodeURIComponent (identifier) }`
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<MainArea className="min-w-0 max-w-full">
|
||||
<Helmet>
|
||||
<title>{`ヘルプ 差分: ${ title } | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
@@ -7,10 +7,12 @@ import PrefetchLink from '@/components/PrefetchLink'
|
||||
import SortHeader from '@/components/SortHeader'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { getHelpSearchEntries } from '@/lib/help'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
|
||||
import { dateString, inputClass } from '@/lib/utils'
|
||||
|
||||
import type { FormEvent, FC } from 'react'
|
||||
@@ -25,6 +27,13 @@ const defaultDirection: Record<HelpOrderField, 'asc' | 'desc'> = {
|
||||
|
||||
const HelpSearchPage: FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams ()
|
||||
const pageValue = searchParams.get ('page') ?? '1'
|
||||
const pageNumber = Number (pageValue)
|
||||
const requestedPage =
|
||||
(/^[1-9]\d*$/.test (pageValue) && Number.isSafeInteger (pageNumber))
|
||||
? pageNumber : 1
|
||||
const limit = LIST_LIMIT_OPTIONS.find (
|
||||
value => String (value) === searchParams.get ('limit')) ?? 20
|
||||
const requestedOrder = searchParams.get ('order') ?? 'updated_at:desc'
|
||||
const order: HelpOrder =
|
||||
['title:asc', 'title:desc', 'updated_at:asc', 'updated_at:desc'].includes (requestedOrder)
|
||||
@@ -68,6 +77,17 @@ const HelpSearchPage: FC = () => {
|
||||
}))
|
||||
}, [entries, conditions, order])
|
||||
|
||||
const totalPages = Math.ceil (results.length / limit)
|
||||
const page = requestedPage <= Math.max (1, totalPages) ? requestedPage : 1
|
||||
const pageResults = results.slice ((page - 1) * limit, page * limit)
|
||||
|
||||
const handleLimit = (value: string) => {
|
||||
const next = new URLSearchParams (searchParams)
|
||||
next.set ('limit', value)
|
||||
next.set ('page', '1')
|
||||
setSearchParams (next)
|
||||
}
|
||||
|
||||
const handleSearch = (ev: FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault ()
|
||||
const next = new URLSearchParams (searchParams)
|
||||
@@ -79,17 +99,29 @@ const HelpSearchPage: FC = () => {
|
||||
next.set ('text', text)
|
||||
else
|
||||
next.delete ('text')
|
||||
next.set ('page', '1')
|
||||
setSearchParams (next)
|
||||
}
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<MainArea className="min-w-0 max-w-full">
|
||||
<Helmet>
|
||||
<title>{`ヘルプ | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<PageTitle>ヘルプ</PageTitle>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>ヘルプ</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<select value={limit} onChange={ev => handleLimit (ev.target.value)}
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground">
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>{value}</option>))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<form onSubmit={handleSearch} className="space-y-2">
|
||||
<FormField label="タイトル">
|
||||
{({ invalid }) => (
|
||||
@@ -125,43 +157,48 @@ const HelpSearchPage: FC = () => {
|
||||
: (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2 text-sm" aria-live="polite">{results.length} 件</p>
|
||||
<table className="table-auto w-full border-collapse">
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left">
|
||||
<SortHeader<HelpOrderField>
|
||||
by="title"
|
||||
label="タイトル"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left">
|
||||
<SortHeader<HelpOrderField>
|
||||
by="updated_at"
|
||||
label="最終更新"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map (entry => {
|
||||
const updatedAt = new Date(entry.edition.replace (/\..*$/, ''))
|
||||
<div className="min-w-0 max-w-full overflow-x-auto">
|
||||
<table className="table-auto w-full min-w-[640px] whitespace-nowrap
|
||||
border-collapse text-gray-900 dark:text-gray-100">
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left">
|
||||
<SortHeader<HelpOrderField>
|
||||
by="title"
|
||||
label="タイトル"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left">
|
||||
<SortHeader<HelpOrderField>
|
||||
by="updated_at"
|
||||
label="有効開始年月日"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pageResults.map (entry => {
|
||||
const updatedAt = new Date (entry.edition.replace (/\..*$/, ''))
|
||||
|
||||
return (
|
||||
<tr key={entry.identifier}
|
||||
className="even:bg-gray-100 dark:even:bg-gray-700">
|
||||
<td className="p-2">
|
||||
<PrefetchLink
|
||||
to={`/help/${encodeURIComponent (entry.identifier)}`}>
|
||||
{entry.title}
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2">{dateString (updatedAt, 'hour')}</td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
return (
|
||||
<tr key={entry.identifier}
|
||||
className="even:bg-gray-100 text-gray-900
|
||||
dark:even:bg-gray-700 dark:text-gray-100">
|
||||
<td className="p-2">
|
||||
<PrefetchLink
|
||||
to={`/help/${encodeURIComponent (entry.identifier)}`}>
|
||||
{entry.title}
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2">{dateString (updatedAt, 'hour')}</td>
|
||||
</tr>)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{totalPages > 0 && <Pagination page={page} totalPages={totalPages}/>}
|
||||
{results.length === 0 && <p className="mt-4">該当する記事がありません.</p>}
|
||||
</div>))}
|
||||
</MainArea>)
|
||||
|
||||
@@ -11,7 +11,7 @@ import TabGroup, { Tab } from '@/components/common/TabGroup'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||
import { getHelpChanges } from '@/lib/help'
|
||||
import { getHelpTitleIndex } from '@/lib/help'
|
||||
import { fetchPosts } from '@/lib/posts'
|
||||
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchTagByName } from '@/lib/tags'
|
||||
@@ -49,17 +49,15 @@ const WikiDetailPage: FC = () => {
|
||||
const lookupTitle = title.trim ().normalize ('NFKC').toLowerCase ().replace (/^ヘルプ:/, '')
|
||||
|
||||
void queryClient.fetchQuery ({
|
||||
queryKey: helpKeys.changes ('ja'),
|
||||
queryFn: () => getHelpChanges ('ja'),
|
||||
staleTime: Infinity }).then (changes => {
|
||||
queryKey: helpKeys.titles ('ja'),
|
||||
queryFn: () => getHelpTitleIndex ('ja'),
|
||||
staleTime: Infinity }).then (index => {
|
||||
if (!(active))
|
||||
return
|
||||
|
||||
const identifiers = new Set (changes
|
||||
.filter (entry => entry.title.trim ().normalize ('NFKC').toLowerCase () === lookupTitle)
|
||||
.map (entry => entry.identifier))
|
||||
if (identifiers.size === 1)
|
||||
navigate (`/help/${ encodeURIComponent ([...identifiers][0]) }`, { replace: true })
|
||||
const identifiers = Object.hasOwn (index, lookupTitle) ? index[lookupTitle] : []
|
||||
if (identifiers.length === 1)
|
||||
navigate (`/help/${ encodeURIComponent (identifiers[0]) }`, { replace: true })
|
||||
}).catch (() => {
|
||||
// A failed Help lookup leaves the original missing Wiki URL in place.
|
||||
})
|
||||
|
||||
ベンダーファイル
+28
@@ -1 +1,29 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module 'virtual:help/articles' {
|
||||
const articles: Record<string, import ('./lib/help').ArticleMeta>
|
||||
export default articles
|
||||
}
|
||||
|
||||
declare module 'virtual:help/search' {
|
||||
const search: Record<string, import ('./lib/help').HelpSearchEntry[]>
|
||||
export default search
|
||||
}
|
||||
|
||||
declare module 'virtual:help/changes' {
|
||||
const changes: Record<string, import ('./lib/help').HelpChange[]>
|
||||
export default changes
|
||||
}
|
||||
|
||||
declare module 'virtual:help/titles' {
|
||||
const titles: Record<string, import ('./lib/help').HelpTitleIndex>
|
||||
export default titles
|
||||
}
|
||||
|
||||
declare module 'virtual:help/loaders' {
|
||||
export const components: Record<string,
|
||||
() => Promise<{ default: import ('react').ComponentType }>>
|
||||
export const sources: Record<string, () => Promise<{ default: string }>>
|
||||
export const texts: Record<string,
|
||||
() => Promise<{ default: import ('./lib/help').HelpEditionText }>>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import path from 'path'
|
||||
import remarkGFM from 'remark-gfm'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import { helpAssets } from './scripts/help-assets'
|
||||
|
||||
const mdxPlugin = mdx ({ providerImportSource: '@/mdx-components',
|
||||
remarkPlugins: [remarkGFM] })
|
||||
const originalTransform = mdxPlugin.transform
|
||||
@@ -20,7 +22,7 @@ if (typeof originalTransform === 'function')
|
||||
}
|
||||
|
||||
export default defineConfig ({
|
||||
plugins: [mdxPlugin, react ()],
|
||||
plugins: [helpAssets (), mdxPlugin, react ()],
|
||||
resolve: { alias: { '@': path.resolve (__dirname, './src') } },
|
||||
server: { host: true,
|
||||
port: 5173,
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする