172 lines
3.8 KiB
TypeScript
172 lines
3.8 KiB
TypeScript
import { Glob } from "bun";
|
|
import { type LoadedFile, loadFileContent } from "./files";
|
|
import { type UnknownRecord, autoParseEntry } from "./records";
|
|
import { z } from "zod";
|
|
import { MuseError } from "#errors";
|
|
|
|
export const SourceSchema = z.union([
|
|
z.object({
|
|
file: z.string(),
|
|
}),
|
|
z.object({
|
|
url: z.string().url(),
|
|
}),
|
|
]);
|
|
|
|
export type SourceShorthand = z.infer<typeof SourceSchema>;
|
|
|
|
export function sourceShorthandToSource(
|
|
shorthand: SourceShorthand,
|
|
): MuseSource {
|
|
if ("file" in shorthand) {
|
|
return {
|
|
location: shorthand.file,
|
|
type: "file",
|
|
};
|
|
}
|
|
|
|
if ("url" in shorthand) {
|
|
return {
|
|
location: shorthand.url,
|
|
type: "url",
|
|
};
|
|
}
|
|
|
|
throw new MuseError("Invalid source shorthand");
|
|
}
|
|
|
|
export type SourceType = "file" | "url";
|
|
|
|
/** A descriptor of a location to load into a Muse binding */
|
|
export interface MuseSource {
|
|
location: string;
|
|
type: SourceType;
|
|
}
|
|
|
|
interface SourceOptions {
|
|
cwd: string;
|
|
contentKey: string;
|
|
ignore: string[];
|
|
}
|
|
|
|
/** A single loaded entry from a Muse source */
|
|
export interface MuseEntry<MetaShape = UnknownRecord> {
|
|
_raw: string;
|
|
location: string;
|
|
data: Record<string, unknown>;
|
|
meta: MetaShape;
|
|
source: MuseSource;
|
|
}
|
|
|
|
export async function parseMuseFile(
|
|
rawFilePath: string,
|
|
{ contentKey = "content" }: SourceOptions,
|
|
): Promise<MuseEntry[]> {
|
|
const file = await loadFileContent(rawFilePath);
|
|
const entries = autoParseEntry(file, contentKey);
|
|
|
|
const partial = {
|
|
_raw: file.content,
|
|
source: {
|
|
location: file.filePath,
|
|
type: "file" as SourceType,
|
|
},
|
|
location: file.filePath,
|
|
meta: {},
|
|
};
|
|
|
|
return entries.map((data) => ({
|
|
...partial,
|
|
data,
|
|
}));
|
|
}
|
|
|
|
async function loadFromFileSource(
|
|
source: MuseSource,
|
|
options: SourceOptions,
|
|
): Promise<MuseEntry[]> {
|
|
const paths = Array.from(
|
|
new Glob(source.location).scanSync({
|
|
cwd: options.cwd,
|
|
absolute: true,
|
|
followSymlinks: true,
|
|
onlyFiles: true,
|
|
}),
|
|
);
|
|
|
|
const filteredPaths = paths.filter((path) => !options.ignore.includes(path));
|
|
|
|
const entries: MuseEntry[] = [];
|
|
for (const filePath of filteredPaths) {
|
|
const fileEntries = await parseMuseFile(filePath, options);
|
|
entries.push(...fileEntries);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function getFileExtensionFromURL(url: string): string | null {
|
|
const parsedUrl = new URL(url);
|
|
|
|
const pathname = parsedUrl.pathname;
|
|
const filename = pathname.substring(pathname.lastIndexOf("/") + 1);
|
|
const extension = filename.substring(filename.lastIndexOf(".") + 1);
|
|
|
|
return extension === filename ? null : extension;
|
|
}
|
|
|
|
async function loadFromURLSource(
|
|
source: MuseSource,
|
|
options: SourceOptions,
|
|
): Promise<MuseEntry[]> {
|
|
const { contentKey = "content" } = options;
|
|
const response = await fetch(source.location);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch URL: ${source.location}`);
|
|
}
|
|
const content = await response.text();
|
|
const mimeType = response.headers.get("Content-Type") || "unknown";
|
|
|
|
const parseType = getFileExtensionFromURL(source.location) ?? "unknown";
|
|
|
|
const loadedFile: LoadedFile = {
|
|
content,
|
|
filePath: source.location,
|
|
fileType: parseType,
|
|
dirname: "",
|
|
basename: "",
|
|
mimeType,
|
|
};
|
|
|
|
const entries = autoParseEntry(loadedFile, contentKey);
|
|
|
|
const partial = {
|
|
_raw: content,
|
|
source: {
|
|
location: source.location,
|
|
type: "url" as SourceType,
|
|
},
|
|
location: source.location,
|
|
meta: {},
|
|
};
|
|
|
|
return entries.map((data) => ({
|
|
...partial,
|
|
data,
|
|
}));
|
|
}
|
|
|
|
export async function loadFromSource(
|
|
source: MuseSource,
|
|
options: SourceOptions,
|
|
): Promise<MuseEntry[]> {
|
|
switch (source.type) {
|
|
case "file":
|
|
return loadFromFileSource(source, options);
|
|
case "url":
|
|
return loadFromURLSource(source, options);
|
|
default:
|
|
throw new Error(`Unsupported source type: ${source.type}`);
|
|
}
|
|
}
|