refactor: cached and reloaded file-index

This commit is contained in:
hypercross 2026-03-13 15:50:50 +08:00
parent 2e04934881
commit 27bc2fc747
4 changed files with 107 additions and 134 deletions

View File

@ -5,6 +5,7 @@ import {loadElementSrc, resolvePath} from "../utils/path";
import {createStore} from "solid-js/store"; import {createStore} from "solid-js/store";
import {RunnerOptions} from "../../yarn-spinner/runtime/runner"; import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
import {getTtrpgFunctions} from "./ttrpgRunner"; import {getTtrpgFunctions} from "./ttrpgRunner";
import { getIndexedData } from "../../data-loader/file-index";
type YarnSpinnerStore = { type YarnSpinnerStore = {
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[], dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
@ -43,8 +44,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
startAt: props.start, startAt: props.start,
}); });
const {commands, functions} = getTtrpgFunctions(runner); const {commands, functions} = getTtrpgFunctions(runner);
runner.registerFunctions(functions);
runner.registerCommands(commands); runner.registerCommands(commands);
runner.registerFunctions(functions);
return runner; return runner;
} catch (error) { } catch (error) {
console.error('Failed to initialize YarnRunner:', error); console.error('Failed to initialize YarnRunner:', error);
@ -77,7 +78,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
const processRunnerOutput = () => { const processRunnerOutput = () => {
const runner = store.runnerInstance; const runner = store.runnerInstance;
if(!runner)return; if(!runner)return;
const result = runner.currentResult; const result = runner.currentResult;
if (!result) return; if (!result) return;
@ -100,7 +101,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
}); });
processRunnerOutput(); processRunnerOutput();
}; };
return { return {
store, store,
advance, advance,
@ -108,22 +109,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
} }
} }
// 缓存已加载的 yarn 文件内容
const yarnCache = new Map<string, string>();
// 加载 yarn 文件内容
async function loadYarnFile(path: string): Promise<string> {
if (yarnCache.has(path)) {
return yarnCache.get(path)!;
}
const response = await fetch(path);
const content = await response.text();
yarnCache.set(path, content);
return content;
}
// 加载多个 yarn 文件并拼接 // 加载多个 yarn 文件并拼接
async function loadYarnFiles(paths: string[]): Promise<string> { async function loadYarnFiles(paths: string[]): Promise<string> {
const contents = await Promise.all(paths.map(path => loadYarnFile(path))); const contents = await Promise.all(paths.map(path => getIndexedData(path)));
return contents.join('\n'); return contents.join('\n');
} }

View File

@ -1,10 +1,6 @@
import { parse } from 'csv-parse/browser/esm/sync'; import { parse } from 'csv-parse/browser/esm/sync';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { getIndexedData } from '../../data-loader/file-index';
/**
* CSV
*/
const csvCache = new Map<string, Record<string, string>[]>();
/** /**
* front matter * front matter
@ -19,7 +15,7 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
// 分割内容 // 分割内容
const parts = content.split(/(?:^|\n)---\s*\n/); const parts = content.split(/(?:^|\n)---\s*\n/);
// 至少需要三个部分空字符串、front matter、剩余内容 // 至少需要三个部分空字符串、front matter、剩余内容
if (parts.length < 3) { if (parts.length < 3) {
return { remainingContent: content }; return { remainingContent: content };
@ -29,10 +25,10 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
// 解析 YAML front matter // 解析 YAML front matter
const frontmatterStr = parts[1].trim(); const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as JSONObject; const frontmatter = yaml.load(frontmatterStr) as JSONObject;
// 剩余内容是第三部分及之后的所有内容 // 剩余内容是第三部分及之后的所有内容
const remainingContent = parts.slice(2).join('---\n').trimStart(); const remainingContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, remainingContent }; return { frontmatter, remainingContent };
} catch (error) { } catch (error) {
console.warn('Failed to parse front matter:', error); console.warn('Failed to parse front matter:', error);
@ -45,16 +41,12 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
* @template T Record<string, string> * @template T Record<string, string>
*/ */
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> { export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
if (csvCache.has(path)) { // 尝试从索引获取
return csvCache.get(path)! as CSV<T>; const content = await getIndexedData(path);
}
const response = await fetch(path);
const content = await response.text();
// 解析 front matter // 解析 front matter
const { frontmatter, remainingContent } = parseFrontMatter(content); const { frontmatter, remainingContent } = parseFrontMatter(content);
const records = parse(remainingContent, { const records = parse(remainingContent, {
columns: true, columns: true,
comment: '#', comment: '#',
@ -72,8 +64,6 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
} }
} }
csvResult.sourcePath = path; csvResult.sourcePath = path;
csvCache.set(path, result);
return csvResult; return csvResult;
} }
@ -100,4 +90,4 @@ export function processVariables<T extends JSONObject> (body: string, currentRow
} }
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || ''; return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
} }

View File

