diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 9cd3022cc..2ad4f2f9e 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -10,7 +10,6 @@ import { info, warn, type LogOptions } from '../core/logger/core.js'; import { isRelativePath } from '../core/path.js'; import { CONTENT_TYPES_FILE } from './consts.js'; import { - getContentEntryExts, getContentPaths, getEntryInfo, getEntrySlug, @@ -23,6 +22,7 @@ import { type ContentPaths, type EntryInfo, updateLookupMaps, + getContentEntryConfigByExtMap, } from './utils.js'; type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'; @@ -59,7 +59,8 @@ export async function createContentTypesGenerator({ }: CreateContentGeneratorParams) { const contentTypes: ContentTypes = {}; const contentPaths = getContentPaths(settings.config, fs); - const contentEntryExts = getContentEntryExts(settings); + const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings); + const contentEntryExts = [...contentEntryConfigByExt.keys()]; let events: EventWithOptions[] = []; let debounceTimeout: NodeJS.Timeout | undefined; @@ -280,7 +281,7 @@ export async function createContentTypesGenerator({ contentEntryTypes: settings.contentEntryTypes, }); await updateLookupMaps({ - contentEntryExts, + contentEntryConfigByExt, contentPaths, root: settings.config.root, fs, @@ -308,7 +309,7 @@ function removeCollection(contentMap: ContentTypes, collectionKey: string) { async function parseSlug({ fs, event, - entryInfo, + entryInfo: { id, collection, slug: generatedSlug }, }: { fs: typeof fsMod; event: ContentEvent; @@ -321,7 +322,7 @@ async function parseSlug({ // on dev server startup or production build init. const rawContents = await fs.promises.readFile(event.entry, 'utf-8'); const { data: frontmatter } = parseFrontmatter(rawContents, fileURLToPath(event.entry)); - return getEntrySlug({ ...entryInfo, unvalidatedSlug: frontmatter.slug }); + return getEntrySlug({ id, collection, generatedSlug, frontmatterSlug: frontmatter.slug }); } function setEntry( diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 9ad57195d..91c6f1a77 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -2,12 +2,17 @@ import glob, { type Options as FastGlobOptions } from 'fast-glob'; import { slug as githubSlug } from 'github-slugger'; import matter from 'gray-matter'; import fsMod from 'node:fs'; -import path from 'node:path'; +import path, { extname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { PluginContext } from 'rollup'; import { normalizePath, type ErrorPayload as ViteErrorPayload, type ViteDevServer } from 'vite'; import { z } from 'zod'; -import type { AstroConfig, AstroSettings, ImageInputFormat } from '../@types/astro.js'; +import type { + AstroConfig, + AstroSettings, + ContentEntryType, + ImageInputFormat, +} from '../@types/astro.js'; import { VALID_INPUT_FORMATS } from '../assets/consts.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { CONTENT_TYPES_FILE } from './consts.js'; @@ -50,11 +55,16 @@ export const msg = { export function getEntrySlug({ id, collection, - slug, - unvalidatedSlug, -}: EntryInfo & { unvalidatedSlug?: unknown }) { + generatedSlug, + frontmatterSlug, +}: { + id: string; + collection: string; + generatedSlug: string; + frontmatterSlug?: unknown; +}) { try { - return z.string().default(slug).parse(unvalidatedSlug); + return z.string().default(generatedSlug).parse(frontmatterSlug); } catch { throw new AstroError({ ...AstroErrorData.InvalidContentEntrySlugError, @@ -128,6 +138,16 @@ export function getContentEntryExts(settings: Pick t.extensions).flat(); } +export function getContentEntryConfigByExtMap(settings: Pick) { + const map: Map = new Map(); + for (const entryType of settings.contentEntryTypes) { + for (const ext of entryType.extensions) { + map.set(ext, entryType); + } + } + return map; +} + export class NoCollectionError extends Error {} export function getEntryInfo( @@ -370,18 +390,18 @@ function search(fs: typeof fsMod, srcDir: URL) { export async function updateLookupMaps({ contentPaths, - contentEntryExts, + contentEntryConfigByExt, root, fs, }: { - contentEntryExts: string[]; + contentEntryConfigByExt: ReturnType; contentPaths: Pick; root: URL; fs: typeof fsMod; }) { const { contentDir } = contentPaths; const globOpts: FastGlobOptions = { - absolute: false, + absolute: true, cwd: fileURLToPath(root), fs: { readdir: fs.readdir.bind(fs), @@ -390,7 +410,10 @@ export async function updateLookupMaps({ }; const relContentDir = rootRelativePath(root, contentDir, false); - const contentGlob = await glob(`${relContentDir}/**/*${getExtGlob(contentEntryExts)}`, globOpts); + const contentGlob = await glob( + `${relContentDir}**/*${getExtGlob([...contentEntryConfigByExt.keys()])}`, + globOpts + ); let filePathByLookupId: { [collection: string]: Record; } = {}; @@ -398,9 +421,17 @@ export async function updateLookupMaps({ for (const filePath of contentGlob) { const info = getEntryInfo({ contentDir, entry: filePath }); if (info instanceof NoCollectionError) continue; - filePathByLookupId[info.collection] ??= {}; - // TODO: frontmatter slugs - filePathByLookupId[info.collection][info.slug] = '/' + filePath; + const contentEntryConfig = contentEntryConfigByExt.get(extname(filePath)); + if (!contentEntryConfig) continue; + + const { id, collection, slug: generatedSlug } = info; + filePathByLookupId[collection] ??= {}; + const { slug: frontmatterSlug } = await contentEntryConfig.getEntryInfo({ + fileUrl: pathToFileURL(filePath), + contents: await fs.promises.readFile(filePath, 'utf-8'), + }); + const slug = getEntrySlug({ id, collection, generatedSlug, frontmatterSlug }); + filePathByLookupId[collection][slug] = rootRelativePath(root, filePath); } await fs.promises.writeFile( diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 1c7a80b91..75e63550e 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -19,6 +19,7 @@ import { globalContentConfigObserver, NoCollectionError, type ContentConfig, + getContentEntryConfigByExtMap, } from './utils.js'; function isContentFlagImport(viteId: string) { @@ -55,12 +56,7 @@ export function astroContentImportPlugin({ const contentPaths = getContentPaths(settings.config, fs); const contentEntryExts = getContentEntryExts(settings); - const contentEntryExtToParser: Map = new Map(); - for (const entryType of settings.contentEntryTypes) { - for (const ext of entryType.extensions) { - contentEntryExtToParser.set(ext, entryType); - } - } + const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings); const plugins: Plugin[] = [ { @@ -196,7 +192,7 @@ export function astroContentImportPlugin({ const contentConfig = await getContentConfigFromGlobal(); const rawContents = await fs.promises.readFile(fileId, 'utf-8'); const fileExt = extname(fileId); - if (!contentEntryExtToParser.has(fileExt)) { + if (!contentEntryConfigByExt.has(fileExt)) { throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: `No parser found for content entry ${JSON.stringify( @@ -204,13 +200,13 @@ export function astroContentImportPlugin({ )}. Did you apply an integration for this file type?`, }); } - const contentEntryParser = contentEntryExtToParser.get(fileExt)!; + const contentEntryConfig = contentEntryConfigByExt.get(fileExt)!; const { rawData, body, - slug: unvalidatedSlug, + slug: frontmatterSlug, data: unvalidatedData, - } = await contentEntryParser.getEntryInfo({ + } = await contentEntryConfig.getEntryInfo({ fileUrl: pathToFileURL(fileId), contents: rawContents, }); @@ -225,7 +221,12 @@ export function astroContentImportPlugin({ const _internal = { filePath: fileId, rawData: rawData }; // TODO: move slug calculation to the start of the build // to generate a performant lookup map for `getEntryBySlug` - const slug = getEntrySlug({ id, collection, slug: generatedSlug, unvalidatedSlug }); + const slug = getEntrySlug({ + id, + collection, + generatedSlug, + frontmatterSlug, + }); const collectionConfig = contentConfig?.collections[collection]; let data = collectionConfig