Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate render function and merge content layer types #11579

Merged
merged 2 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions packages/astro/src/content/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ export const CONTENT_TYPES_FILE = 'types.d.ts';

export const DATA_STORE_FILE = 'data-store.json';
export const ASSET_IMPORTS_FILE = 'assets.mjs';

export const CONTENT_LAYER_TYPE = 'experimental_content';
4 changes: 2 additions & 2 deletions packages/astro/src/content/data-store.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { promises as fs, type PathLike, existsSync } from 'fs';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import type { MarkdownHeading } from '@astrojs/markdown-remark';
import * as devalue from 'devalue';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
const SAVE_DEBOUNCE_MS = 500;

export interface RenderedContent {
Expand Down
28 changes: 21 additions & 7 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
render as serverRender,
unescapeHTML,
} from '../runtime/server/index.js';
import { IMAGE_IMPORT_PREFIX } from './consts.js';
import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from './consts.js';
import { type DataEntry, globalDataStore } from './data-store.js';
import type { ContentLookupMap } from './utils.js';
type LazyImport = () => Promise<any>;
Expand All @@ -26,6 +26,13 @@ type CollectionToEntryMap = Record<string, GlobResult>;
type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;

export function defineCollection(config: any) {
if (
('loader' in config && config.type !== CONTENT_LAYER_TYPE) ||
(config.type === CONTENT_LAYER_TYPE && !('loader' in config))
) {
// TODO: when this moves out of experimental, we will set the type automatically
throw new AstroError(AstroErrorData.ContentLayerTypeError);
ascorbic marked this conversation as resolved.
Show resolved Hide resolved
}
if (!config.type) config.type = 'content';
return config;
}
Expand Down Expand Up @@ -62,7 +69,7 @@ export function createGetCollection({
}) {
return async function getCollection(collection: string, filter?: (entry: any) => unknown) {
const store = await globalDataStore.get();
let type: 'content' | 'data' | 'experimental_data' | 'experimental_content';
let type: 'content' | 'data';
if (collection in contentCollectionToEntryMap) {
type = 'content';
} else if (collection in dataCollectionToEntryMap) {
Expand All @@ -80,7 +87,6 @@ export function createGetCollection({
...entry,
data,
collection,
render: () => renderEntry(entry),
});
}
return result;
Expand Down Expand Up @@ -167,7 +173,6 @@ export function createGetEntryBySlug({
return {
...entry,
collection,
render: () => renderEntry(entry),
};
}
if (!collectionNames.has(collection)) {
Expand Down Expand Up @@ -201,7 +206,10 @@ export function createGetEntryBySlug({
export function createGetDataEntryById({
getEntryImport,
collectionNames,
}: { getEntryImport: GetEntryImport; collectionNames: Set<string> }) {
}: {
getEntryImport: GetEntryImport;
collectionNames: Set<string>;
}) {
return async function getDataEntryById(collection: string, id: string) {
const store = await globalDataStore.get();

Expand Down Expand Up @@ -307,7 +315,6 @@ export function createGetEntry({
return {
...entry,
collection,
render: () => renderEntry(entry),
} as DataEntryResult | ContentEntryResult;
}

Expand Down Expand Up @@ -433,7 +440,14 @@ function updateImageReferencesInData<T extends Record<string, unknown>>(
});
}

async function renderEntry(entry?: DataEntry) {
export async function renderEntry(
entry?: DataEntry | { render: () => Promise<{ Content: AstroComponentFactory }> }
ascorbic marked this conversation as resolved.
Show resolved Hide resolved
) {
if (entry && 'render' in entry) {
// This is an old content collection entry, so we use its render method
return entry.render();
}

const html =
entry?.rendered?.metadata?.imagePaths?.length && entry.filePath
? await updateImageReferencesInBody(entry.rendered.html, entry.filePath)
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/content/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { FSWatcher } from 'vite';
import xxhash from 'xxhash-wasm';
import type { AstroSettings } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import { ASSET_IMPORTS_FILE, DATA_STORE_FILE } from './consts.js';
import { ASSET_IMPORTS_FILE, CONTENT_LAYER_TYPE, DATA_STORE_FILE } from './consts.js';
import type { DataStore } from './data-store.js';
import type { LoaderContext } from './loaders/types.js';
import { getEntryDataAndImages, globalContentConfigObserver, posixRelative } from './utils.js';
Expand Down Expand Up @@ -49,7 +49,7 @@ export async function syncContentLayer({

await Promise.all(
Object.entries(contentConfig.config.collections).map(async ([name, collection]) => {
if (collection.type !== 'experimental_data' && collection.type !== 'experimental_content') {
if (collection.type !== CONTENT_LAYER_TYPE) {
return;
}

Expand Down
17 changes: 7 additions & 10 deletions packages/astro/src/content/types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AstroError } from '../core/errors/errors.js';
import { AstroErrorData } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
import { isRelativePath } from '../core/path.js';
import { CONTENT_LAYER_TYPE } from './consts.js';
import { CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js';
import {
type ContentConfig,
Expand Down Expand Up @@ -45,7 +46,7 @@ type CollectionEntryMap = {
entries: Record<string, ContentEntryMetadata>;
}
| {
type: 'data' | 'experimental_content' | 'experimental_data';
type: 'data' | typeof CONTENT_LAYER_TYPE;
entries: Record<string, DataEntryMetadata>;
};
};
Expand Down Expand Up @@ -366,7 +367,7 @@ async function typeForCollection<T extends keyof ContentConfig['collections']>(
}

if (
(collection?.type === 'experimental_data' || collection?.type === 'experimental_content') &&
collection?.type === CONTENT_LAYER_TYPE &&
typeof collection.loader === 'object' &&
collection.loader.schema
) {
Expand Down Expand Up @@ -426,8 +427,7 @@ async function writeContentFiles({
if (
collectionConfig?.type &&
collection.type !== 'unknown' &&
collectionConfig.type !== 'experimental_data' &&
collectionConfig.type !== 'experimental_content' &&
collectionConfig.type !== CONTENT_LAYER_TYPE &&
collection.type !== collectionConfig.type
) {
viteServer.hot.send({
Expand All @@ -450,7 +450,7 @@ async function writeContentFiles({
});
return;
}
const resolvedType: 'content' | 'data' | 'experimental_data' | 'experimental_content' =
const resolvedType =
collection.type === 'unknown'
? // Add empty / unknown collections to the data type map by default
// This ensures `getCollection('empty-collection')` doesn't raise a type error
Expand All @@ -477,11 +477,8 @@ async function writeContentFiles({
}
contentTypesStr += `};\n`;
break;
case 'experimental_data':
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n collection: ${collectionKey};\n data: ${dataType};\n}>;\n`;
break;
case 'experimental_content':
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n collection: ${collectionKey};\n data: ${dataType};\n render(): Promise<ContentLayerRenderer>;\n rendered?: RenderedContent \n}>;\n`;
case CONTENT_LAYER_TYPE:
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n collection: ${collectionKey};\n data: ${dataType};\n rendered?: RenderedContent \n}>;\n`;
break;
case 'data':
if (collectionEntryKeys.length === 0) {
Expand Down
20 changes: 9 additions & 11 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import type {
import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js';
import { isYAMLException } from '../core/errors/utils.js';
import type { Logger } from '../core/logger/core.js';
import { CONTENT_FLAGS, IMAGE_IMPORT_PREFIX, PROPAGATED_ASSET_FLAG } from './consts.js';
import {
CONTENT_FLAGS,
CONTENT_LAYER_TYPE,
IMAGE_IMPORT_PREFIX,
PROPAGATED_ASSET_FLAG,
} from './consts.js';
import { createImage } from './runtime-assets.js';
/**
* Amap from a collection + slug to the local file path.
Expand All @@ -36,7 +41,7 @@ export const collectionConfigParser = z.union([
schema: z.any().optional(),
}),
z.object({
type: z.union([z.literal('experimental_data'), z.literal('experimental_content')]),
type: z.literal(CONTENT_LAYER_TYPE),
schema: z.any().optional(),
loader: z.union([
z.function().returns(
Expand Down Expand Up @@ -135,11 +140,7 @@ export async function getEntryDataAndImages<
pluginContext?: PluginContext
): Promise<{ data: TOutputData; imageImports: Array<string> }> {
let data: TOutputData;
if (
collectionConfig.type === 'data' ||
collectionConfig.type === 'experimental_data' ||
collectionConfig.type === 'experimental_content'
) {
if (collectionConfig.type === 'data' || collectionConfig.type === CONTENT_LAYER_TYPE) {
data = entry.unvalidatedData as TOutputData;
} else {
const { slug, ...unvalidatedData } = entry.unvalidatedData;
Expand All @@ -155,10 +156,7 @@ export async function getEntryDataAndImages<
schema = schema({
image: createImage(pluginContext, shouldEmitFile, entry._internal.filePath),
});
} else if (
collectionConfig.type === 'experimental_data' ||
collectionConfig.type === 'experimental_content'
) {
} else if (collectionConfig.type === CONTENT_LAYER_TYPE) {
schema = schema({
image: () =>
z.string().transform((val) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/dev/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { performance } from 'perf_hooks';
import { gt, major, minor, patch } from 'semver';
import type * as vite from 'vite';
import type { AstroInlineConfig } from '../../@types/astro.js';
import { DATA_STORE_FILE } from '../../content/consts.js';
import { DataStore, globalDataStore } from '../../content/data-store.js';
import { attachContentServerListeners } from '../../content/index.js';
import { syncContentLayer } from '../../content/sync.js';
Expand All @@ -19,7 +20,6 @@ import {
fetchLatestAstroVersion,
shouldCheckForUpdates,
} from './update-check.js';
import { DATA_STORE_FILE } from '../../content/consts.js';

export interface DevServer {
address: AddressInfo;
Expand Down
15 changes: 15 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,21 @@ export const GenerateContentTypesError = {
`\`astro sync\` command failed to generate content collection types: ${errorMessage}`,
hint: 'Check your `src/content/config.*` file for typos.',
} satisfies ErrorData;

/**
* @docs
* @description
* Invalid content layer collection definition
* @see
* - [Content collections documentation](https://docs.astro.build/en/guides/content-collections/)
*/
export const ContentLayerTypeError = {
name: 'ContentLayerTypeError',
title: 'Invalid content layer collection definition',
message: 'Collections that use the content layer must have a `loader` defined and `type` set to `experimental_content`',
hint: 'Check your collection definitions in `src/content/config.*`.',
} satisfies ErrorData;

/**
* @docs
* @kind heading
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 || typeof e === 'object' && (e as AstroError).type === 'AstroError';
ascorbic marked this conversation as resolved.
Show resolved Hide resolved
}

export class AstroError extends Error {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { dim } from 'kleur/colors';
import { type HMRPayload, createServer } from 'vite';
import type { AstroConfig, AstroInlineConfig, AstroSettings } from '../../@types/astro.js';
import { getPackage } from '../../cli/install-package.js';
import { DATA_STORE_FILE } from '../../content/consts.js';
import { DataStore, globalDataStore } from '../../content/data-store.js';
import { createContentTypesGenerator } from '../../content/index.js';
import { syncContentLayer } from '../../content/sync.js';
Expand All @@ -30,7 +31,6 @@ import type { Logger } from '../logger/core.js';
import { formatErrorMessage } from '../messages.js';
import { ensureProcessNodeEnv } from '../util.js';
import { setUpEnvTs } from './setup-env-ts.js';
import { DATA_STORE_FILE } from '../../content/consts.js';

export type SyncOptions = {
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/templates/content/module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
createReference,
} from 'astro/content/runtime';

export { defineCollection } from 'astro/content/runtime';
export { defineCollection, renderEntry as render } from 'astro/content/runtime';
export { z } from 'astro/zod';

const contentDir = '@@CONTENT_DIR@@';
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/templates/content/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ declare module 'astro:content' {
remarkPluginFrontmatter: Record<string, any>;
}>;
}
interface ContentLayerRenderer {
interface ContentLayerRenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
}

Expand Down Expand Up @@ -109,6 +109,10 @@ declare module 'astro:content' {
}[]
): Promise<CollectionEntry<C>[]>;

export function render<C extends keyof AnyEntryMap>(
entry: AnyEntryMap[C][string]
): Promise<ContentLayerRenderResult>;

export function reference<C extends keyof AnyEntryMap>(
collection: C
): import('astro/zod').ZodEffects<
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/test/content-layer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { promises as fs } from 'node:fs';
import { sep } from 'node:path';
import { sep as posixSep } from 'node:path/posix';
import { after, before, describe, it } from 'node:test';
import { loadFixture } from './test-utils.js';
import * as devalue from 'devalue';
import { loadFixture } from './test-utils.js';
describe('Content Layer', () => {
/** @type {import("./test-utils.js").Fixture} */
let fixture;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { loader } from '../loaders/post-loader.js';
import { fileURLToPath } from 'node:url';

const blog = defineCollection({
type: 'experimental_data',
type: 'experimental_content',
loader: loader({ url: 'https://jsonplaceholder.typicode.com/posts' }),
});

const dogs = defineCollection({
type: 'experimental_data',
type: 'experimental_content',
loader: file('src/data/dogs.json'),
schema: z.object({
breed: z.string(),
Expand All @@ -22,7 +22,7 @@ const dogs = defineCollection({
});

const cats = defineCollection({
type: 'experimental_data',
type: 'experimental_content',
loader: async function () {
return [
{
Expand Down Expand Up @@ -92,7 +92,7 @@ const numbers = defineCollection({
});

const increment = defineCollection({
type: 'experimental_data',
type: 'experimental_content',
loader: {
name: 'increment-loader',
load: async ({ store }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection, getEntry } from "astro:content"
import { getCollection, getEntry, render } from "astro:content"
import { Image } from "astro:assets"
export const getStaticPaths = (async () => {
Expand All @@ -22,7 +22,7 @@ export const getStaticPaths = (async () => {
const { craft } = Astro.props as any
let cat = craft.data.cat ? await getEntry(craft.data.cat) : undefined
const { Content } = await craft.render()
const { Content } = await render(craft)
---
<meta charset="utf-8">
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/types/content.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ declare module 'astro:content' {
export type SchemaContext = { image: ImageFunction };

type ContentLayerConfig<S extends BaseSchema, TData extends { id: string } = { id: string }> = {
type: 'experimental_data' | 'experimental_content';
type: 'experimental_content';
schema?: S | ((context: SchemaContext) => S);
loader: import('astro/loaders').Loader | (() => Array<TData> | Promise<Array<TData>>);
};
Expand Down
Loading