@ -0,0 +1,88 @@
/**
*
*
*/
type FileIndex = Record<string, string>;
let fileIndex: FileIndex | null = null;
let indexLoadPromise: Promise<void> | null = null;
/**
*
* CLI JSON Dev 使 webpackContext .md
*/
function ensureIndexLoaded(): Promise<void> {
if (indexLoadPromise) return indexLoadPromise;
indexLoadPromise = (async () => {
// 尝试 CLI 环境:从 /__FILE_INDEX.json 加载
try {
const response = await fetch("/__FILE_INDEX.json");
if (response.ok) {
const index = await response.json();
fileIndex = { ...fileIndex, ...index };
return;
}
} catch (e) {
// CLI 索引不可用时尝试 dev 环境
}
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载 .md, .csv, .yarn 文件
try {
const context = import.meta.webpackContext("../../content", {
recursive: true,
regExp: /\.md|\.yarn|\.csv$/i,
});
const keys = context.keys();
const index: FileIndex = {};
for (const key of keys) {
// context 返回的是模块,需要访问其 default 导出raw-loader 处理后的内容)
const module = context(key) as { default?: string } | string;
const content = typeof module === "string" ? module : module.default ?? "";
const normalizedPath = "/content" + key.slice(1);
index[normalizedPath] = content;
}
fileIndex = { ...fileIndex, ...index };
} catch (e) {
// webpackContext 不可用时忽略
}
})();
return indexLoadPromise;
}
/**
*
*/
export async function getIndexedData(path: string): Promise<string> {
await ensureIndexLoaded();
if (fileIndex && fileIndex[path]) {
return fileIndex[path];
}
const res = await fetch(path);
const content = await res.text();
fileIndex = fileIndex || {};
fileIndex[path] = content;
return content;
}
/**
*
*/
export async function getPathsByExtension(ext: string): Promise<string[]> {
await ensureIndexLoaded();
if (!fileIndex) return [];
const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
return Object.keys(fileIndex).filter(path =>
path.toLowerCase().endsWith(normalizedExt.toLowerCase())
);
}
/**
*
*/
export function clearIndex(): void {
fileIndex = null;
indexLoadPromise = null;
}

View File

@ -1,83 +1,19 @@
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc"; import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
import {getIndexedData, getPathsByExtension} from "./file-index";
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection }; export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
let dataIndex: Record<string, string> | null = null;
let indexLoadPromise: Promise<void> | null = null;
/**
*
*/
function getIndexedData(path: string): string | null {
if (dataIndex && dataIndex[path]) {
return dataIndex[path];
}
return null;
}
/**
*
*/
export function getIndexedPaths(): string[] {
if (!dataIndex) return [];
return Object.keys(dataIndex);
}
/**
*
*/
export function ensureIndexLoaded(): Promise<void> {
if (indexLoadPromise) return indexLoadPromise;
indexLoadPromise = (async () => {
// 尝试 CLI 环境:从 /__CONTENT_INDEX.json 加载
try {
const response = await fetch("/__CONTENT_INDEX.json");
if (response.ok) {
const index = await response.json();
dataIndex = { ...dataIndex, ...index };
return;
}
} catch (e) {
// CLI 索引不可用时尝试 dev 环境
}
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载
try {
const context = import.meta.webpackContext("../../content", {
recursive: true,
regExp: /\.md$/,
});
const keys = context.keys();
const index: Record<string, string> = {};
for (const key of keys) {
// context 返回的是模块,需要访问其 default 导出raw-loader 处理后的内容)
const module = context(key) as { default?: string } | string;
const content = typeof module === "string" ? module : module.default ?? "";
const normalizedPath = "/content" + key.slice(1);
index[normalizedPath] = content;
}
dataIndex = { ...dataIndex, ...index };
} catch (e) {
// webpackContext 不可用时忽略
}
})();
return indexLoadPromise;
}
/** /**
* + * +
* .md
*/ */
export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> { export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> {
await ensureIndexLoaded(); const mdPaths = await getPathsByExtension("md");
const fileTree = buildFileTree(mdPaths);
const paths = getIndexedPaths();
const fileTree = buildFileTree(paths);
const pathHeadings: Record<string, TocNode[]> = {}; const pathHeadings: Record<string, TocNode[]> = {};
for (const path of paths) { for (const path of mdPaths) {
const content = getIndexedData(path); const content = await getIndexedData(path);
if (content) { if (content) {
pathHeadings[path] = extractHeadings(content); pathHeadings[path] = extractHeadings(content);
} }
@ -85,31 +21,3 @@ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeading
return { fileTree, pathHeadings }; return { fileTree, pathHeadings };
} }
/**
*
* @param path
* @returns
*/
export async function fetchData(path: string): Promise<string> {
await ensureIndexLoaded();
// 首先尝试从索引获取
const indexedData = getIndexedData(path);
if (indexedData) {
return indexedData;
}
// 索引不存在时,使用 fetch 加载
if (dataIndex !== null) throw new Error(`no data in index: ${path}`);
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Failed to fetch: ${path}`);
}
return await response.text();
} catch (error) {
console.error("fetchData error:", error);
throw error;
}
}