From 6313259149d98afd069bb53c02e78dc1b2f03bdb Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Wed, 13 Mar 2024 16:49:08 +0700 Subject: [PATCH] feat: export `scan` function (#136) --- workspace/aubade/src/compass/index.js | 87 ++++++++++++++-------- workspace/aubade/src/types.d.ts | 19 ++++- workspace/content/10-modules.md | 45 ++++++++--- workspace/website/src/lib/content/index.ts | 5 +- 4 files changed, 109 insertions(+), 47 deletions(-) diff --git a/workspace/aubade/src/compass/index.js b/workspace/aubade/src/compass/index.js index 95756b9..2c5baa3 100644 --- a/workspace/aubade/src/compass/index.js +++ b/workspace/aubade/src/compass/index.js @@ -14,50 +14,75 @@ export function visit(entry) { } /** + * @template {'all' | 'files' | 'directories'} T + * @param {T} type * @param {string} entry - * @param {{ - * depth?: number; - * files?(path: string): boolean; - * }} [options] + * @returns {T extends 'all' + * ? import('../types.js').HydrateChunk['siblings'] : T extends 'files' + * ? import('../types.js').FileChunk[] : import('../types.js').DirChunk[]} */ -export function traverse(entry, { depth: level = 0, files = (v) => v.endsWith('.md') } = {}) { +export function scan(type, entry) { /** @type {import('../types.js').HydrateChunk['siblings']} */ - const tree = fs.readdirSync(entry).map((name) => { + const entries = []; + for (const name of fs.statSync(entry).isDirectory() ? fs.readdirSync(entry) : []) { const path = join(entry, name); - return { - /** @type {any} - trick TS to enable discriminated union */ - type: fs.statSync(path).isDirectory() ? 'directory' : 'file', + /** @type {any} - trick TS to enable discriminated union */ + const stat = fs.statSync(path).isDirectory() ? 'directory' : 'file'; + if (type === 'files' && stat === 'directory') continue; + if (type === 'directories' && stat === 'file') continue; + entries.push({ + type: stat, + path, breadcrumb: path.split(/[/\\]/).reverse(), get buffer() { - return this.type === 'file' ? fs.readFileSync(path) : void 0; + return stat === 'file' ? fs.readFileSync(path) : void 0; }, - }; - }); + }); + } + return /** @type {any} */ (entries); +} + +/** + * @param {string} entry + * @param {{ + * depth?: number; + * }} [options] + */ +export function traverse(entry, { depth: level = 0 } = {}) { + const entries = scan('files', entry); + for (const { path } of level ? scan('directories', entry) : []) { + entries.push(...traverse(path, { depth: level - 1 }).files); + } return { + files: entries, + /** - * @template {object} Output - * @template Transformed + * Hydrate `files` scanned on to the shelf with the `load` function. * + * @template {object} Output * @param {(chunk: import('../types.js').HydrateChunk) => undefined | Output} load - * @param {(items: Output[]) => Transformed} [transform] - * @returns {Transformed} + * @param {(path: string) => boolean} [files] filter item to process with `load` + * @returns {Output[]} */ - hydrate(load, transform = (v) => /** @type {Transformed} */ (v)) { - const backpack = tree.flatMap(({ type, breadcrumb, buffer }) => { - const path = [...breadcrumb].reverse().join('/'); - if (type === 'file') { - if (!files(path)) return []; - const siblings = tree.filter(({ breadcrumb: [name] }) => name !== breadcrumb[0]); - return load({ breadcrumb, buffer, marker, parse, siblings }) ?? []; - } else if (level !== 0) { - const depth = level < 0 ? level : level - 1; - return traverse(path, { depth, files }).hydrate(load); - } - return []; - }); - - return transform(/** @type {any} */ (backpack)); + hydrate(load, files = (v) => v.endsWith('.md')) { + const items = []; + for (const { path, breadcrumb, buffer } of entries) { + if (!files(path)) continue; + const item = load({ + breadcrumb, + buffer, + marker, + parse, + get siblings() { + const parent = breadcrumb.slice(1).reverse(); + const tree = scan('all', parent.join('/')); + return tree.filter(({ path: file }) => file !== path); + }, + }); + item && items.push(item); + } + return items; }, }; } diff --git a/workspace/aubade/src/types.d.ts b/workspace/aubade/src/types.d.ts index 2f75392..ac11cdd 100644 --- a/workspace/aubade/src/types.d.ts +++ b/workspace/aubade/src/types.d.ts @@ -7,15 +7,26 @@ export interface FrontMatter { [key: string]: Primitives | Primitives[] | FrontMatter | FrontMatter[]; } +export interface FileChunk { + type: 'file'; + path: string; + breadcrumb: string[]; + buffer: Buffer; +} + +export interface DirChunk { + type: 'directory'; + path: string; + breadcrumb: string[]; + buffer: undefined; +} + export interface HydrateChunk { breadcrumb: string[]; buffer: Buffer; marker: typeof marker; parse: typeof parse; - siblings: Array< - | { type: 'file'; breadcrumb: string[]; buffer: Buffer } - | { type: 'directory'; breadcrumb: string[]; buffer: undefined } - >; + siblings: Array; } export interface Metadata { diff --git a/workspace/content/10-modules.md b/workspace/content/10-modules.md index 73762ea..660d378 100644 --- a/workspace/content/10-modules.md +++ b/workspace/content/10-modules.md @@ -162,26 +162,49 @@ export function visit(entry: string): Output & { The first argument of `visit` is the source entry point. +### scan + +```typescript +interface FileChunk { + type: 'file'; + path: string; + breadcrumb: string[]; + buffer: Buffer; +} + +interface DirChunk { + type: 'directory'; + path: string; + breadcrumb: string[]; + buffer: undefined; +} + +export function scan( + type: T, + entry: string, +): FileChunk[] | DirChunk[]; +``` + +The first argument of `scan` is the type of item to scan, and the second argument is the entrypoint. It returns an array of `FileChunk` when `type` is `'files'`, `DirChunk` when `type` is `'directories'`, and both when `type` is `'all'`. The `entry` argument can be anything as long as it exists in the filesystem, though it would return an empty array if it's not a directory. + ### traverse ```typescript export function traverse( entry: string, - options: { + options?: { depth?: number; - files?(path: string): boolean; }, ): { + files: FileChunk[]; hydrate( load: (chunk: HydrateChunk) => undefined | Output, - transform?: (items: Output[]) => Transformed, - ): Transformed; + filter?: (path: string) => boolean, + ): Output[]; }; ``` -The first argument of `traverse` is the directory entrypoint, and the second argument is its `options`. It returns an object with the `hydrate` method that accepts a `load` callback and an optional `transform` callback function. - -The `files` property in `options` is an optional function that takes the full path of a file and returns a boolean. If the function returns `true`, the `load` callback will be called upon the file, else it will ignored and filtered out from the final output. +The first argument of `traverse` is the directory entrypoint, and the second argument is its `options`. It returns an object with the `hydrate` method that accepts a `load` callback and an optional `files` function to filter item to process into `load`, it takes the full path of a file and returns a boolean. If the function returns `true`, the `load` callback will be called upon the file, else it will ignored and filtered out from the final output. ``` content @@ -247,13 +270,15 @@ const data = traverse('content/reviews', { depth: -1 }).hydrate( ## /transform -This module provides a set of transformer function for the [`traverse(...).hydrate(..., /* transform */)` parameter](/docs/modules#compass-traverse). These function can be used in conjunction with each other, by utilizing the `pipe` function provided from the `'mauss'` package and re-exported by this module, you can do the following +This is a standalone module which provides a set of transformer function. They can be used in conjunction with each other by utilizing the `pipe` function provided from the `'mauss'` package and re-exported by this module, you can do the following ```typescript import { traverse } from 'aubade/compass'; -import { pipe } from 'aubade/transform'; +import { chain } from 'aubade/transform'; + +const items = traverse('path/to/content').hydrate(() => {}); -traverse('path/to/content').hydrate(() => {}, pipe(/* ... */)); +chain(items, { ... }); ``` ### chain diff --git a/workspace/website/src/lib/content/index.ts b/workspace/website/src/lib/content/index.ts index c3a6ca1..c7915f3 100644 --- a/workspace/website/src/lib/content/index.ts +++ b/workspace/website/src/lib/content/index.ts @@ -6,7 +6,7 @@ const ROOT = `${process.cwd()}/static/uploads`; export const DATA = { get 'docs/'() { - return traverse('../content').hydrate( + const items = traverse('../content').hydrate( ({ breadcrumb: [filename], buffer, marker, parse, siblings }) => { const { body, metadata } = parse(buffer.toString('utf-8')); @@ -25,7 +25,8 @@ export const DATA = { content: marker.render(content), }; }, - (items) => chain(items, { base: '/docs/' }), ); + + return chain(items, { base: '/docs/' }); }, };