refactor(cli): centralize content registry building

Extract buildRegistryFromIndex and normalizePathKey from the CLI serve
command and the browser completions module into the shared
content-registry module. This ensures both paths produce identical
pathIndex/docContent, including the spark-injection pass that was
previously only applied in the CLI.
This commit is contained in:
hyper
2026-08-07 12:46:08 +08:00
parent e8aa7165cb
commit 0a68122efd
3 changed files with 63 additions and 33 deletions
+7 -24
View File
@@ -2,15 +2,15 @@ import type { ServeCommandHandler } from "../types.js";
import { createServer, Server, IncomingMessage, ServerResponse } from "http"; import { createServer, Server, IncomingMessage, ServerResponse } from "http";
import { readdirSync, statSync, readFileSync, existsSync } from "fs"; import { readdirSync, statSync, readFileSync, existsSync } from "fs";
import { createReadStream } from "fs"; import { createReadStream } from "fs";
import { join, resolve, extname, sep, relative, dirname } from "path"; import { join, resolve, extname, relative, dirname } from "path";
import { watch } from "chokidar"; import { watch } from "chokidar";
import { networkInterfaces } from "os"; import { networkInterfaces } from "os";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { createJournalServer } from "../journal.js"; import { createJournalServer } from "../journal.js";
import { import {
scanDoc, buildRegistryFromIndex,
normalizePathKey,
deriveCompletions, deriveCompletions,
injectSparkDirectives,
type ContentRegistry, type ContentRegistry,
} from "../content-registry.js"; } from "../content-registry.js";
import type { CompletionsPayload } from "../completions/types.js"; import type { CompletionsPayload } from "../completions/types.js";
@@ -86,7 +86,7 @@ function getBestIP(): string {
* 扫描目录内的 .md 等文件,构建内容注册表(路径索引 + 每文档内联内容) * 扫描目录内的 .md 等文件,构建内容注册表(路径索引 + 每文档内联内容)
*/ */
export function buildRegistry(dir: string): ContentRegistry { export function buildRegistry(dir: string): ContentRegistry {
const registry: ContentRegistry = { pathIndex: {}, docContent: {} }; const index: Record<string, string> = {};
function scan(currentPath: string, relativePath: string) { function scan(currentPath: string, relativePath: string) {
const entries = readdirSync(currentPath); const entries = readdirSync(currentPath);
@@ -96,7 +96,7 @@ export function buildRegistry(dir: string): ContentRegistry {
const fullPath = join(currentPath, entry); const fullPath = join(currentPath, entry);
const relPath = relativePath ? join(relativePath, entry) : entry; const relPath = relativePath ? join(relativePath, entry) : entry;
const normalizedRelPath = "/" + relPath.split(sep).join("/"); const normalizedRelPath = normalizePathKey(relPath);
const stats = statSync(fullPath); const stats = statSync(fullPath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
@@ -108,14 +108,7 @@ export function buildRegistry(dir: string): ContentRegistry {
entry.endsWith(".svg") entry.endsWith(".svg")
) { ) {
try { try {
const content = readFileSync(fullPath, "utf-8"); index[normalizedRelPath] = readFileSync(fullPath, "utf-8");
if (entry.endsWith(".md")) {
const result = scanDoc(content, normalizedRelPath);
registry.pathIndex[normalizedRelPath] = result.stripped;
registry.docContent[normalizedRelPath] = result.content;
} else {
registry.pathIndex[normalizedRelPath] = content;
}
} catch (e) { } catch (e) {
console.error(`读取文件失败:${fullPath}`, e); console.error(`读取文件失败:${fullPath}`, e);
} }
@@ -125,17 +118,7 @@ export function buildRegistry(dir: string): ContentRegistry {
scan(dir, ""); scan(dir, "");
// ---- Inject data-spark into real-file spark table directives ---- return buildRegistryFromIndex(index);
for (const [relPath, content] of Object.entries(registry.pathIndex)) {
if (!relPath.endsWith(".md")) continue;
registry.pathIndex[relPath] = injectSparkDirectives(
content,
relPath,
registry,
);
}
return registry;
} }
/** /**
+50
View File
@@ -63,6 +63,16 @@ export const EMPTY_REGISTRY: ContentRegistry = {
docContent: {}, docContent: {},
}; };
/**
* Normalize a file path into a stable registry key: forward slashes and a
* single leading `/`. Both the CLI filesystem walk and the browser folder
* scan funnel through this so identical files always share a key.
*/
export function normalizePathKey(path: string): string {
const normalized = path.split("\\").join("/");
return normalized.startsWith("/") ? normalized : `/${normalized}`;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Id / hash derivation (single source of truth) // Id / hash derivation (single source of truth)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -185,6 +195,46 @@ export function scanDoc(content: string, docPath: string): DocScanResult {
return { stripped: rewritten, content: contentStore }; return { stripped: rewritten, content: contentStore };
} }
/**
* Build a `ContentRegistry` from a raw `{ path: content }` index.
*
* Shared by the CLI (`buildRegistry` in `commands/serve.ts`) and the browser
* folder scan (`scanClientSide` in `components/journal/completions.ts`) so
* both modes produce identical `pathIndex`/`docContent` — including the
* `data-spark` injection pass that only the CLI used to run.
*
* Keys are normalized via `normalizePathKey`; `.md` files are run through
* `scanDoc` and the spark-injection pass, other extensions are stored raw.
*/
export function buildRegistryFromIndex(
index: Record<string, string>,
): ContentRegistry {
const registry: ContentRegistry = { pathIndex: {}, docContent: {} };
for (const [rawPath, content] of Object.entries(index)) {
const path = normalizePathKey(rawPath);
if (path.endsWith(".md")) {
const result = scanDoc(content, path);
registry.pathIndex[path] = result.stripped;
registry.docContent[path] = result.content;
} else {
registry.pathIndex[path] = content;
}
}
// Inject data-spark into real-file spark table directives (idempotent).
for (const [relPath, content] of Object.entries(registry.pathIndex)) {
if (!relPath.endsWith(".md")) continue;
registry.pathIndex[relPath] = injectSparkDirectives(
content,
relPath,
registry,
);
}
return registry;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Spark table coercion // Spark table coercion
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+6 -9
View File
@@ -16,7 +16,7 @@ import {
setInlineResolver, setInlineResolver,
} from "../../data-loader/file-index"; } from "../../data-loader/file-index";
import { import {
scanDoc, buildRegistryFromIndex,
deriveCompletions, deriveCompletions,
resolveInlineByPath, resolveInlineByPath,
type ContentRegistry, type ContentRegistry,
@@ -108,19 +108,16 @@ async function tryServerRegistry(): Promise<void> {
async function scanClientSide(): Promise<JournalCompletions> { async function scanClientSide(): Promise<JournalCompletions> {
const paths = await getPathsByExtension("md"); const paths = await getPathsByExtension("md");
// Build a registry from the in-memory file index, then derive completions // Load all .md content into a raw index, then build the registry through
// through the same shared pipeline as the CLI. // the same shared pipeline as the CLI (scanDoc + spark injection).
const registry: ContentRegistry = { pathIndex: {}, docContent: {} }; const index: Record<string, string> = {};
// First pass: load all .md content into the registry.
for (const filePath of paths) { for (const filePath of paths) {
const content = await getIndexedData(filePath); const content = await getIndexedData(filePath);
if (!content) continue; if (!content) continue;
const result = scanDoc(content, filePath); index[filePath] = content;
registry.pathIndex[filePath] = result.stripped;
registry.docContent[filePath] = result.content;
} }
const registry = buildRegistryFromIndex(index);
activeRegistry = registry; activeRegistry = registry;
return deriveCompletions(registry); return deriveCompletions(registry);
} }