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:
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user