ヘルプを Wiki から分離 (#424) (#430)

画面上でのテストは完了.
移行にす早さが求められるので,準備ができたらマージする.

Reviewed-on: #430
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #430 でマージされました。
このコミットが含まれているのは:
2026-10-13 12:25:11 +09:00
committed by みてるぞ
コミット e44887cf15
85個のファイルの変更、4126行の追加、764行の削除
+37
ファイルの表示
@@ -0,0 +1,37 @@
import * as jsxRuntime from 'react/jsx-runtime'
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript'
import { useMDXComponents } from '@/mdx-components'
import viteConfig from '../../vite.config'
import type { ComponentType } from 'react'
type Transform = (source: string, id: string) => Promise<{ code: string } | null>
export const transformMdx = async (source: string, suffix = '.mdx') => {
const plugins = viteConfig.plugins?.flat () as { name: string; transform: Transform }[]
const plugin = plugins.find (entry => entry.name === '@mdx-js/rollup')
if (plugin == null)
throw new Error ('The application MDX compiler is not registered')
return plugin.transform.call (plugin, source, `/test/help-table${suffix}`)
}
export const compileMdx = async (source: string): Promise<ComponentType> => {
const result = await transformMdx (source)
if (result == null)
throw new Error ('MDX was not compiled')
const code = transpileModule (result.code, {
compilerOptions: { module: ModuleKind.CommonJS, target: ScriptTarget.ES2022 } }).outputText
const exports = { } as { default: ComponentType }
const requireFixture = (name: string) => {
if (name === 'react/jsx-runtime')
return jsxRuntime
if (name === '@/mdx-components')
return { useMDXComponents }
throw new Error (`Unexpected MDX dependency: ${name}`)
}
new Function ('require', 'exports', code) (requireFixture, exports)
return exports.default
}
+1
ファイルの表示
@@ -59,6 +59,7 @@ export const buildUser = (overrides: Partial<User> = {}): User => ({
})
export const buildWikiPage = (overrides: Partial<WikiPage> = {}): WikiPage => ({
resourceType: 'tag_description',
id: 1,
title: 'テストWiki',
deprecatedAt: null,
+126
ファイルの表示
@@ -0,0 +1,126 @@
import { createElement } from 'react'
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript'
import { vi } from 'vitest'
import source from '@/lib/help.ts?raw'
import type { ComponentType } from 'react'
import type * as Help from '@/lib/help'
export const editionRows = [
['guide', 'ja', '2026-10-01', '旧ホーム', '過去限定本文'],
['guide', 'ja', '2026-10-02', 'ホーム', 'タグを検索する'],
['guide', 'ja', '2026-10-02.2', 'ホーム第二版', '第二版本文'],
['guide', 'ja', '2026-10-02.3', 'ホーム第三版', '第三版本文'],
['guide', 'ja', '2026-10-02.10', 'ホーム第十版', 'タグを検索する 第十版本文'],
['guide', 'ja', '2026-10-02.11', '未公開ホーム', '未公開限定本文'],
['guide', 'en', '2026-10-02', 'Home', 'English body'],
['設定 手順', 'ja', '2026-10-03', '設定', 'タグを設定する']] as const
const compiled = transpileModule (source, {
compilerOptions: { module: ModuleKind.CommonJS, target: ScriptTarget.ES2022 } }).outputText
// Supply generated virtual modules, executing the actual runtime Help module.
export const createHelpFixture = () => {
const articles: Record<string, Help.ArticleMeta> = {
guide: { identifier: 'guide', canonicalLanguage: 'ja', languages: {
ja: { currentEdition: '2026-10-02.10', publishedEditions: [
'2026-10-01', '2026-10-02', '2026-10-02.2', '2026-10-02.3', '2026-10-02.10'] },
en: { currentEdition: '2026-10-02', publishedEditions: ['2026-10-02'] } } },
'設定 手順': { identifier: '設定 手順', canonicalLanguage: 'ja', languages: {
ja: { currentEdition: '2026-10-03', publishedEditions: ['2026-10-03'] } } } }
const published = editionRows.filter (([id, language, edition]) =>
articles[id].languages[language].publishedEditions.includes (edition))
const raw = Object.fromEntries (editionRows.map (([id, language, edition, title, body]) => [
`../../help/${id}/editions/${language}/${edition}.mdx`,
vi.fn (async () => `# ${title}\n\n${body}`)]))
const moduleLoads = Object.fromEntries (published.map (([id, language, edition, title, body]) => [
`../../help/${id}/editions/${language}/${edition}.mdx`,
vi.fn (async (): Promise<{ default: ComponentType }> => ({
default: () => createElement ('div', null,
createElement ('h1', null, title), createElement ('p', null, body)) }))]))
// Dynamic imports reuse a module namespace even when called more than once.
const caches: Record<string, { promise?: Promise<{ default: ComponentType }> }> = { }
const modules = Object.fromEntries (Object.entries (moduleLoads).map (([path, load]) => {
caches[path] = { }
return [path, vi.fn (() => caches[path].promise ??= load ())]
}))
const texts = Object.fromEntries (published.map (([id, language, edition, title, body]) => [
`${ id }/${ language }/${ edition }`,
vi.fn (async () => ({ default: { title, body: `\n${ body }` } }))]))
const components = Object.fromEntries (published.map (([id, language, edition]) => [
`${ id }/${ language }/${ edition }`,
modules[`../../help/${ id }/editions/${ language }/${ edition }.mdx`]]))
const sources = Object.fromEntries (published.map (([id, language, edition]) => [
`${ id }/${ language }/${ edition }`,
async () => ({ default: await raw[
`../../help/${ id }/editions/${ language }/${ edition }.mdx`] () })]))
const search: Record<string, Help.HelpSearchEntry[]> = { }
const changes: Record<string, Help.HelpChange[]> = { }
const titles: Record<string, Help.HelpTitleIndex> = { }
published.forEach (([id, language, edition, title, body]) => {
const editions = articles[id].languages[language].publishedEditions
const index = editions.indexOf (edition)
const searchRows = search[language] ??= []
if (articles[id].languages[language].currentEdition === edition)
searchRows.push ({ identifier: id, edition, title, body: `\n${ body }` })
const rows = changes[language] ??= []
rows.push ({ identifier: id, edition, title,
pred: editions[index - 1] ?? null,
change: index === 0 ? 'new' : 'update', date: edition.split ('.')[0] })
const lookup = (titles[language] ??= { })
const identifiers = lookup[title.trim ().normalize ('NFKC').toLowerCase ()] ??= []
if (!(identifiers.includes (id)))
identifiers.push (id)
})
Object.values (changes).forEach (rows => rows.sort ((a, b) => {
const [dateA, revisionA = '1'] = a.edition.split ('.')
const [dateB, revisionB = '1'] = b.edition.split ('.')
return dateB.localeCompare (dateA) || Number (revisionB) - Number (revisionA)
|| a.identifier.localeCompare (b.identifier)
}))
const assets = {
articles: { default: articles }, search: { default: search },
changes: { default: changes }, titles: { default: titles } }
const snapshot = JSON.stringify (assets)
const loaders = { components, sources, texts }
const loads = {
articles: vi.fn (() => assets.articles), search: vi.fn (() => assets.search),
changes: vi.fn (() => assets.changes), titles: vi.fn (() => assets.titles),
loaders: vi.fn (() => loaders) }
const api = { } as typeof Help
const requireFixture = (name: string) => {
const key = name.replace ('virtual:help/', '') as keyof typeof loads
if (Object.hasOwn (loads, key))
return loads[key] ()
throw new Error (`Unexpected Help dependency: ${name}`)
}
new Function ('require', 'exports', compiled) (requireFixture, api)
const reset = () => {
Object.assign (assets, JSON.parse (snapshot))
editionRows.forEach (([id, language, edition, title, body]) => {
const path = `../../help/${ id }/editions/${ language }/${ edition }.mdx`
raw[path].mockReset ().mockResolvedValue (`# ${ title }\n\n${ body }`)
})
published.forEach (([id, language, edition, title, body]) => {
const path = `../../help/${ id }/editions/${ language }/${ edition }.mdx`
caches[path].promise = undefined
moduleLoads[path].mockClear ()
modules[path].mockReset ().mockImplementation (() =>
caches[path].promise ??= moduleLoads[path] ())
texts[`${ id }/${ language }/${ edition }`].mockReset ().mockResolvedValue ({
default: { title, body: `\n${ body }` } })
})
loads.articles.mockReset ().mockImplementation (() => assets.articles)
loads.search.mockReset ().mockImplementation (() => assets.search)
loads.changes.mockReset ().mockImplementation (() => assets.changes)
loads.titles.mockReset ().mockImplementation (() => assets.titles)
loads.loaders.mockReset ().mockImplementation (() => loaders)
}
return { api, assets, loads, raw, modules, moduleLoads, texts, reset }
}
export const helpFixture = createHelpFixture ()