Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 73 additions & 13 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
render as serverRender,
unescapeHTML,
} from '../runtime/server/index.js';
import type { LiveDataEntry } from '../types/public/content.js';
import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX, LIVE_CONTENT_TYPE } from './consts.js';
import { type DataEntry, globalDataStore } from './data-store.js';
import type { LiveLoader } from './loaders/types.js';
Expand All @@ -30,40 +31,63 @@ type CollectionToEntryMap = Record<string, GlobResult>;
type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;
type LiveCollectionConfigMap = Record<
string,
{ loader: LiveLoader; type: typeof LIVE_CONTENT_TYPE }
{ loader: LiveLoader; type: typeof LIVE_CONTENT_TYPE; schema?: z.ZodType }
>;

export function getImporterFilename() {
// The 4th line in the stack trace should be the importer filename
const stackLine = new Error().stack?.split('\n')?.[3];
if (!stackLine) {
return null;
return undefined;
}
// Extract the relative path from the stack line
const match = /\/(src\/.*?):\d+:\d+/.exec(stackLine);
return match?.[1] ?? null;
return match?.[1] ?? undefined;
}

export function defineCollection(config: any) {
const isInLiveConfig = getImporterFilename()?.endsWith('/live.config.ts');

if (config.type === LIVE_CONTENT_TYPE) {
if (!isInLiveConfig) {
throw new AstroUserError(
`Collections with type "live" must be defined in a \`src/live.config.ts\` file. Check your collection definitions in ${getImporterFilename() ?? 'your content config file'}.`,
);
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
'Collections with type `live` must be defined in a `src/live.config.ts` file.',
getImporterFilename() ?? 'your content config file',
),
});
}
if (!config.loader) {
throw new AstroUserError(
`Collections that use the Live Content API must have a \`loader\` defined. Check your collection definitions in ${getImporterFilename() ?? 'your live content config file'}.`,
);
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
'Collections with type `live` must have a `loader` defined.',
getImporterFilename(),
),
});
}
if (config.schema) {
if (typeof config.schema === 'function') {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
'The schema cannot be a function for live collections. Please use a schema object instead.',
getImporterFilename(),
),
});
}
}
return config;
}
if (isInLiveConfig) {
throw new AstroUserError(
`Collections in a \`live.config.ts\` file must be defined with the type "live". Check your collection definitions.`,
);
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
'Collections in a `live.config.ts` file must have a type of `live`.',
getImporterFilename(),
),
});
}

if ('loader' in config) {
Expand Down Expand Up @@ -97,6 +121,28 @@ export function createCollectionToGlobResultMap({
return collectionToGlobResultMap;
}

async function parseLiveEntry(
entry: LiveDataEntry,
schema: z.ZodType,
collection: string,
): Promise<LiveDataEntry> {
const parsed = await schema.safeParseAsync(entry.data);
if (!parsed.success) {
throw new AstroError({
...AstroErrorData.InvalidContentEntryDataError,
message: AstroErrorData.InvalidContentEntryDataError.message(
collection,
entry.id,
parsed.error,
),
});
}
return {
...entry,
data: parsed.data,
};
}

export function createGetCollection({
contentCollectionToEntryMap,
dataCollectionToEntryMap,
Expand Down Expand Up @@ -130,6 +176,14 @@ export function createGetCollection({
liveCollections[collection].loader as LiveLoader<any, any, Record<string, unknown>>
)?.loadCollection?.(context);

const { schema } = liveCollections[collection];

if (schema) {
response.entries = await Promise.all(
response.entries.map((entry) => parseLiveEntry(entry, schema, collection)),
);
}

return {
...response,
collection,
Expand Down Expand Up @@ -394,7 +448,7 @@ export function createGetEntry({
filter: typeof lookup === 'string' ? { id: lookup } : lookup,
};

const entry = await (
let entry = await (
liveCollections[collection].loader as LiveLoader<
Record<string, unknown>,
Record<string, unknown>
Expand All @@ -404,6 +458,12 @@ export function createGetEntry({
if (!entry) {
return;
}

const { schema } = liveCollections[collection];
if (schema) {
entry = await parseLiveEntry(entry, schema, collection);
}

return {
...entry,
collection,
Expand Down
7 changes: 1 addition & 6 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,12 +762,7 @@ function searchConfig(
}

function searchLiveConfig(fs: typeof fsMod, srcDir: URL): { exists: boolean; url: URL } {
const paths = [
'live.config.mjs',
'live.config.js',
'live.config.mts',
'live.config.ts',
];
const paths = ['live.config.mjs', 'live.config.js', 'live.config.mts', 'live.config.ts'];
return search(fs, srcDir, paths);
}

Expand Down
19 changes: 19 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,25 @@ export const ContentEntryDataError = {
hint: 'See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas.',
} satisfies ErrorData;

/**
* @docs
* @message
* **Example error message:**<br/>
* The schema cannot be a function for live collections. Please use a schema object instead. Check your collection definitions in your live content config file.
* @description
* Error in live content config.
* @see
* - [Experimental live content](https://astro.build/en/reference/experimental-flags/live-content/)
*/

export const LiveContentConfigError = {
name: 'LiveContentConfigError',
title: 'Error in live content config.',
message: (error: string, filename?: string) =>
`${error} Check your collection definitions in ${filename ?? 'your live content config file'}.`,
hint: 'See https://docs.astro.build/en/reference/experimental-flags/live-content/ for more information on live content collections.',
} satisfies ErrorData;

/**
* @docs
* @message
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type ErrorTypes =
| 'AggregateError';

export function isAstroError(e: unknown): e is AstroError {
return e instanceof AstroError;
return e instanceof AstroError || AstroError.is(e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the AstroError is thrown inside the content config it's not recognising it as an instance of the AstroError class, so isn't giving the nicely-formatted error messages

}

export class AstroError extends Error {
Expand Down
9 changes: 6 additions & 3 deletions packages/astro/templates/content/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,12 @@ declare module 'astro:content' {
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];

type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> = ExtractDataType<
LiveContentConfig['collections'][C]['loader']
>;
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/test/fixtures/live-loaders/src/live.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineCollection, z, type BaseSchema } from 'astro:content';
import { defineCollection, z } from 'astro:content';
import type { LiveLoader } from 'astro/loaders';

type Entry = {
Expand Down Expand Up @@ -26,6 +26,10 @@ const loader: LiveLoader<Entry, { id: keyof typeof entries }> = {
const liveStuff = defineCollection({
type: 'live',
loader,
schema: z.object({
title: z.string(),
age: z.number().optional(),
}),
});

export const collections = { liveStuff };