refactor: cached and reloaded file-index
This commit is contained in:
parent
2e04934881
commit
27bc2fc747
|
|
@ -5,6 +5,7 @@ import {loadElementSrc, resolvePath} from "../utils/path";
|
|||
import {createStore} from "solid-js/store";
|
||||
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
||||
import {getTtrpgFunctions} from "./ttrpgRunner";
|
||||
import { getIndexedData } from "../../data-loader/file-index";
|
||||
|
||||
type YarnSpinnerStore = {
|
||||
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
||||
|
|
@ -43,8 +44,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
|||
startAt: props.start,
|
||||
});
|
||||
const {commands, functions} = getTtrpgFunctions(runner);
|
||||
runner.registerFunctions(functions);
|
||||
runner.registerCommands(commands);
|
||||
runner.registerFunctions(functions);
|
||||
return runner;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize YarnRunner:', error);
|
||||
|
|
@ -77,7 +78,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
|||
const processRunnerOutput = () => {
|
||||
const runner = store.runnerInstance;
|
||||
if(!runner)return;
|
||||
|
||||
|
||||
const result = runner.currentResult;
|
||||
if (!result) return;
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
|||
});
|
||||
processRunnerOutput();
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
store,
|
||||
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 文件并拼接
|
||||
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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { parse } from 'csv-parse/browser/esm/sync';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
/**
|
||||
* 全局缓存已加载的 CSV 内容
|
||||
*/
|
||||
const csvCache = new Map<string, Record<string, string>[]>();
|
||||
import { getIndexedData } from '../../data-loader/file-index';
|
||||
|
||||
/**
|
||||
* 解析 front matter
|
||||
|
|
@ -19,7 +15,7 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
|||
|
||||
// 分割内容
|
||||
const parts = content.split(/(?:^|\n)---\s*\n/);
|
||||
|
||||
|
||||
// 至少需要三个部分:空字符串、front matter、剩余内容
|
||||
if (parts.length < 3) {
|
||||
return { remainingContent: content };
|
||||
|
|
@ -29,10 +25,10 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
|||
// 解析 YAML front matter
|
||||
const frontmatterStr = parts[1].trim();
|
||||
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
|
||||
|
||||
|
||||
// 剩余内容是第三部分及之后的所有内容
|
||||
const remainingContent = parts.slice(2).join('---\n').trimStart();
|
||||
|
||||
|
||||
return { frontmatter, remainingContent };
|
||||
} catch (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>
|
||||
*/
|
||||
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
|
||||
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
||||
|
||||
|
||||
const records = parse(remainingContent, {
|
||||
columns: true,
|
||||
comment: '#',
|
||||
|
|
@ -72,8 +64,6 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
|
|||
}
|
||||
}
|
||||
csvResult.sourcePath = path;
|
||||
|
||||
csvCache.set(path, result);
|
||||
return csvResult;
|
||||
}
|
||||
|
||||
|
|
@ -100,4 +90,4 @@ export function processVariables<T extends JSONObject> (body: string, currentRow
|
|||
}
|
||||
|
||||
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,83 +1,19 @@
|
|||
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
|
||||
import {getIndexedData, getPathsByExtension} from "./file-index";
|
||||
|
||||
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[]> }> {
|
||||
await ensureIndexLoaded();
|
||||
|
||||
const paths = getIndexedPaths();
|
||||
const fileTree = buildFileTree(paths);
|
||||
const mdPaths = await getPathsByExtension("md");
|
||||
const fileTree = buildFileTree(mdPaths);
|
||||
const pathHeadings: Record<string, TocNode[]> = {};
|
||||
|
||||
for (const path of paths) {
|
||||
const content = getIndexedData(path);
|
||||
for (const path of mdPaths) {
|
||||
const content = await getIndexedData(path);
|
||||
if (content) {
|
||||
pathHeadings[path] = extractHeadings(content);
|
||||
}
|
||||
|
|
@ -85,31 +21,3 @@ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeading
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue