shelving
Version:
Toolkit for using data in JavaScript.
133 lines (132 loc) • 5.67 kB
JavaScript
import { walkElements } from "../util/element.js";
import { requirePath } from "../util/path.js";
import { Extractor } from "./Extractor.js";
import { ModuleExtractor } from "./ModuleExtractor.js";
/** Default source-file extensions used when resolving exports back to source elements. */
const DEFAULT_EXTENSIONS = ["ts", "tsx", "js", "jsx"];
/**
* Extractor that reads a `package.json` and produces a flat tree of modules — one `kind: "module"`
* `DocumentationElement` per export entry, in declaration order.
* - Static export keys (e.g. `"./api"`, `"./firestore/client"`) become one module each.
* - Wildcard export keys (e.g. `"./util/*"`) expand against the source tree — one module per matching child file (with
* an extension in `extensions`) or subdirectory.
* - The `"."` root export is skipped — its content is the root tree element itself.
* - Throws if a static export key has no matching source element in the tree.
*/
export class PackageExtractor extends Extractor {
_tree;
_extensions;
_module;
_base;
constructor({ tree, extensions = DEFAULT_EXTENSIONS, module = new ModuleExtractor(), base }) {
super();
this._tree = tree;
this._extensions = extensions;
this._module = module;
this._base = base;
}
async extract(packageJson) {
const pkgPath = requirePath(packageJson, this._base, this.extract);
const pkg = (await Bun.file(pkgPath).json());
const exports = pkg.exports ?? {};
const modules = [];
for (const key of Object.keys(exports)) {
if (key === ".")
continue;
const subpath = key.startsWith("./") ? key.slice(2) : key;
if (subpath.includes("*")) {
modules.push(...this._expandWildcard(subpath));
}
else {
const source = this._resolve(subpath);
if (!source)
throw new Error(`PackageExtractor: export "${key}" has no matching source in the tree`);
modules.push(this._module.extract({ name: subpath, source }));
}
}
const tree = this._tree;
return {
type: "tree-directory",
key: pkg.name ?? tree.key,
props: {
source: tree.props.source,
name: pkg.name ?? tree.props.name,
title: pkg.name ?? tree.props.title,
description: pkg.description ?? tree.props.description,
content: tree.props.content,
children: modules,
},
};
}
/** Resolve a static export subpath (e.g. `"firestore/client"`) to a file or directory element in the tree. */
_resolve(subpath) {
const segments = subpath.split("/");
let current = this._tree;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i] ?? "";
const isLast = i === segments.length - 1;
let found;
for (const child of walkElements(current.props.children)) {
const treeChild = child;
if (treeChild.type === "tree-directory" && treeChild.key === segment) {
found = treeChild;
break;
}
if (isLast && treeChild.type === "tree-file") {
if (this._extensions.some(ext => treeChild.key === `${segment}.${ext}`)) {
found = treeChild;
break;
}
}
}
if (!found)
return undefined;
current = found;
}
return current;
}
/** Expand a wildcard export subpath (e.g. `"util/*"`) into one module per matching child. */
_expandWildcard(subpath) {
const wildcardIndex = subpath.indexOf("*");
const prefix = subpath.slice(0, wildcardIndex);
const suffix = subpath.slice(wildcardIndex + 1);
if (suffix)
throw new Error(`PackageExtractor: wildcard exports with a suffix are not supported ("${subpath}")`);
if (subpath.indexOf("*", wildcardIndex + 1) >= 0)
throw new Error(`PackageExtractor: multiple wildcards not supported ("${subpath}")`);
// The parent directory of the wildcard.
const prefixPath = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
let parent;
if (!prefixPath) {
parent = this._tree;
}
else {
const resolved = this._resolve(prefixPath);
if (!resolved || resolved.type !== "tree-directory") {
throw new Error(`PackageExtractor: wildcard parent "${prefixPath}" did not resolve to a directory`);
}
parent = resolved;
}
// One module per qualifying child of the parent directory.
const modules = [];
for (const child of walkElements(parent.props.children)) {
const treeChild = child;
let stem;
if (treeChild.type === "tree-directory") {
stem = treeChild.key;
}
else if (treeChild.type === "tree-file") {
for (const ext of this._extensions) {
if (treeChild.key.endsWith(`.${ext}`)) {
stem = treeChild.key.slice(0, -(ext.length + 1));
break;
}
}
}
if (!stem)
continue;
modules.push(this._module.extract({ name: `${prefix}${stem}`, source: treeChild }));
}
return modules;
}
}