Skip to content
Draft
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
12 changes: 12 additions & 0 deletions packages/astro/src/content/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ export function defineCollection<S extends BaseSchema>(
);
}
config.type = CONTENT_LAYER_TYPE;
} else {
// Collection without loader = using v4 backwards compat mode
if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
console.warn(
`⚠️ DEPRECATION: Content collection defined without a loader (${importerFilename ?? 'unknown'})

The v4 content collections API is deprecated and will be removed in Astro 7.
Please migrate to the Content Layer API with a loader.

Migration guide: https://docs.astro.build/en/guides/migrate-to-astro/upgrade-to/v5/#content-collections`,
);
}
}
if (!config.type) config.type = 'content';
return config;
Expand Down
55 changes: 51 additions & 4 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,14 @@ export function createGetCollection({
getRenderEntryImport,
cacheEntriesByCollection,
liveCollections,
contentCollectionsStrict,
}: {
contentCollectionToEntryMap: CollectionToEntryMap;
dataCollectionToEntryMap: CollectionToEntryMap;
getRenderEntryImport: GetEntryImport;
cacheEntriesByCollection: Map<string, any[]>;
liveCollections: LiveCollectionConfigMap;
contentCollectionsStrict: boolean;
}) {
return async function getCollection(
collection: string,
Expand All @@ -145,6 +147,18 @@ export function createGetCollection({
} else if (collection in dataCollectionToEntryMap) {
type = 'data';
} else if (store.hasCollection(collection)) {
if (contentCollectionsStrict) {
// Check if this is a v4 collection by looking for legacyId in first entry
const firstEntry = store.values<DataEntry>(collection)[0];
if (firstEntry?.legacyId) {
throw new AstroError({
name: 'ContentCollectionError',
title: 'Deprecated v4 API',
message: `getCollection() is not available for v4 content collections. Use getCollection() with Content Layer API collections instead for collection "${collection}".`,
hint: 'See https://docs.astro.build/en/guides/migrate-to-astro/upgrade-to/v5/#content-collections for migration information.',
});
}
}
// @ts-expect-error virtual module
const { default: imageAssetMap } = await import('astro:asset-imports');

Expand All @@ -159,7 +173,7 @@ export function createGetCollection({
};

if (entry.legacyId) {
entry = emulateLegacyEntry(entry);
entry = emulateLegacyEntry(entry, contentCollectionsStrict);
}

if (hasFilter && !filter(entry)) {
Expand Down Expand Up @@ -233,17 +247,26 @@ export function createGetEntryBySlug({
getRenderEntryImport,
collectionNames,
getEntry,
contentCollectionsStrict,
}: {
getEntryImport: GetEntryImport;
getRenderEntryImport: GetEntryImport;
collectionNames: Set<string>;
getEntry: ReturnType<typeof createGetEntry>;
contentCollectionsStrict: boolean;
}) {
return async function getEntryBySlug(collection: string, slug: string) {
const store = await globalDataStore.get();

if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
if (contentCollectionsStrict) {
throw new AstroError({
name: 'ContentCollectionError',
title: 'Deprecated v4 API',
message: `getEntryBySlug() is deprecated for collections using the Content Layer API. Use getEntry() instead for collection "${collection}".`,
});
}
const entry = await getEntry(collection, slug);
if (entry && 'slug' in entry) {
return entry;
Expand Down Expand Up @@ -285,16 +308,25 @@ export function createGetDataEntryById({
getEntryImport,
collectionNames,
getEntry,
contentCollectionsStrict,
}: {
getEntryImport: GetEntryImport;
collectionNames: Set<string>;
getEntry: ReturnType<typeof createGetEntry>;
contentCollectionsStrict: boolean;
}) {
return async function getDataEntryById(collection: string, id: string) {
const store = await globalDataStore.get();

if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
if (contentCollectionsStrict) {
throw new AstroError({
name: 'ContentCollectionError',
title: 'Deprecated v4 API',
message: `getDataEntryById() is deprecated for collections using the Content Layer API. Use getEntry() instead for collection "${collection}".`,
});
}
return getEntry(collection, id);
}
console.warn(
Expand Down Expand Up @@ -333,7 +365,10 @@ type DataEntryResult = {

type EntryLookupObject = { collection: string; id: string } | { collection: string; slug: string };

function emulateLegacyEntry({ legacyId, ...entry }: DataEntry & { collection: string }) {
function emulateLegacyEntry(
{ legacyId, ...entry }: DataEntry & { collection: string },
contentCollectionsStrict: boolean,
) {
// Define this first so it's in scope for the render function
const legacyEntry = {
...entry,
Expand All @@ -343,7 +378,17 @@ function emulateLegacyEntry({ legacyId, ...entry }: DataEntry & { collection: st
return {
...legacyEntry,
// Define separately so the render function isn't included in the object passed to `renderEntry()`
render: () => renderEntry(legacyEntry),
render: () => {
if (contentCollectionsStrict) {
throw new AstroError({
name: 'ContentCollectionError',
title: 'Deprecated v4 API',
message: `entry.render() is deprecated for collections using the Content Layer API. Use renderEntry(entry) instead for collection "${entry.collection}".`,
hint: 'See https://docs.astro.build/en/guides/migrate-to-astro/upgrade-to/v5/#content-collections for migration information.',
});
}
return renderEntry(legacyEntry);
},
} as ContentEntryResult;
}

Expand All @@ -352,11 +397,13 @@ export function createGetEntry({
getRenderEntryImport,
collectionNames,
liveCollections,
contentCollectionsStrict,
}: {
getEntryImport: GetEntryImport;
getRenderEntryImport: GetEntryImport;
collectionNames: Set<string>;
liveCollections: LiveCollectionConfigMap;
contentCollectionsStrict: boolean;
}) {
return async function getEntry(
// Can either pass collection and identifier as 2 positional args,
Expand Down Expand Up @@ -408,7 +455,7 @@ export function createGetEntry({
const { default: imageAssetMap } = await import('astro:asset-imports');
entry.data = updateImageReferencesInData(entry.data, entry.filePath, imageAssetMap);
if (entry.legacyId) {
return emulateLegacyEntry({ ...entry, collection });
return emulateLegacyEntry({ ...entry, collection }, contentCollectionsStrict);
}
return {
...entry,
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,22 @@ async function autogenerateCollections({
if (settings.config.legacy.collections) {
return config;
}

// If strict mode is enabled, error on any legacy collections
if (settings.config.experimental.contentCollectionsStrict) {
const collections: Record<string, CollectionConfig> = config?.collections ?? {};
for (const [collectionName, collection] of Object.entries(collections)) {
if (collection?.type === 'content' || collection?.type === 'data') {
throw new AstroError({
name: 'ContentCollectionError',
title: 'Invalid content collection configuration',
message: `Collection "${collectionName}" is using the deprecated v4 API (type: "${collection.type}"). Collections must use the Content Layer API with a loader.`,
hint: 'See https://docs.astro.build/en/guides/migrate-to-astro/upgrade-to/v5/#content-collections for migration information.',
});
}
}
}

const contentDir = new URL('./content/', settings.config.srcDir);

const collections: Record<string, CollectionConfig> = config?.collections ?? {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ async function generateContentEntryFile({
.replace("'@@DATA_ENTRY_GLOB_PATH@@'", dataEntryGlobResult)
.replace("'@@RENDER_ENTRY_GLOB_PATH@@'", renderEntryGlobResult)
.replace('/* @@LOOKUP_MAP_ASSIGNMENT@@ */', `lookupMap = ${JSON.stringify(lookupMap)};`)
.replace('@@CONTENT_COLLECTIONS_STRICT@@', String(settings.config.experimental.contentCollectionsStrict ?? false))
.replace(
'/* @@LIVE_CONTENT_CONFIG@@ */',
contentPaths.liveConfig.exists
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
experimental: {
clientPrerender: false,
contentIntellisense: false,
contentCollectionsStrict: false,
headingIdCompat: false,
preserveScriptOrder: false,
liveContentCollections: false,
Expand Down Expand Up @@ -480,6 +481,10 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.contentIntellisense),
contentCollectionsStrict: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.contentCollectionsStrict),
headingIdCompat: z
.boolean()
.optional()
Expand Down
13 changes: 13 additions & 0 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2162,6 +2162,19 @@ export interface AstroUserConfig<
*/
contentIntellisense?: boolean;

/**
*
* @name experimental.contentCollectionsStrict
* @type {boolean}
* @default `false`
* @version 5.x
* @description
*
* Enables strict mode for content collections, requiring all collections to use the Content Layer API with loaders.
* When enabled, collections using the deprecated v4 API (without loaders) will throw an error during build.
*/
contentCollectionsStrict?: boolean;

/**
*
* @name experimental.fonts
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/templates/content/module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,32 +59,38 @@ const collectionToRenderEntryMap = createCollectionToGlobResultMap({
});

const cacheEntriesByCollection = new Map();
const contentCollectionsStrict = '@@CONTENT_COLLECTIONS_STRICT@@';

export const getCollection = createGetCollection({
contentCollectionToEntryMap,
dataCollectionToEntryMap,
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
cacheEntriesByCollection,
liveCollections,
contentCollectionsStrict,
});

export const getEntry = createGetEntry({
getEntryImport: createGlobLookup(collectionToEntryMap),
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
collectionNames,
liveCollections,
contentCollectionsStrict,
});

export const getEntryBySlug = createGetEntryBySlug({
getEntryImport: createGlobLookup(contentCollectionToEntryMap),
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
collectionNames,
getEntry,
contentCollectionsStrict,
});

export const getDataEntryById = createGetDataEntryById({
getEntryImport: createGlobLookup(dataCollectionToEntryMap),
collectionNames,
getEntry,
contentCollectionsStrict,
});

export const getEntries = createGetEntries(getEntry);
Expand Down
31 changes: 31 additions & 0 deletions packages/astro/test/content-collections.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,35 @@ describe('Content Collections', () => {
assert.equal(index, anotherPage);
});
});

describe('Strict mode', () => {
it('Throws error when contentCollectionsStrict=true and legacy collections exist', async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections/',
experimental: { contentCollectionsStrict: true },
});
let error;
try {
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
assert.match(error, /using the deprecated v4 API/);
assert.match(error, /Collections must use the Content Layer API with a loader/);
});

it('Allows legacy collections when strict mode is not enabled', async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections/',
experimental: { contentCollectionsStrict: false },
});
let error;
try {
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
assert.equal(error, undefined);
});
});
});
Loading