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 { 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(

View file

@ -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<AstroSettings, 'contentEntryT
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 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<typeof getContentEntryConfigByExtMap>;
contentPaths: Pick<ContentPaths, 'contentDir' | 'cacheDir'>;
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<string, string>;
} = {};
@ -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(

View file

@ -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<string, ContentEntryType> = 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