Skip to content

Commit

Permalink
feat: export scan function (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
ignatiusmb committed Mar 13, 2024
1 parent 6ce509d commit 6313259
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 47 deletions.
87 changes: 56 additions & 31 deletions workspace/aubade/src/compass/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
};
}
Expand Down
19 changes: 15 additions & 4 deletions workspace/aubade/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileChunk | DirChunk>;
}

export interface Metadata {
Expand Down
45 changes: 35 additions & 10 deletions workspace/content/10-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,26 +162,49 @@ export function visit<Output>(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<T extends 'all' | 'files' | 'directories'>(
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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions workspace/website/src/lib/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand All @@ -25,7 +25,8 @@ export const DATA = {
content: marker.render(content),
};
},
(items) => chain(items, { base: '/docs/' }),
);

return chain(items, { base: '/docs/' });
},
};

0 comments on commit 6313259

Please sign in to comment.