-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
feat(content-layer): support references in content layer #11494
Changes from all commits
8058c25
4edfa1c
1139163
bcd4463
fc25102
a587b90
a2dfbc4
bad9301
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,7 +1,7 @@ | ||||||||||||||||||||
import type { MarkdownHeading } from '@astrojs/markdown-remark'; | ||||||||||||||||||||
import { Traverse } from 'neotraverse/modern'; | ||||||||||||||||||||
import pLimit from 'p-limit'; | ||||||||||||||||||||
import { ZodIssueCode, string as zodString } from 'zod'; | ||||||||||||||||||||
import { ZodIssueCode, z } from 'zod'; | ||||||||||||||||||||
import type { GetImageResult, ImageMetadata } from '../@types/astro.js'; | ||||||||||||||||||||
import { imageSrcToImportId } from '../assets/utils/resolveImports.js'; | ||||||||||||||||||||
import { AstroError, AstroErrorData } from '../core/errors/index.js'; | ||||||||||||||||||||
|
@@ -147,9 +147,11 @@ export function createGetCollection({ | |||||||||||||||||||
export function createGetEntryBySlug({ | ||||||||||||||||||||
getEntryImport, | ||||||||||||||||||||
getRenderEntryImport, | ||||||||||||||||||||
collectionNames, | ||||||||||||||||||||
}: { | ||||||||||||||||||||
getEntryImport: GetEntryImport; | ||||||||||||||||||||
getRenderEntryImport: GetEntryImport; | ||||||||||||||||||||
collectionNames: Set<string>; | ||||||||||||||||||||
}) { | ||||||||||||||||||||
return async function getEntryBySlug(collection: string, slug: string) { | ||||||||||||||||||||
const store = await globalDataStore.get(); | ||||||||||||||||||||
|
@@ -168,6 +170,11 @@ export function createGetEntryBySlug({ | |||||||||||||||||||
render: () => renderEntry(entry), | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
if (!collectionNames.has(collection)) { | ||||||||||||||||||||
// eslint-disable-next-line no-console | ||||||||||||||||||||
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`); | ||||||||||||||||||||
return undefined; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const entryImport = await getEntryImport(collection, slug); | ||||||||||||||||||||
if (typeof entryImport !== 'function') return undefined; | ||||||||||||||||||||
|
@@ -191,7 +198,10 @@ export function createGetEntryBySlug({ | |||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
export function createGetDataEntryById({ getEntryImport }: { getEntryImport: GetEntryImport }) { | ||||||||||||||||||||
export function createGetDataEntryById({ | ||||||||||||||||||||
getEntryImport, | ||||||||||||||||||||
collectionNames, | ||||||||||||||||||||
}: { getEntryImport: GetEntryImport; collectionNames: Set<string> }) { | ||||||||||||||||||||
return async function getDataEntryById(collection: string, id: string) { | ||||||||||||||||||||
const store = await globalDataStore.get(); | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -208,6 +218,11 @@ export function createGetDataEntryById({ getEntryImport }: { getEntryImport: Get | |||||||||||||||||||
collection, | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
if (!collectionNames.has(collection)) { | ||||||||||||||||||||
// eslint-disable-next-line no-console | ||||||||||||||||||||
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`); | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the right behaviour? It's not clear whether a missing collection should throw or just warn. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It also warns in astro/packages/astro/src/content/runtime.ts Lines 64 to 72 in 026e8ba
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's the only reason I used it. I was trying to follow the current behaviour. |
||||||||||||||||||||
return undefined; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const lazyImport = await getEntryImport(collection, id); | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -243,9 +258,11 @@ type EntryLookupObject = { collection: string; id: string } | { collection: stri | |||||||||||||||||||
export function createGetEntry({ | ||||||||||||||||||||
getEntryImport, | ||||||||||||||||||||
getRenderEntryImport, | ||||||||||||||||||||
collectionNames, | ||||||||||||||||||||
}: { | ||||||||||||||||||||
getEntryImport: GetEntryImport; | ||||||||||||||||||||
getRenderEntryImport: GetEntryImport; | ||||||||||||||||||||
collectionNames: Set<string>; | ||||||||||||||||||||
}) { | ||||||||||||||||||||
return async function getEntry( | ||||||||||||||||||||
// Can either pass collection and identifier as 2 positional args, | ||||||||||||||||||||
|
@@ -277,7 +294,9 @@ export function createGetEntry({ | |||||||||||||||||||
if (store.hasCollection(collection)) { | ||||||||||||||||||||
const entry = store.get<DataEntry>(collection, lookupId); | ||||||||||||||||||||
if (!entry) { | ||||||||||||||||||||
throw new Error(`Entry ${collection} → ${lookupId} was not found.`); | ||||||||||||||||||||
// eslint-disable-next-line no-console | ||||||||||||||||||||
console.warn(`Entry ${collection} → ${lookupId} was not found.`); | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (entry.filePath) { | ||||||||||||||||||||
|
@@ -292,6 +311,12 @@ export function createGetEntry({ | |||||||||||||||||||
} as DataEntryResult | ContentEntryResult; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (!collectionNames.has(collection)) { | ||||||||||||||||||||
// eslint-disable-next-line no-console | ||||||||||||||||||||
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`); | ||||||||||||||||||||
return undefined; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const entryImport = await getEntryImport(collection, lookupId); | ||||||||||||||||||||
if (typeof entryImport !== 'function') return undefined; | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -514,36 +539,92 @@ async function render({ | |||||||||||||||||||
|
||||||||||||||||||||
export function createReference({ lookupMap }: { lookupMap: ContentLookupMap }) { | ||||||||||||||||||||
return function reference(collection: string) { | ||||||||||||||||||||
return zodString().transform((lookupId: string, ctx) => { | ||||||||||||||||||||
const flattenedErrorPath = ctx.path.join('.'); | ||||||||||||||||||||
if (!lookupMap[collection]) { | ||||||||||||||||||||
ctx.addIssue({ | ||||||||||||||||||||
code: ZodIssueCode.custom, | ||||||||||||||||||||
message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`, | ||||||||||||||||||||
}); | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const { type, entries } = lookupMap[collection]; | ||||||||||||||||||||
const entry = entries[lookupId]; | ||||||||||||||||||||
|
||||||||||||||||||||
if (!entry) { | ||||||||||||||||||||
ctx.addIssue({ | ||||||||||||||||||||
code: ZodIssueCode.custom, | ||||||||||||||||||||
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${Object.keys( | ||||||||||||||||||||
entries | ||||||||||||||||||||
) | ||||||||||||||||||||
.map((c) => JSON.stringify(c)) | ||||||||||||||||||||
.join(' | ')}. Received ${JSON.stringify(lookupId)}.`, | ||||||||||||||||||||
}); | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
// Content is still identified by slugs, so map to a `slug` key for consistency. | ||||||||||||||||||||
if (type === 'content') { | ||||||||||||||||||||
return { slug: lookupId, collection }; | ||||||||||||||||||||
} | ||||||||||||||||||||
return { id: lookupId, collection }; | ||||||||||||||||||||
}); | ||||||||||||||||||||
return z | ||||||||||||||||||||
.union([ | ||||||||||||||||||||
z.string(), | ||||||||||||||||||||
z.object({ | ||||||||||||||||||||
id: z.string(), | ||||||||||||||||||||
collection: z.string(), | ||||||||||||||||||||
}), | ||||||||||||||||||||
z.object({ | ||||||||||||||||||||
slug: z.string(), | ||||||||||||||||||||
collection: z.string(), | ||||||||||||||||||||
}), | ||||||||||||||||||||
]) | ||||||||||||||||||||
.transform( | ||||||||||||||||||||
async ( | ||||||||||||||||||||
lookup: | ||||||||||||||||||||
| string | ||||||||||||||||||||
| { id: string; collection: string } | ||||||||||||||||||||
| { slug: string; collection: string }, | ||||||||||||||||||||
ctx | ||||||||||||||||||||
) => { | ||||||||||||||||||||
const flattenedErrorPath = ctx.path.join('.'); | ||||||||||||||||||||
const store = await globalDataStore.get(); | ||||||||||||||||||||
const collectionIsInStore = store.hasCollection(collection); | ||||||||||||||||||||
|
||||||||||||||||||||
if (typeof lookup === 'object') { | ||||||||||||||||||||
// If these don't match then something is wrong with the reference | ||||||||||||||||||||
if (lookup.collection !== collection) { | ||||||||||||||||||||
ctx.addIssue({ | ||||||||||||||||||||
code: ZodIssueCode.custom, | ||||||||||||||||||||
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${collection}. Received ${lookup.collection}.`, | ||||||||||||||||||||
}); | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// A reference object might refer to an invalid collection, because when we convert it we don't have access to the store. | ||||||||||||||||||||
// If it is an object then we're validating later in the pipeline, so we can check the collection at that point. | ||||||||||||||||||||
if (!lookupMap[collection] && !collectionIsInStore) { | ||||||||||||||||||||
ctx.addIssue({ | ||||||||||||||||||||
code: ZodIssueCode.custom, | ||||||||||||||||||||
message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`, | ||||||||||||||||||||
}); | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
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 (!lookupMap[collection] && store.collections().size === 0) { | ||||||||||||||||||||
// 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. | ||||||||||||||||||||
// 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 }; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const { type, entries } = lookupMap[collection]; | ||||||||||||||||||||
const entry = entries[lookup]; | ||||||||||||||||||||
|
||||||||||||||||||||
if (!entry) { | ||||||||||||||||||||
ctx.addIssue({ | ||||||||||||||||||||
code: ZodIssueCode.custom, | ||||||||||||||||||||
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${Object.keys( | ||||||||||||||||||||
entries | ||||||||||||||||||||
) | ||||||||||||||||||||
.map((c) => JSON.stringify(c)) | ||||||||||||||||||||
.join(' | ')}. Received ${JSON.stringify(lookup)}.`, | ||||||||||||||||||||
}); | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
// Content is still identified by slugs, so map to a `slug` key for consistency. | ||||||||||||||||||||
if (type === 'content') { | ||||||||||||||||||||
return { slug: lookup, collection }; | ||||||||||||||||||||
} | ||||||||||||||||||||
return { id: lookup, collection }; | ||||||||||||||||||||
} | ||||||||||||||||||||
); | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should refactor this code, and provide an instance of the
AstroLogger
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a suggestion of how best to do that? I can't find any other usage of the AstroLogger in the content runtime, so I'm not sure how best to get one in scope
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah anything inside of runtime code is difficult to have a logger for.