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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
declare const defaultSchemaFileResolved: {
|
declare const defaultSchemaFileResolved: {
|
||||||
schema: {
|
schema: {
|
||||||
parse: (mod) => any;
|
parse: (mod) => any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export declare const contentMap: {
|
export declare const contentMap: {
|
||||||
// GENERATED_CONTENT_MAP_ENTRIES
|
// GENERATED_CONTENT_MAP_ENTRIES
|
||||||
};
|
};
|
||||||
export declare const schemaMap: {
|
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) =>
|
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 } };
|
const defaultSchemaFileResolved = { schema: { parse: (mod) => mod } };
|
||||||
/** Used to stub out `schemaMap` entries that don't have a `~schema.ts` file */
|
/** Used to stub out `schemaMap` entries that don't have a `~schema.ts` file */
|
||||||
const defaultSchemaFile = (/** @type {string} */ collection) =>
|
const defaultSchemaFile = (/** @type {string} */ collection) =>
|
||||||
new Promise((/** @type {(value: typeof defaultSchemaFileResolved) => void} */ resolve) => {
|
new Promise((/** @type {(value: typeof defaultSchemaFileResolved) => void} */ resolve) => {
|
||||||
console.warn(NO_SCHEMA_MSG(collection));
|
console.warn(NO_SCHEMA_MSG(collection));
|
||||||
resolve(defaultSchemaFileResolved);
|
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 = {
|
export const contentMap = {
|
||||||
// GENERATED_CONTENT_MAP_ENTRIES
|
// GENERATED_CONTENT_MAP_ENTRIES
|
||||||
};
|
};
|
||||||
|
|
||||||
export const schemaMap = {
|
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",
|
"types": "./config.d.ts",
|
||||||
"default": "./config.mjs"
|
"default": "./config.mjs"
|
||||||
},
|
},
|
||||||
|
"./content-internals": "./dist/content-internals/index.js",
|
||||||
"./app": "./dist/core/app/index.js",
|
"./app": "./dist/core/app/index.js",
|
||||||
"./app/node": "./dist/core/app/node.js",
|
"./app/node": "./dist/core/app/node.js",
|
||||||
"./client/*": "./dist/runtime/client/*",
|
"./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 id = path.relative(contentDir.pathname, filePath);
|
||||||
const slug = entryKey.replace(/\.mdx?$/, '');
|
const slug = entryKey.replace(/\.mdx?$/, '');
|
||||||
const body = await fs.readFile(filePath, 'utf-8');
|
const body = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, matter: rawData } = parseFrontmatter(body, filePath);
|
const { data, matter } = parseFrontmatter(body, filePath);
|
||||||
return [entryKey, { id, slug, body, data, rawData }];
|
return [entryKey, { id, slug, body, data, rawData: matter ?? '' }];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue