feat: add generated lookup-map
This commit is contained in:
parent
4ea716e569
commit
ddc9afe36b
6 changed files with 100 additions and 37 deletions
|
@ -14,6 +14,7 @@ import {
|
||||||
|
|
||||||
type GlobResult = Record<string, () => Promise<any>>;
|
type GlobResult = Record<string, () => Promise<any>>;
|
||||||
type CollectionToEntryMap = Record<string, GlobResult>;
|
type CollectionToEntryMap = Record<string, GlobResult>;
|
||||||
|
type GetEntryImport = (collection: string, lookupId: string) => () => Promise<any>;
|
||||||
|
|
||||||
export function createCollectionToGlobResultMap({
|
export function createCollectionToGlobResultMap({
|
||||||
globResult,
|
globResult,
|
||||||
|
@ -28,9 +29,8 @@ export function createCollectionToGlobResultMap({
|
||||||
const segments = keyRelativeToContentDir.split('/');
|
const segments = keyRelativeToContentDir.split('/');
|
||||||
if (segments.length <= 1) continue;
|
if (segments.length <= 1) continue;
|
||||||
const collection = segments[0];
|
const collection = segments[0];
|
||||||
const entryId = segments.slice(1).join('/');
|
|
||||||
collectionToGlobResultMap[collection] ??= {};
|
collectionToGlobResultMap[collection] ??= {};
|
||||||
collectionToGlobResultMap[collection][entryId] = globResult[key];
|
collectionToGlobResultMap[collection][key] = globResult[key];
|
||||||
}
|
}
|
||||||
return collectionToGlobResultMap;
|
return collectionToGlobResultMap;
|
||||||
}
|
}
|
||||||
|
@ -38,10 +38,10 @@ export function createCollectionToGlobResultMap({
|
||||||
const cacheEntriesByCollection = new Map<string, any[]>();
|
const cacheEntriesByCollection = new Map<string, any[]>();
|
||||||
export function createGetCollection({
|
export function createGetCollection({
|
||||||
collectionToEntryMap,
|
collectionToEntryMap,
|
||||||
collectionToRenderEntryMap,
|
getRenderEntryImport,
|
||||||
}: {
|
}: {
|
||||||
collectionToEntryMap: CollectionToEntryMap;
|
collectionToEntryMap: CollectionToEntryMap;
|
||||||
collectionToRenderEntryMap: CollectionToEntryMap;
|
getRenderEntryImport: GetEntryImport;
|
||||||
}) {
|
}) {
|
||||||
return async function getCollection(collection: string, filter?: (entry: any) => unknown) {
|
return async function getCollection(collection: string, filter?: (entry: any) => unknown) {
|
||||||
const lazyImports = Object.values(collectionToEntryMap[collection] ?? {});
|
const lazyImports = Object.values(collectionToEntryMap[collection] ?? {});
|
||||||
|
@ -64,7 +64,7 @@ export function createGetCollection({
|
||||||
return render({
|
return render({
|
||||||
collection: entry.collection,
|
collection: entry.collection,
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
collectionToRenderEntryMap,
|
renderEntryImport: await getRenderEntryImport(collection, entry.slug),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -82,10 +82,10 @@ export function createGetCollection({
|
||||||
|
|
||||||
export function createGetEntryBySlug({
|
export function createGetEntryBySlug({
|
||||||
getCollection,
|
getCollection,
|
||||||
collectionToRenderEntryMap,
|
getRenderEntryImport,
|
||||||
}: {
|
}: {
|
||||||
getCollection: ReturnType<typeof createGetCollection>;
|
getCollection: ReturnType<typeof createGetCollection>;
|
||||||
collectionToRenderEntryMap: CollectionToEntryMap;
|
getRenderEntryImport: GetEntryImport;
|
||||||
}) {
|
}) {
|
||||||
return async function getEntryBySlug(collection: string, slug: string) {
|
return async function getEntryBySlug(collection: string, slug: string) {
|
||||||
// This is not an optimized lookup. Should look into an O(1) implementation
|
// This is not an optimized lookup. Should look into an O(1) implementation
|
||||||
|
@ -114,7 +114,7 @@ export function createGetEntryBySlug({
|
||||||
return render({
|
return render({
|
||||||
collection: entry.collection,
|
collection: entry.collection,
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
collectionToRenderEntryMap,
|
renderEntryImport: await getRenderEntryImport(collection, entry.slug),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -124,21 +124,20 @@ export function createGetEntryBySlug({
|
||||||
async function render({
|
async function render({
|
||||||
collection,
|
collection,
|
||||||
id,
|
id,
|
||||||
collectionToRenderEntryMap,
|
renderEntryImport,
|
||||||
}: {
|
}: {
|
||||||
collection: string;
|
collection: string;
|
||||||
id: string;
|
id: string;
|
||||||
collectionToRenderEntryMap: CollectionToEntryMap;
|
renderEntryImport?: ReturnType<GetEntryImport>;
|
||||||
}) {
|
}) {
|
||||||
const UnexpectedRenderError = new AstroError({
|
const UnexpectedRenderError = new AstroError({
|
||||||
...AstroErrorData.UnknownContentCollectionError,
|
...AstroErrorData.UnknownContentCollectionError,
|
||||||
message: `Unexpected error while rendering ${String(collection)} → ${String(id)}.`,
|
message: `Unexpected error while rendering ${String(collection)} → ${String(id)}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lazyImport = collectionToRenderEntryMap[collection]?.[id];
|
if (typeof renderEntryImport !== 'function') throw UnexpectedRenderError;
|
||||||
if (typeof lazyImport !== 'function') throw UnexpectedRenderError;
|
|
||||||
|
|
||||||
const baseMod = await lazyImport();
|
const baseMod = await renderEntryImport();
|
||||||
if (baseMod == null || typeof baseMod !== 'object') throw UnexpectedRenderError;
|
if (baseMod == null || typeof baseMod !== 'object') throw UnexpectedRenderError;
|
||||||
|
|
||||||
const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod;
|
const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod;
|
||||||
|
|
|
@ -28,6 +28,16 @@ const collectionToEntryMap = createCollectionToGlobResultMap({
|
||||||
contentDir,
|
contentDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createGlobLookup(entryGlob) {
|
||||||
|
return async (collection, lookupId) => {
|
||||||
|
const { default: lookupMap } = await import('@@LOOKUP_MAP_PATH@@');
|
||||||
|
const filePath = lookupMap[collection]?.[lookupId];
|
||||||
|
|
||||||
|
if (!filePath) return undefined;
|
||||||
|
return entryGlob[collection][filePath];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', {
|
const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', {
|
||||||
query: { astroPropagatedAssets: true },
|
query: { astroPropagatedAssets: true },
|
||||||
});
|
});
|
||||||
|
@ -38,10 +48,10 @@ const collectionToRenderEntryMap = createCollectionToGlobResultMap({
|
||||||
|
|
||||||
export const getCollection = createGetCollection({
|
export const getCollection = createGetCollection({
|
||||||
collectionToEntryMap,
|
collectionToEntryMap,
|
||||||
collectionToRenderEntryMap,
|
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getEntryBySlug = createGetEntryBySlug({
|
export const getEntryBySlug = createGetEntryBySlug({
|
||||||
getCollection,
|
getCollection,
|
||||||
collectionToRenderEntryMap,
|
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
type ContentObservable,
|
type ContentObservable,
|
||||||
type ContentPaths,
|
type ContentPaths,
|
||||||
type EntryInfo,
|
type EntryInfo,
|
||||||
|
updateLookupMaps,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
|
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
|
||||||
|
@ -278,6 +279,12 @@ export async function createContentTypesGenerator({
|
||||||
contentConfig: observable.status === 'loaded' ? observable.config : undefined,
|
contentConfig: observable.status === 'loaded' ? observable.config : undefined,
|
||||||
contentEntryTypes: settings.contentEntryTypes,
|
contentEntryTypes: settings.contentEntryTypes,
|
||||||
});
|
});
|
||||||
|
await updateLookupMaps({
|
||||||
|
contentEntryExts,
|
||||||
|
contentPaths,
|
||||||
|
root: settings.config.root,
|
||||||
|
fs,
|
||||||
|
});
|
||||||
if (observable.status === 'loaded' && ['info', 'warn'].includes(logLevel)) {
|
if (observable.status === 'loaded' && ['info', 'warn'].includes(logLevel)) {
|
||||||
warnNonexistentCollections({
|
warnNonexistentCollections({
|
||||||
logging,
|
logging,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
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';
|
||||||
|
@ -12,6 +13,7 @@ import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import { CONTENT_TYPES_FILE } from './consts.js';
|
import { CONTENT_TYPES_FILE } from './consts.js';
|
||||||
import { errorMap } from './error-map.js';
|
import { errorMap } from './error-map.js';
|
||||||
import { createImage } from './runtime-assets.js';
|
import { createImage } from './runtime-assets.js';
|
||||||
|
import { rootRelativePath } from '../core/util.js';
|
||||||
|
|
||||||
export const collectionConfigParser = z.object({
|
export const collectionConfigParser = z.object({
|
||||||
schema: z.any().optional(),
|
schema: z.any().optional(),
|
||||||
|
@ -129,16 +131,23 @@ export function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryT
|
||||||
export class NoCollectionError extends Error {}
|
export class NoCollectionError extends Error {}
|
||||||
|
|
||||||
export function getEntryInfo(
|
export function getEntryInfo(
|
||||||
params: Pick<ContentPaths, 'contentDir'> & { entry: URL; allowFilesOutsideCollection?: true }
|
params: Pick<ContentPaths, 'contentDir'> & {
|
||||||
|
entry: string | URL;
|
||||||
|
allowFilesOutsideCollection?: true;
|
||||||
|
}
|
||||||
): EntryInfo;
|
): EntryInfo;
|
||||||
export function getEntryInfo({
|
export function getEntryInfo({
|
||||||
entry,
|
entry,
|
||||||
contentDir,
|
contentDir,
|
||||||
allowFilesOutsideCollection = false,
|
allowFilesOutsideCollection = false,
|
||||||
}: Pick<ContentPaths, 'contentDir'> & { entry: URL; allowFilesOutsideCollection?: boolean }):
|
}: Pick<ContentPaths, 'contentDir'> & {
|
||||||
| EntryInfo
|
entry: string | URL;
|
||||||
| NoCollectionError {
|
allowFilesOutsideCollection?: boolean;
|
||||||
const rawRelativePath = path.relative(fileURLToPath(contentDir), fileURLToPath(entry));
|
}): EntryInfo | NoCollectionError {
|
||||||
|
const rawRelativePath = path.relative(
|
||||||
|
fileURLToPath(contentDir),
|
||||||
|
typeof entry === 'string' ? entry : fileURLToPath(entry)
|
||||||
|
);
|
||||||
const rawCollection = path.dirname(rawRelativePath).split(path.sep).shift();
|
const rawCollection = path.dirname(rawRelativePath).split(path.sep).shift();
|
||||||
const isOutsideCollection = rawCollection === '..' || rawCollection === '.';
|
const isOutsideCollection = rawCollection === '..' || rawCollection === '.';
|
||||||
|
|
||||||
|
@ -358,3 +367,51 @@ function search(fs: typeof fsMod, srcDir: URL) {
|
||||||
}
|
}
|
||||||
return { exists: false, url: paths[0] };
|
return { exists: false, url: paths[0] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateLookupMaps({
|
||||||
|
contentPaths,
|
||||||
|
contentEntryExts,
|
||||||
|
root,
|
||||||
|
fs,
|
||||||
|
}: {
|
||||||
|
contentEntryExts: string[];
|
||||||
|
contentPaths: Pick<ContentPaths, 'contentDir' | 'cacheDir'>;
|
||||||
|
root: URL;
|
||||||
|
fs: typeof fsMod;
|
||||||
|
}) {
|
||||||
|
const { contentDir } = contentPaths;
|
||||||
|
const globOpts: FastGlobOptions = {
|
||||||
|
absolute: false,
|
||||||
|
cwd: fileURLToPath(root),
|
||||||
|
fs: {
|
||||||
|
readdir: fs.readdir.bind(fs),
|
||||||
|
readdirSync: fs.readdirSync.bind(fs),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const relContentDir = rootRelativePath(root, contentDir, false);
|
||||||
|
const contentGlob = await glob(`${relContentDir}/**/*${getExtGlob(contentEntryExts)}`, globOpts);
|
||||||
|
let filePathByLookupId: {
|
||||||
|
[collection: string]: Record<string, string>;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
new URL('lookup-map.json', contentPaths.cacheDir),
|
||||||
|
JSON.stringify(filePathByLookupId, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExtGlob(exts: string[]) {
|
||||||
|
return exts.length === 1
|
||||||
|
? // Wrapping {...} breaks when there is only one extension
|
||||||
|
exts[0]
|
||||||
|
: `{${exts.join(',')}}`;
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import fsMod from 'node:fs';
|
import fsMod from 'node:fs';
|
||||||
import * as path from 'node:path';
|
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { normalizePath } from 'vite';
|
|
||||||
import type { AstroSettings } from '../@types/astro.js';
|
import type { AstroSettings } from '../@types/astro.js';
|
||||||
import { appendForwardSlash, prependForwardSlash } from '../core/path.js';
|
|
||||||
import { VIRTUAL_MODULE_ID } from './consts.js';
|
import { VIRTUAL_MODULE_ID } from './consts.js';
|
||||||
import { getContentEntryExts, getContentPaths } from './utils.js';
|
import { getContentEntryExts, getContentPaths, getExtGlob } from './utils.js';
|
||||||
|
import { rootRelativePath } from '../core/util.js';
|
||||||
|
|
||||||
interface AstroContentVirtualModPluginParams {
|
interface AstroContentVirtualModPluginParams {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
|
@ -15,13 +13,7 @@ export function astroContentVirtualModPlugin({
|
||||||
settings,
|
settings,
|
||||||
}: AstroContentVirtualModPluginParams): Plugin {
|
}: AstroContentVirtualModPluginParams): Plugin {
|
||||||
const contentPaths = getContentPaths(settings.config);
|
const contentPaths = getContentPaths(settings.config);
|
||||||
const relContentDir = normalizePath(
|
const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);
|
||||||
appendForwardSlash(
|
|
||||||
prependForwardSlash(
|
|
||||||
path.relative(settings.config.root.pathname, contentPaths.contentDir.pathname)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const contentEntryExts = getContentEntryExts(settings);
|
const contentEntryExts = getContentEntryExts(settings);
|
||||||
|
|
||||||
const extGlob =
|
const extGlob =
|
||||||
|
@ -32,6 +24,7 @@ export function astroContentVirtualModPlugin({
|
||||||
const entryGlob = `${relContentDir}**/*${extGlob}`;
|
const entryGlob = `${relContentDir}**/*${extGlob}`;
|
||||||
const virtualModContents = fsMod
|
const virtualModContents = fsMod
|
||||||
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
|
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
|
||||||
|
.replace('@@LOOKUP_MAP_PATH@@', new URL('lookup-map.json', contentPaths.cacheDir).pathname)
|
||||||
.replace('@@CONTENT_DIR@@', relContentDir)
|
.replace('@@CONTENT_DIR@@', relContentDir)
|
||||||
.replace('@@ENTRY_GLOB_PATH@@', entryGlob)
|
.replace('@@ENTRY_GLOB_PATH@@', entryGlob)
|
||||||
.replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob);
|
.replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob);
|
||||||
|
|
|
@ -151,18 +151,15 @@ export function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string) {
|
||||||
return id.slice(slash(fileURLToPath(config.srcDir)).length);
|
return id.slice(slash(fileURLToPath(config.srcDir)).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rootRelativePath(root: URL, idOrUrl: URL | string) {
|
export function rootRelativePath(root: URL, idOrUrl: URL | string, prependSlash = true) {
|
||||||
let id: string;
|
let id: string;
|
||||||
if (typeof idOrUrl !== 'string') {
|
if (typeof idOrUrl !== 'string') {
|
||||||
id = unwrapId(viteID(idOrUrl));
|
id = unwrapId(viteID(idOrUrl));
|
||||||
} else {
|
} else {
|
||||||
id = idOrUrl;
|
id = idOrUrl;
|
||||||
}
|
}
|
||||||
const normalizedRoot = normalizePath(fileURLToPath(root));
|
id = id.slice(normalizePath(fileURLToPath(root)).length);
|
||||||
if (id.startsWith(normalizedRoot)) {
|
return prependSlash ? prependForwardSlash(id) : id;
|
||||||
id = id.slice(normalizedRoot.length);
|
|
||||||
}
|
|
||||||
return prependForwardSlash(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emoji(char: string, fallback: string) {
|
export function emoji(char: string, fallback: string) {
|
||||||
|
|
Loading…
Reference in a new issue