fix: defer validation of reference entries (#12990)

This commit is contained in:
Matt Kane 2025-01-16 09:24:48 +00:00 committed by GitHub
parent 9f34e3f889
commit 2e12f1d752
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 70 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a bug that caused references to be incorrectly reported as invalid

View file

@ -642,7 +642,6 @@ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap })
}
const flattenedErrorPath = ctx.path.join('.');
const collectionIsInStore = store.hasCollection(collection);
if (typeof lookup === 'object') {
// If these don't match then something is wrong with the reference
@ -659,22 +658,8 @@ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap })
return lookup;
}
if (collectionIsInStore) {
const entry = store.get(collection, lookup);
if (!entry) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Entry ${lookup} does not exist.`,
});
return;
}
return { id: lookup, collection };
}
// If the collection is not in the lookup map or store, it may be a content layer collection and the store may not yet be populated.
// If the store has 0 or 1 entries it probably means that the entries have not yet been loaded.
// The store may have a single entry even if the collections have not loaded, because the top-level metadata collection is generated early.
if (!lookupMap[collection] && store.collections().size <= 1) {
// If the collection is not in the lookup map it may be a content layer collection and the store may not yet be populated.
if (!lookupMap[collection]) {
// For now, we can't validate this reference, so we'll optimistically convert it to a reference object which we'll validate
// later in the pipeline when we do have access to the store.
return { id: lookup, collection };

View file

@ -314,6 +314,24 @@ describe('Content Layer', () => {
assert.equal(newJson.increment.data.lastValue, 1);
await fixture.resetAllFiles();
});
it('can handle references being renamed after a build', async () => {
let newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.deepEqual(newJson.entryWithReference.data.cat, { collection: 'cats', id: 'tabby' });
await fixture.editFile('src/data/cats.json', (prev) => {
return prev.replace('tabby', 'tabby-cat');
});
await fixture.editFile('src/content/space/columbia-copy.md', (prev) => {
return prev.replace('cat: tabby', 'cat: tabby-cat');
});
await fixture.build();
newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.deepEqual(newJson.entryWithReference.data.cat, { collection: 'cats', id: 'tabby-cat' });
await fixture.resetAllFiles();
});
});
describe('Dev', () => {

View file

@ -2,6 +2,7 @@ import { defineCollection, z, reference } from 'astro:content';
import { file, glob } from 'astro/loaders';
import { loader } from './loaders/post-loader.js';
import { parse as parseToml } from 'toml';
import { readFile } from 'fs/promises';
const blog = defineCollection({
loader: loader({ url: 'https://jsonplaceholder.typicode.com/posts' }),
@ -71,43 +72,32 @@ const rodents = defineCollection({
nocturnal: z.boolean(),
}),
});
// Absolute paths should also work
const absoluteRoot = new URL('content/space', import.meta.url);
const spacecraft = defineCollection({
loader: glob({ pattern: '*.md', base: absoluteRoot }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
publishedDate: z.coerce.date(),
tags: z.array(z.string()),
heroImage: image().optional(),
cat: reference('cats').default('siamese'),
something: z
.string()
.optional()
.transform((str) => ({ type: 'test', content: str })),
}),
});
const cats = defineCollection({
loader: async function () {
return [
{
breed: 'Siamese',
id: 'siamese',
size: 'Medium',
origin: 'Thailand',
lifespan: '15 years',
temperament: ['Active', 'Affectionate', 'Social', 'Playful'],
},
{
breed: 'Persian',
id: 'persian',
size: 'Medium',
origin: 'Iran',
lifespan: '15 years',
temperament: ['Calm', 'Affectionate', 'Social'],
},
{
breed: 'Tabby',
id: 'tabby',
size: 'Medium',
origin: 'Egypt',
lifespan: '15 years',
temperament: ['Curious', 'Playful', 'Independent'],
},
{
breed: 'Ragdoll',
id: 'ragdoll',
size: 'Medium',
origin: 'United States',
lifespan: '15 years',
temperament: ['Calm', 'Affectionate', 'Social'],
},
];
const file = new URL('data/cats.json', import.meta.url);
const data = await readFile(file, 'utf-8');
return JSON.parse(data);
},
schema: z.object({
breed: z.string(),
@ -140,25 +130,6 @@ const birds = defineCollection({
}),
});
// Absolute paths should also work
const absoluteRoot = new URL('content/space', import.meta.url);
const spacecraft = defineCollection({
loader: glob({ pattern: '*.md', base: absoluteRoot }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
publishedDate: z.coerce.date(),
tags: z.array(z.string()),
heroImage: image().optional(),
cat: reference('cats').default('siamese'),
something: z
.string()
.optional()
.transform((str) => ({ type: 'test', content: str })),
}),
});
const probes = defineCollection({
loader: glob({ pattern: ['*.md', '!voyager-*'], base: 'src/data/space-probes' }),

View file

@ -0,0 +1,51 @@
[
{
"breed": "Siamese",
"id": "siamese",
"size": "Medium",
"origin": "Thailand",
"lifespan": "15 years",
"temperament": [
"Active",
"Affectionate",
"Social",
"Playful"
]
},
{
"breed": "Persian",
"id": "persian",
"size": "Medium",
"origin": "Iran",
"lifespan": "15 years",
"temperament": [
"Calm",
"Affectionate",
"Social"
]
},
{
"breed": "Tabby",
"id": "tabby",
"size": "Medium",
"origin": "Egypt",
"lifespan": "15 years",
"temperament": [
"Curious",
"Playful",
"Independent"
]
},
{
"breed": "Ragdoll",
"id": "ragdoll",
"size": "Medium",
"origin": "United States",
"lifespan": "15 years",
"temperament": [
"Calm",
"Affectionate",
"Social"
]
}
]