fix: consider frontmatter slugs

This commit is contained in:
bholmesdev 2023-04-26 15:11:15 -04:00
parent aa18be7fed
commit 0d5fe3186e
3 changed files with 62 additions and 29 deletions

View file

@ -10,7 +10,6 @@ import { info, warn, type LogOptions } from '../core/logger/core.js';
import { isRelativePath } from '../core/path.js'; import { isRelativePath } from '../core/path.js';
import { CONTENT_TYPES_FILE } from './consts.js'; import { CONTENT_TYPES_FILE } from './consts.js';
import { import {
getContentEntryExts,
getContentPaths, getContentPaths,
getEntryInfo, getEntryInfo,
getEntrySlug, getEntrySlug,
@ -23,6 +22,7 @@ import {
type ContentPaths, type ContentPaths,
type EntryInfo, type EntryInfo,
updateLookupMaps, updateLookupMaps,
getContentEntryConfigByExtMap,
} from './utils.js'; } from './utils.js';
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'; type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
@ -59,7 +59,8 @@ export async function createContentTypesGenerator({
}: CreateContentGeneratorParams) { }: CreateContentGeneratorParams) {
const contentTypes: ContentTypes = {}; const contentTypes: ContentTypes = {};
const contentPaths = getContentPaths(settings.config, fs); const contentPaths = getContentPaths(settings.config, fs);
const contentEntryExts = getContentEntryExts(settings); const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
const contentEntryExts = [...contentEntryConfigByExt.keys()];
let events: EventWithOptions[] = []; let events: EventWithOptions[] = [];
let debounceTimeout: NodeJS.Timeout | undefined; let debounceTimeout: NodeJS.Timeout | undefined;
@ -280,7 +281,7 @@ export async function createContentTypesGenerator({
contentEntryTypes: settings.contentEntryTypes, contentEntryTypes: settings.contentEntryTypes,
}); });
await updateLookupMaps({ await updateLookupMaps({
contentEntryExts, contentEntryConfigByExt,
contentPaths, contentPaths,
root: settings.config.root, root: settings.config.root,
fs, fs,
@ -308,7 +309,7 @@ function removeCollection(contentMap: ContentTypes, collectionKey: string) {
async function parseSlug({ async function parseSlug({
fs, fs,
event, event,
entryInfo, entryInfo: { id, collection, slug: generatedSlug },
}: { }: {
fs: typeof fsMod; fs: typeof fsMod;
event: ContentEvent; event: ContentEvent;
@ -321,7 +322,7 @@ async function parseSlug({
// on dev server startup or production build init. // on dev server startup or production build init.
const rawContents = await fs.promises.readFile(event.entry, 'utf-8'); const rawContents = await fs.promises.readFile(event.entry, 'utf-8');
const { data: frontmatter } = parseFrontmatter(rawContents, fileURLToPath(event.entry)); const { data: frontmatter } = parseFrontmatter(rawContents, fileURLToPath(event.entry));
return getEntrySlug({ ...entryInfo, unvalidatedSlug: frontmatter.slug }); return getEntrySlug({ id, collection, generatedSlug, frontmatterSlug: frontmatter.slug });
} }
function setEntry( function setEntry(

View file

@ -2,12 +2,17 @@ import glob, { type Options as FastGlobOptions } from 'fast-glob';
import { slug as githubSlug } from 'github-slugger'; import { slug as githubSlug } from 'github-slugger';
import matter from 'gray-matter'; import matter from 'gray-matter';
import fsMod from 'node:fs'; import fsMod from 'node:fs';
import path from 'node:path'; import path, { extname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import type { PluginContext } from 'rollup'; import type { PluginContext } from 'rollup';
import { normalizePath, type ErrorPayload as ViteErrorPayload, type ViteDevServer } from 'vite'; import { normalizePath, type ErrorPayload as ViteErrorPayload, type ViteDevServer } from 'vite';
import { z } from 'zod'; 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 { VALID_INPUT_FORMATS } from '../assets/consts.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { CONTENT_TYPES_FILE } from './consts.js'; import { CONTENT_TYPES_FILE } from './consts.js';
@ -50,11 +55,16 @@ export const msg = {
export function getEntrySlug({ export function getEntrySlug({
id, id,
collection, collection,
slug, generatedSlug,
unvalidatedSlug, frontmatterSlug,
}: EntryInfo & { unvalidatedSlug?: unknown }) { }: {
id: string;
collection: string;
generatedSlug: string;
frontmatterSlug?: unknown;
}) {
try { try {
return z.string().default(slug).parse(unvalidatedSlug); return z.string().default(generatedSlug).parse(frontmatterSlug);
} catch { } catch {
throw new AstroError({ throw new AstroError({
...AstroErrorData.InvalidContentEntrySlugError, ...AstroErrorData.InvalidContentEntrySlugError,
@ -128,6 +138,16 @@ export function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryT
return settings.contentEntryTypes.map((t) => t.extensions).flat(); return settings.contentEntryTypes.map((t) => t.extensions).flat();
} }
export function getContentEntryConfigByExtMap(settings: Pick<AstroSettings, 'contentEntryTypes'>) {
const map: Map<string, ContentEntryType> = 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 class NoCollectionError extends Error {}
export function getEntryInfo( export function getEntryInfo(
@ -370,18 +390,18 @@ function search(fs: typeof fsMod, srcDir: URL) {
export async function updateLookupMaps({ export async function updateLookupMaps({
contentPaths, contentPaths,
contentEntryExts, contentEntryConfigByExt,
root, root,
fs, fs,
}: { }: {
contentEntryExts: string[]; contentEntryConfigByExt: ReturnType<typeof getContentEntryConfigByExtMap>;
contentPaths: Pick<ContentPaths, 'contentDir' | 'cacheDir'>; contentPaths: Pick<ContentPaths, 'contentDir' | 'cacheDir'>;
root: URL; root: URL;
fs: typeof fsMod; fs: typeof fsMod;
}) { }) {
const { contentDir } = contentPaths; const { contentDir } = contentPaths;
const globOpts: FastGlobOptions = { const globOpts: FastGlobOptions = {
absolute: false, absolute: true,
cwd: fileURLToPath(root), cwd: fileURLToPath(root),
fs: { fs: {
readdir: fs.readdir.bind(fs), readdir: fs.readdir.bind(fs),
@ -390,7 +410,10 @@ export async function updateLookupMaps({
}; };
const relContentDir = rootRelativePath(root, contentDir, false); 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: { let filePathByLookupId: {
[collection: string]: Record<string, string>; [collection: string]: Record<string, string>;
} = {}; } = {};
@ -398,9 +421,17 @@ export async function updateLookupMaps({
for (const filePath of contentGlob) { for (const filePath of contentGlob) {
const info = getEntryInfo({ contentDir, entry: filePath }); const info = getEntryInfo({ contentDir, entry: filePath });
if (info instanceof NoCollectionError) continue; if (info instanceof NoCollectionError) continue;
filePathByLookupId[info.collection] ??= {}; const contentEntryConfig = contentEntryConfigByExt.get(extname(filePath));
// TODO: frontmatter slugs if (!contentEntryConfig) continue;
filePathByLookupId[info.collection][info.slug] = '/' + filePath;
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( await fs.promises.writeFile(

View file

@ -19,6 +19,7 @@ import {
globalContentConfigObserver, globalContentConfigObserver,
NoCollectionError, NoCollectionError,
type ContentConfig, type ContentConfig,
getContentEntryConfigByExtMap,
} from './utils.js'; } from './utils.js';
function isContentFlagImport(viteId: string) { function isContentFlagImport(viteId: string) {
@ -55,12 +56,7 @@ export function astroContentImportPlugin({
const contentPaths = getContentPaths(settings.config, fs); const contentPaths = getContentPaths(settings.config, fs);
const contentEntryExts = getContentEntryExts(settings); const contentEntryExts = getContentEntryExts(settings);
const contentEntryExtToParser: Map<string, ContentEntryType> = new Map(); const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
for (const entryType of settings.contentEntryTypes) {
for (const ext of entryType.extensions) {
contentEntryExtToParser.set(ext, entryType);
}
}
const plugins: Plugin[] = [ const plugins: Plugin[] = [
{ {
@ -196,7 +192,7 @@ export function astroContentImportPlugin({
const contentConfig = await getContentConfigFromGlobal(); const contentConfig = await getContentConfigFromGlobal();
const rawContents = await fs.promises.readFile(fileId, 'utf-8'); const rawContents = await fs.promises.readFile(fileId, 'utf-8');
const fileExt = extname(fileId); const fileExt = extname(fileId);
if (!contentEntryExtToParser.has(fileExt)) { if (!contentEntryConfigByExt.has(fileExt)) {
throw new AstroError({ throw new AstroError({
...AstroErrorData.UnknownContentCollectionError, ...AstroErrorData.UnknownContentCollectionError,
message: `No parser found for content entry ${JSON.stringify( 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?`, )}. Did you apply an integration for this file type?`,
}); });
} }
const contentEntryParser = contentEntryExtToParser.get(fileExt)!; const contentEntryConfig = contentEntryConfigByExt.get(fileExt)!;
const { const {
rawData, rawData,
body, body,
slug: unvalidatedSlug, slug: frontmatterSlug,
data: unvalidatedData, data: unvalidatedData,
} = await contentEntryParser.getEntryInfo({ } = await contentEntryConfig.getEntryInfo({
fileUrl: pathToFileURL(fileId), fileUrl: pathToFileURL(fileId),
contents: rawContents, contents: rawContents,
}); });
@ -225,7 +221,12 @@ export function astroContentImportPlugin({
const _internal = { filePath: fileId, rawData: rawData }; const _internal = { filePath: fileId, rawData: rawData };
// TODO: move slug calculation to the start of the build // TODO: move slug calculation to the start of the build
// to generate a performant lookup map for `getEntryBySlug` // 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]; const collectionConfig = contentConfig?.collections[collection];
let data = collectionConfig let data = collectionConfig