244 lines
7.4 KiB
TypeScript
244 lines
7.4 KiB
TypeScript
import type { PostData } from './PostEngine';
|
|
|
|
export interface RequestedPostRoute {
|
|
year: number;
|
|
month: number;
|
|
day: number;
|
|
slug: string;
|
|
}
|
|
|
|
export interface MissingPathPlan {
|
|
requestedCategories: Set<string>;
|
|
requestedTags: Set<string>;
|
|
requestedYears: Set<number>;
|
|
requestedYearMonths: Set<string>;
|
|
requestedYearMonthDays: Set<string>;
|
|
requestedPostRoutes: RequestedPostRoute[];
|
|
requestedPageSlugs: Set<string>;
|
|
requestRootRoutes: boolean;
|
|
requiresFallbackSectionRender: boolean;
|
|
}
|
|
|
|
export interface TargetedValidationPlan {
|
|
requestedPostIds: Set<string>;
|
|
requestedCategorySet: Set<string>;
|
|
requestedTagSet: Set<string>;
|
|
requestedYears: Set<number>;
|
|
requestedYearMonths: Set<string>;
|
|
requestedYearMonthDays: Set<string>;
|
|
requestedPageSlugs: Set<string>;
|
|
requestRootRoutes: boolean;
|
|
}
|
|
|
|
function normalizeUrlPath(urlPath: string): string {
|
|
const trimmed = (urlPath || '').trim();
|
|
if (!trimmed || trimmed === '/') {
|
|
return '/';
|
|
}
|
|
|
|
const noQuery = trimmed.split('?')[0]?.split('#')[0] ?? '';
|
|
const withoutSlashes = noQuery.replace(/^\/+|\/+$/g, '');
|
|
return withoutSlashes ? `/${withoutSlashes}` : '/';
|
|
}
|
|
|
|
function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
|
|
if (post.createdAt instanceof Date) {
|
|
return post.createdAt;
|
|
}
|
|
|
|
const parsed = new Date(post.createdAt);
|
|
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
|
}
|
|
|
|
function decodePathSegment(value: string): string {
|
|
try {
|
|
return decodeURIComponent(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export function planMissingValidationPaths(missingPaths: string[]): MissingPathPlan {
|
|
const requestedCategories = new Set<string>();
|
|
const requestedTags = new Set<string>();
|
|
const requestedYears = new Set<number>();
|
|
const requestedYearMonths = new Set<string>();
|
|
const requestedYearMonthDays = new Set<string>();
|
|
const requestedPostRoutes: RequestedPostRoute[] = [];
|
|
const requestedPageSlugs = new Set<string>();
|
|
let requestRootRoutes = false;
|
|
let requiresFallbackSectionRender = false;
|
|
|
|
for (const missingPath of missingPaths) {
|
|
const normalizedPath = normalizeUrlPath(missingPath);
|
|
|
|
if (normalizedPath === '/' || /^\/page\/\d+$/.test(normalizedPath)) {
|
|
requestRootRoutes = true;
|
|
continue;
|
|
}
|
|
|
|
const categoryMatch = normalizedPath.match(/^\/category\/([^/]+)(?:\/page\/\d+)?$/);
|
|
if (categoryMatch) {
|
|
requestedCategories.add(decodePathSegment(categoryMatch[1]));
|
|
continue;
|
|
}
|
|
|
|
const tagMatch = normalizedPath.match(/^\/tag\/([^/]+)(?:\/page\/\d+)?$/);
|
|
if (tagMatch) {
|
|
requestedTags.add(decodePathSegment(tagMatch[1]));
|
|
continue;
|
|
}
|
|
|
|
const singleMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/([^/]+)$/);
|
|
if (singleMatch) {
|
|
requestedPostRoutes.push({
|
|
year: Number(singleMatch[1]),
|
|
month: Number(singleMatch[2]),
|
|
day: Number(singleMatch[3]),
|
|
slug: decodePathSegment(singleMatch[4]),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const yearMatch = normalizedPath.match(/^\/(\d{4})(?:\/page\/\d+)?$/);
|
|
if (yearMatch) {
|
|
requestedYears.add(Number(yearMatch[1]));
|
|
continue;
|
|
}
|
|
|
|
const monthMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})(?:\/page\/\d+)?$/);
|
|
if (monthMatch) {
|
|
requestedYearMonths.add(`${monthMatch[1]}/${monthMatch[2]}`);
|
|
continue;
|
|
}
|
|
|
|
const dayMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})\/(\d{2})(?:\/page\/\d+)?$/);
|
|
if (dayMatch) {
|
|
requestedYearMonthDays.add(`${dayMatch[1]}/${dayMatch[2]}/${dayMatch[3]}`);
|
|
continue;
|
|
}
|
|
|
|
const pageMatch = normalizedPath.match(/^\/([^/]+)$/);
|
|
if (pageMatch) {
|
|
requestedPageSlugs.add(decodePathSegment(pageMatch[1]));
|
|
continue;
|
|
}
|
|
|
|
requiresFallbackSectionRender = true;
|
|
break;
|
|
}
|
|
|
|
return {
|
|
requestedCategories,
|
|
requestedTags,
|
|
requestedYears,
|
|
requestedYearMonths,
|
|
requestedYearMonthDays,
|
|
requestedPostRoutes,
|
|
requestedPageSlugs,
|
|
requestRootRoutes,
|
|
requiresFallbackSectionRender,
|
|
};
|
|
}
|
|
|
|
interface BuildTargetedValidationPlanParams {
|
|
initialPlan: MissingPathPlan;
|
|
publishedPosts: PostData[];
|
|
allCategories: Set<string>;
|
|
allTags: Set<string>;
|
|
availableYearMonths: Iterable<string>;
|
|
availableYearMonthDays: Iterable<string>;
|
|
}
|
|
|
|
export function buildTargetedValidationPlan(params: BuildTargetedValidationPlanParams): TargetedValidationPlan {
|
|
const {
|
|
initialPlan,
|
|
publishedPosts,
|
|
allCategories,
|
|
allTags,
|
|
availableYearMonths,
|
|
availableYearMonthDays,
|
|
} = params;
|
|
|
|
const requestedCategories = new Set(initialPlan.requestedCategories);
|
|
const requestedTags = new Set(initialPlan.requestedTags);
|
|
const requestedYears = new Set(initialPlan.requestedYears);
|
|
const requestedYearMonths = new Set(initialPlan.requestedYearMonths);
|
|
const requestedYearMonthDays = new Set(initialPlan.requestedYearMonthDays);
|
|
const requestedPostIds = new Set<string>();
|
|
|
|
for (const requestedRoute of initialPlan.requestedPostRoutes) {
|
|
const routePost = publishedPosts.find((post) => {
|
|
if (post.slug !== requestedRoute.slug) {
|
|
return false;
|
|
}
|
|
const createdAt = resolvePostCreatedAt(post);
|
|
return createdAt.getFullYear() === requestedRoute.year
|
|
&& (createdAt.getMonth() + 1) === requestedRoute.month
|
|
&& createdAt.getDate() === requestedRoute.day;
|
|
});
|
|
|
|
if (routePost) {
|
|
requestedPostIds.add(routePost.id);
|
|
for (const category of routePost.categories || []) {
|
|
requestedCategories.add(category);
|
|
}
|
|
for (const tag of routePost.tags || []) {
|
|
requestedTags.add(tag);
|
|
}
|
|
|
|
const createdAt = resolvePostCreatedAt(routePost);
|
|
const year = createdAt.getFullYear();
|
|
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
|
|
const day = String(createdAt.getDate()).padStart(2, '0');
|
|
requestedYears.add(year);
|
|
requestedYearMonths.add(`${year}/${month}`);
|
|
requestedYearMonthDays.add(`${year}/${month}/${day}`);
|
|
} else {
|
|
requestedYears.add(requestedRoute.year);
|
|
requestedYearMonths.add(`${requestedRoute.year}/${String(requestedRoute.month).padStart(2, '0')}`);
|
|
requestedYearMonthDays.add(`${requestedRoute.year}/${String(requestedRoute.month).padStart(2, '0')}/${String(requestedRoute.day).padStart(2, '0')}`);
|
|
}
|
|
}
|
|
|
|
for (const year of Array.from(requestedYears.values())) {
|
|
for (const ym of availableYearMonths) {
|
|
if (ym.startsWith(`${year}/`)) {
|
|
requestedYearMonths.add(ym);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const ym of Array.from(requestedYearMonths.values())) {
|
|
for (const ymd of availableYearMonthDays) {
|
|
if (ymd.startsWith(`${ym}/`)) {
|
|
requestedYearMonthDays.add(ymd);
|
|
}
|
|
}
|
|
|
|
const [yearStr] = ym.split('/');
|
|
requestedYears.add(Number(yearStr));
|
|
}
|
|
|
|
for (const ymd of Array.from(requestedYearMonthDays.values())) {
|
|
const [yearStr, monthStr] = ymd.split('/');
|
|
requestedYears.add(Number(yearStr));
|
|
requestedYearMonths.add(`${yearStr}/${monthStr}`);
|
|
}
|
|
|
|
return {
|
|
requestedPostIds,
|
|
requestedCategorySet: new Set(
|
|
Array.from(requestedCategories.values()).filter((category) => allCategories.has(category)),
|
|
),
|
|
requestedTagSet: new Set(
|
|
Array.from(requestedTags.values()).filter((tag) => allTags.has(tag)),
|
|
),
|
|
requestedYears,
|
|
requestedYearMonths,
|
|
requestedYearMonthDays,
|
|
requestedPageSlugs: new Set(initialPlan.requestedPageSlugs),
|
|
requestRootRoutes: initialPlan.requestRootRoutes,
|
|
};
|
|
}
|