feat: add fetchContent
to .astro
This commit is contained in:
parent
e018648545
commit
a2218fa905
5 changed files with 123 additions and 14 deletions
21
packages/astro/content-reference/content.d.ts
vendored
21
packages/astro/content-reference/content.d.ts
vendored
|
@ -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]][]>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/*",
|
||||
|
|
26
packages/astro/src/content-internals/index.ts
Normal file
26
packages/astro/src/content-internals/index.ts
Normal 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;
|
||||
}
|
|
@ -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 ?? '' }];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue