diff --git a/engine/decofile/fsFolder.ts b/engine/decofile/fsFolder.ts index 4e65c7795..be964ba21 100644 --- a/engine/decofile/fsFolder.ts +++ b/engine/decofile/fsFolder.ts @@ -15,6 +15,19 @@ import type { } from "./provider.ts"; import type { VersionedDecofile } from "./realtime.ts"; +interface BaseMetadata { + blockType: string; + __resolveType: string; + usedBy: string[]; +} + +interface PageMetadata extends BaseMetadata { + name: string; + path: string; +} + +type Metadata = BaseMetadata | PageMetadata; + export const parseBlockId = (filename: string) => decodeURIComponent(filename.slice(0, filename.length - ".json".length)); @@ -26,7 +39,10 @@ const inferBlockType = (resolveType: string, knownBlockTypes: Set) => { return blockType; }; -const inferMetadata = (content: unknown, knownBlockTypes: Set) => { +const inferMetadata = ( + content: unknown, + knownBlockTypes: Set, +): Metadata | null => { try { const { __resolveType, name, path } = content as Record; const blockType = inferBlockType(__resolveType, knownBlockTypes); @@ -41,24 +57,76 @@ const inferMetadata = (content: unknown, knownBlockTypes: Set) => { path: path, blockType, __resolveType, + usedBy: [], }; } return { blockType, __resolveType, + usedBy: [], }; - // TODO @gimenes: when the json is wrong, we should somehow resolve to a standard block that talks well to the admin so the user can fix it somehow } catch { return null; } }; +/** + * Recursively maps block references in an object and updates the `usedBy` property of the corresponding blocks. + * + * @param obj - The object to map block references in. + * @param blocks - A map of block paths to block metadata. + * @param currentPath - The current path of the object being processed. + * @param isRoot - Indicates whether the current object is the root object. + */ +/** + * Recursively maps block references in an object and updates the `usedBy` property of the corresponding blocks. + * + * @param obj - The object to inspect for block references. + * @param blockMetadata - A map of block paths to block metadata. + * @param blockPathByName - A map of block names to their corresponding file paths. + * @param currentPath - The path of the current block being processed. + * @param isRoot - Indicates whether the current object is the root object. + */ +const mapBlockReferences = ( + obj: unknown, + blockMetadata: Map, + blockPathByName: Map, + currentPath: string, + isRoot = true +): void => { + if (typeof obj !== 'object' || obj === null) return; + + if (Array.isArray(obj)) { + for (const item of obj) { + mapBlockReferences(item, blockMetadata, blockPathByName, currentPath, false); + } + return; + } + + for (const [key, value] of Object.entries(obj)) { + if (key === '__resolveType' && !isRoot) { + const blockName = value.split('/').pop(); + if (blockName) { + const referencedBlockPath = blockPathByName.get(blockName); + if (referencedBlockPath && referencedBlockPath !== currentPath) { + const blockMeta = blockMetadata.get(referencedBlockPath); + if (blockMeta && !blockMeta.usedBy.includes(currentPath)) { + blockMeta.usedBy.push(currentPath); + } + } + } + } else if (typeof value === 'object' && value !== null) { + mapBlockReferences(value, blockMetadata, blockPathByName, currentPath, false); + } + } +}; + /** Syncs FileSystem Metadata with Storage metadata */ export const genMetadata = async () => { try { const knownBlockTypes = new Set(getBlocks().map((x) => x.type)); - const paths = []; + const blockPaths: string[] = []; const walker = walk(join(DECO_FOLDER, BLOCKS_FOLDER), { includeDirs: false, @@ -67,11 +135,11 @@ export const genMetadata = async () => { }); for await (const entry of walker) { - paths.push(entry.path); + blockPaths.push(entry.path); } - const entries = await Promise.all( - paths.map(async (path) => + const blockEntries = await Promise.all( + blockPaths.map(async (path) => [ `/${path.replaceAll("\\", "/")}`, JSON.parse(await Deno.readTextFile(path)), @@ -79,13 +147,28 @@ export const genMetadata = async () => { ), ); - const metadata = Object.fromEntries(entries.map(( - [path, content], - ) => [path, inferMetadata(content, knownBlockTypes)])); + const blockMetadata: Map = new Map(); + const blockPathByName = new Map(); + + for (const [path, content] of blockEntries) { + const meta = inferMetadata(content, knownBlockTypes); + if (meta !== null) { + blockMetadata.set(path, meta); + const blockName = parseBlockId(basename(path)); + blockPathByName.set(blockName, path); + } + } - const pathname = join(Deno.cwd(), METADATA_PATH); - await ensureFile(pathname); - await Deno.writeTextFile(pathname, JSON.stringify(metadata)); + for (const [path, content] of blockEntries) { + mapBlockReferences(content, blockMetadata, blockPathByName, path); + } + + const metadataPath = join(Deno.cwd(), METADATA_PATH); + await ensureFile(metadataPath); + await Deno.writeTextFile( + metadataPath, + JSON.stringify(Object.fromEntries(blockMetadata)), + ); } catch (error) { console.error("Error while auto-generating blocks.json", error); }