feat: add fetchContent to .astro

This commit is contained in:
bholmesdev 2022-10-24 11:02:28 -04:00
parent e018648545
commit a2218fa905
5 changed files with 123 additions and 14 deletions

View file

@ -1,13 +1,24 @@
import { z } from 'zod';
declare const defaultSchemaFileResolved: {
schema: {
parse: (mod) => any;
};
schema: {
parse: (mod) => any;
};
};
export declare const contentMap: {
// GENERATED_CONTENT_MAP_ENTRIES
// GENERATED_CONTENT_MAP_ENTRIES
};
export declare const schemaMap: {
// GENERATED_SCHEMA_MAP_ENTRIES
// GENERATED_SCHEMA_MAP_ENTRIES
};
export declare function fetchContentByEntry<
C extends keyof typeof contentMap,
E extends keyof typeof contentMap[C]
>(collection: C, entryKey: E): Promise<typeof contentMap[C][E]>;
export declare function fetchContent<
C extends keyof typeof contentMap,
E extends keyof typeof contentMap[C]
>(
collection: C,
filter?: (data: typeof contentMap[C][E]['data']) => boolean
): Promise<typeof contentMap[C][keyof typeof contentMap[C]][]>;

View file

@ -1,18 +1,89 @@
import { z } from 'zod';
import { getFrontmatterErrorLine, errorMap } from 'astro/content-internals';
const NO_SCHEMA_MSG = (/** @type {string} */ collection) =>
`${collection} does not have a ~schema file. We suggest adding one for type safety!`;
`${collection} does not have a ~schema file. We suggest adding one for type safety!`;
const defaultSchemaFileResolved = { schema: { parse: (mod) => mod } };
/** Used to stub out `schemaMap` entries that don't have a `~schema.ts` file */
const defaultSchemaFile = (/** @type {string} */ collection) =>
new Promise((/** @type {(value: typeof defaultSchemaFileResolved) => void} */ resolve) => {
console.warn(NO_SCHEMA_MSG(collection));
resolve(defaultSchemaFileResolved);
});
new Promise((/** @type {(value: typeof defaultSchemaFileResolved) => void} */ resolve) => {
console.warn(NO_SCHEMA_MSG(collection));
resolve(defaultSchemaFileResolved);
});
const getSchemaError = (collection) =>
new Error(`${collection}/~schema needs a named \`schema\` export.`);
async function parseEntryData(
/** @type {string} */ collection,
/** @type {string} */ entryKey,
/** @type {{ data: any; rawData: string; }} */ unparsedEntry,
/** @type {{ schemaMap: any }} */ { schemaMap }
) {
const defineSchemaResult = await schemaMap[collection];
if (!defineSchemaResult) throw getSchemaError(collection);
const { schema } = defineSchemaResult;
try {
return schema.parse(unparsedEntry.data, { errorMap });
} catch (e) {
if (e instanceof z.ZodError) {
const formattedError = new Error(
[
`Could not parse frontmatter in ${String(collection)}${String(entryKey)}`,
...e.errors.map((e) => e.message),
].join('\n')
);
formattedError.loc = {
file: 'TODO',
line: getFrontmatterErrorLine(unparsedEntry.rawData, String(e.errors[0].path[0])),
column: 1,
};
throw formattedError;
}
}
}
export const contentMap = {
// GENERATED_CONTENT_MAP_ENTRIES
// GENERATED_CONTENT_MAP_ENTRIES
};
export const schemaMap = {
// GENERATED_SCHEMA_MAP_ENTRIES
// GENERATED_SCHEMA_MAP_ENTRIES
};
export async function fetchContentByEntry(
/** @type {string} */ collection,
/** @type {string} */ entryKey
) {
const entry = contentMap[collection][entryKey];
return {
id: entry.id,
slug: entry.slug,
body: entry.body,
data: await parseEntryData(collection, entryKey, entry, { schemaMap }),
};
}
export async function fetchContent(
/** @type {string} */ collection,
/** @type {undefined | (() => boolean)} */ filter
) {
const entries = Promise.all(
Object.entries(contentMap[collection]).map(async ([key, entry]) => {
return {
id: entry.id,
slug: entry.slug,
body: entry.body,
data: await parseEntryData(collection, key, entry, { schemaMap }),
};
})
);
if (typeof filter === 'function') {
return (await entries).filter(filter);
} else {
return entries;
}
}

View file

@ -42,6 +42,7 @@
"types": "./config.d.ts",
"default": "./config.mjs"
},
"./content-internals": "./dist/content-internals/index.js",
"./app": "./dist/core/app/index.js",
"./app/node": "./dist/core/app/node.js",
"./client/*": "./dist/runtime/client/*",

View file

@ -0,0 +1,26 @@
import { z } from 'zod';
const flattenPath = (path: (string | number)[]) => path.join('.');
export const errorMap: z.ZodErrorMap = (error, ctx) => {
if (error.code === 'invalid_type') {
const badKeyPath = JSON.stringify(flattenPath(error.path));
if (error.received === 'undefined') {
return { message: `${badKeyPath} is required.` };
} else {
return { message: `${badKeyPath} should be ${error.expected}, not ${error.received}.` };
}
}
return { message: ctx.defaultError };
};
// WARNING: MAXIMUM JANK AHEAD
export function getFrontmatterErrorLine(rawFrontmatter: string, frontmatterKey: string) {
console.log({ rawFrontmatter, frontmatterKey });
const indexOfFrontmatterKey = rawFrontmatter.indexOf(`\n${frontmatterKey}`);
if (indexOfFrontmatterKey === -1) return 0;
const frontmatterBeforeKey = rawFrontmatter.substring(0, indexOfFrontmatterKey + 1);
const numNewlinesBeforeKey = frontmatterBeforeKey.split('\n').length;
return numNewlinesBeforeKey;
}

View file

@ -190,8 +190,8 @@ async function getEntriesByCollection(
const id = path.relative(contentDir.pathname, filePath);
const slug = entryKey.replace(/\.mdx?$/, '');
const body = await fs.readFile(filePath, 'utf-8');
const { data, matter: rawData } = parseFrontmatter(body, filePath);
return [entryKey, { id, slug, body, data, rawData }];
const { data, matter } = parseFrontmatter(body, filePath);
return [entryKey, { id, slug, body, data, rawData: matter ?? '' }];
})
);
}