650 lines
20 KiB
JavaScript
650 lines
20 KiB
JavaScript
import fsMod from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { parseFrontmatter } from "@astrojs/markdown-remark";
|
|
import { slug as githubSlug } from "github-slugger";
|
|
import { green } from "kleur/colors";
|
|
import xxhash from "xxhash-wasm";
|
|
import { z } from "zod";
|
|
import { AstroError, AstroErrorData, errorMap, MarkdownError } from "../core/errors/index.js";
|
|
import { isYAMLException } from "../core/errors/utils.js";
|
|
import { appendForwardSlash } from "../core/path.js";
|
|
import { normalizePath } from "../core/viteUtils.js";
|
|
import {
|
|
CONTENT_LAYER_TYPE,
|
|
CONTENT_MODULE_FLAG,
|
|
DEFERRED_MODULE,
|
|
IMAGE_IMPORT_PREFIX,
|
|
LIVE_CONTENT_TYPE,
|
|
PROPAGATED_ASSET_FLAG
|
|
} from "./consts.js";
|
|
import { glob } from "./loaders/glob.js";
|
|
import { createImage } from "./runtime-assets.js";
|
|
const entryTypeSchema = z.object({
|
|
id: z.string({
|
|
invalid_type_error: "Content entry `id` must be a string"
|
|
// Default to empty string so we can validate properly in the loader
|
|
})
|
|
}).passthrough();
|
|
const loaderReturnSchema = z.union([
|
|
z.array(entryTypeSchema),
|
|
z.record(
|
|
z.string(),
|
|
z.object({
|
|
id: z.string({
|
|
invalid_type_error: "Content entry `id` must be a string"
|
|
}).optional()
|
|
}).passthrough()
|
|
)
|
|
]);
|
|
const collectionConfigParser = z.union([
|
|
z.object({
|
|
type: z.literal("content").optional().default("content"),
|
|
schema: z.any().optional()
|
|
}),
|
|
z.object({
|
|
type: z.literal("data"),
|
|
schema: z.any().optional()
|
|
}),
|
|
z.object({
|
|
type: z.literal(CONTENT_LAYER_TYPE),
|
|
schema: z.any().optional(),
|
|
loader: z.union([
|
|
z.function(),
|
|
z.object({
|
|
name: z.string(),
|
|
load: z.function(
|
|
z.tuple(
|
|
[
|
|
z.object({
|
|
collection: z.string(),
|
|
store: z.any(),
|
|
meta: z.any(),
|
|
logger: z.any(),
|
|
config: z.any(),
|
|
entryTypes: z.any(),
|
|
parseData: z.any(),
|
|
renderMarkdown: z.any(),
|
|
generateDigest: z.function(z.tuple([z.any()], z.string())),
|
|
watcher: z.any().optional(),
|
|
refreshContextData: z.record(z.unknown()).optional()
|
|
})
|
|
],
|
|
z.unknown()
|
|
)
|
|
),
|
|
schema: z.any().optional(),
|
|
render: z.function(z.tuple([z.any()], z.unknown())).optional()
|
|
})
|
|
]),
|
|
/** deprecated */
|
|
_legacy: z.boolean().optional()
|
|
}),
|
|
z.object({
|
|
type: z.literal(LIVE_CONTENT_TYPE).optional().default(LIVE_CONTENT_TYPE),
|
|
schema: z.any().optional(),
|
|
loader: z.function()
|
|
})
|
|
]);
|
|
const contentConfigParser = z.object({
|
|
collections: z.record(collectionConfigParser)
|
|
});
|
|
function parseEntrySlug({
|
|
id,
|
|
collection,
|
|
generatedSlug,
|
|
frontmatterSlug
|
|
}) {
|
|
try {
|
|
return z.string().default(generatedSlug).parse(frontmatterSlug);
|
|
} catch {
|
|
throw new AstroError({
|
|
...AstroErrorData.InvalidContentEntrySlugError,
|
|
message: AstroErrorData.InvalidContentEntrySlugError.message(collection, id)
|
|
});
|
|
}
|
|
}
|
|
async function getEntryDataAndImages(entry, collectionConfig, shouldEmitFile, experimentalSvgEnabled, pluginContext) {
|
|
let data;
|
|
if (collectionConfig.type === "content" || collectionConfig._legacy) {
|
|
const { slug, ...unvalidatedData } = entry.unvalidatedData;
|
|
data = unvalidatedData;
|
|
} else {
|
|
data = entry.unvalidatedData;
|
|
}
|
|
let schema = collectionConfig.schema;
|
|
const imageImports = /* @__PURE__ */ new Set();
|
|
if (typeof schema === "function") {
|
|
if (pluginContext) {
|
|
schema = schema({
|
|
image: createImage(
|
|
pluginContext,
|
|
shouldEmitFile,
|
|
entry._internal.filePath,
|
|
experimentalSvgEnabled
|
|
)
|
|
});
|
|
} else if (collectionConfig.type === CONTENT_LAYER_TYPE) {
|
|
schema = schema({
|
|
image: () => z.string().transform((val) => {
|
|
imageImports.add(val);
|
|
return `${IMAGE_IMPORT_PREFIX}${val}`;
|
|
})
|
|
});
|
|
}
|
|
}
|
|
if (schema) {
|
|
if (collectionConfig.type === "content" && typeof schema === "object" && "shape" in schema && schema.shape.slug) {
|
|
throw new AstroError({
|
|
...AstroErrorData.ContentSchemaContainsSlugError,
|
|
message: AstroErrorData.ContentSchemaContainsSlugError.message(entry.collection)
|
|
});
|
|
}
|
|
let formattedError;
|
|
const parsed = await schema.safeParseAsync(data, {
|
|
errorMap(error, ctx) {
|
|
if (error.code === "custom" && error.params?.isHoistedAstroError) {
|
|
formattedError = error.params?.astroError;
|
|
}
|
|
return errorMap(error, ctx);
|
|
}
|
|
});
|
|
if (parsed.success) {
|
|
data = parsed.data;
|
|
} else {
|
|
if (!formattedError) {
|
|
const errorType = collectionConfig.type === "content" ? AstroErrorData.InvalidContentEntryFrontmatterError : AstroErrorData.InvalidContentEntryDataError;
|
|
formattedError = new AstroError({
|
|
...errorType,
|
|
message: errorType.message(entry.collection, entry.id, parsed.error),
|
|
location: {
|
|
file: entry._internal?.filePath,
|
|
line: getYAMLErrorLine(
|
|
entry._internal?.rawData,
|
|
String(parsed.error.errors[0].path[0])
|
|
),
|
|
column: 0
|
|
}
|
|
});
|
|
}
|
|
throw formattedError;
|
|
}
|
|
}
|
|
return { data, imageImports: Array.from(imageImports) };
|
|
}
|
|
async function getEntryData(entry, collectionConfig, shouldEmitFile, experimentalSvgEnabled, pluginContext) {
|
|
const { data } = await getEntryDataAndImages(
|
|
entry,
|
|
collectionConfig,
|
|
shouldEmitFile,
|
|
experimentalSvgEnabled,
|
|
pluginContext
|
|
);
|
|
return data;
|
|
}
|
|
function getContentEntryExts(settings) {
|
|
return settings.contentEntryTypes.map((t) => t.extensions).flat();
|
|
}
|
|
function getDataEntryExts(settings) {
|
|
return settings.dataEntryTypes.map((t) => t.extensions).flat();
|
|
}
|
|
function getEntryConfigByExtMap(entryTypes) {
|
|
const map = /* @__PURE__ */ new Map();
|
|
for (const entryType of entryTypes) {
|
|
for (const ext of entryType.extensions) {
|
|
map.set(ext, entryType);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
async function getSymlinkedContentCollections({
|
|
contentDir,
|
|
logger,
|
|
fs
|
|
}) {
|
|
const contentPaths = /* @__PURE__ */ new Map();
|
|
const contentDirPath = fileURLToPath(contentDir);
|
|
try {
|
|
if (!fs.existsSync(contentDirPath) || !fs.lstatSync(contentDirPath).isDirectory()) {
|
|
return contentPaths;
|
|
}
|
|
} catch {
|
|
return contentPaths;
|
|
}
|
|
try {
|
|
const contentDirEntries = await fs.promises.readdir(contentDir, { withFileTypes: true });
|
|
for (const entry of contentDirEntries) {
|
|
if (entry.isSymbolicLink()) {
|
|
const entryPath = path.join(contentDirPath, entry.name);
|
|
const realPath = await fs.promises.realpath(entryPath);
|
|
contentPaths.set(normalizePath(realPath), entry.name);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logger.warn("content", `Error when reading content directory "${contentDir}"`);
|
|
logger.debug("content", e);
|
|
return /* @__PURE__ */ new Map();
|
|
}
|
|
return contentPaths;
|
|
}
|
|
function reverseSymlink({
|
|
entry,
|
|
symlinks,
|
|
contentDir
|
|
}) {
|
|
const entryPath = normalizePath(typeof entry === "string" ? entry : fileURLToPath(entry));
|
|
const contentDirPath = typeof contentDir === "string" ? contentDir : fileURLToPath(contentDir);
|
|
if (!symlinks || symlinks.size === 0) {
|
|
return entryPath;
|
|
}
|
|
for (const [realPath, symlinkName] of symlinks) {
|
|
if (entryPath.startsWith(realPath)) {
|
|
return normalizePath(path.join(contentDirPath, symlinkName, entryPath.replace(realPath, "")));
|
|
}
|
|
}
|
|
return entryPath;
|
|
}
|
|
function getEntryCollectionName({
|
|
contentDir,
|
|
entry
|
|
}) {
|
|
const entryPath = typeof entry === "string" ? entry : fileURLToPath(entry);
|
|
const rawRelativePath = path.relative(fileURLToPath(contentDir), entryPath);
|
|
const collectionName = path.dirname(rawRelativePath).split(path.sep)[0];
|
|
const isOutsideCollection = !collectionName || collectionName === "" || collectionName === ".." || collectionName === ".";
|
|
if (isOutsideCollection) {
|
|
return void 0;
|
|
}
|
|
return collectionName;
|
|
}
|
|
function getDataEntryId({
|
|
entry,
|
|
contentDir,
|
|
collection
|
|
}) {
|
|
const relativePath = getRelativeEntryPath(entry, collection, contentDir);
|
|
const withoutFileExt = normalizePath(relativePath).replace(
|
|
new RegExp(path.extname(relativePath) + "$"),
|
|
""
|
|
);
|
|
return withoutFileExt;
|
|
}
|
|
function getContentEntryIdAndSlug({
|
|
entry,
|
|
contentDir,
|
|
collection
|
|
}) {
|
|
const relativePath = getRelativeEntryPath(entry, collection, contentDir);
|
|
const withoutFileExt = relativePath.replace(new RegExp(path.extname(relativePath) + "$"), "");
|
|
const rawSlugSegments = withoutFileExt.split(path.sep);
|
|
const slug = rawSlugSegments.map((segment) => githubSlug(segment)).join("/").replace(/\/index$/, "");
|
|
const res = {
|
|
id: normalizePath(relativePath),
|
|
slug
|
|
};
|
|
return res;
|
|
}
|
|
function getRelativeEntryPath(entry, collection, contentDir) {
|
|
const relativeToContent = path.relative(fileURLToPath(contentDir), fileURLToPath(entry));
|
|
const relativeToCollection = path.relative(collection, relativeToContent);
|
|
return relativeToCollection;
|
|
}
|
|
function isParentDirectory(parent, child) {
|
|
const relative = path.relative(fileURLToPath(parent), fileURLToPath(child));
|
|
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
}
|
|
function getEntryType(entryPath, paths, contentFileExts, dataFileExts) {
|
|
const { ext } = path.parse(entryPath);
|
|
const fileUrl = pathToFileURL(entryPath);
|
|
const dotAstroDir = new URL("./.astro/", paths.root);
|
|
if (fileUrl.href === paths.config.url.href) {
|
|
return "config";
|
|
} else if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir)) {
|
|
return "ignored";
|
|
} else if (isParentDirectory(dotAstroDir, fileUrl)) {
|
|
return "ignored";
|
|
} else if (contentFileExts.includes(ext)) {
|
|
return "content";
|
|
} else if (dataFileExts.includes(ext)) {
|
|
return "data";
|
|
} else {
|
|
return "ignored";
|
|
}
|
|
}
|
|
function hasUnderscoreBelowContentDirectoryPath(fileUrl, contentDir) {
|
|
const parts = fileUrl.pathname.replace(contentDir.pathname, "").split("/");
|
|
for (const part of parts) {
|
|
if (part.startsWith("_")) return true;
|
|
}
|
|
return false;
|
|
}
|
|
function getYAMLErrorLine(rawData, objectKey) {
|
|
if (!rawData) return 0;
|
|
const indexOfObjectKey = rawData.search(
|
|
// Match key either at the top of the file or after a newline
|
|
// Ensures matching on top-level object keys only
|
|
new RegExp(`(
|
|
|^)${objectKey}`)
|
|
);
|
|
if (indexOfObjectKey === -1) return 0;
|
|
const dataBeforeKey = rawData.substring(0, indexOfObjectKey + 1);
|
|
const numNewlinesBeforeKey = dataBeforeKey.split("\n").length;
|
|
return numNewlinesBeforeKey;
|
|
}
|
|
function safeParseFrontmatter(source, id) {
|
|
try {
|
|
return parseFrontmatter(source, { frontmatter: "empty-with-spaces" });
|
|
} catch (err) {
|
|
const markdownError = new MarkdownError({
|
|
name: "MarkdownError",
|
|
message: err.message,
|
|
stack: err.stack,
|
|
location: id ? {
|
|
file: id
|
|
} : void 0
|
|
});
|
|
if (isYAMLException(err)) {
|
|
markdownError.setLocation({
|
|
file: id,
|
|
line: err.mark.line,
|
|
column: err.mark.column
|
|
});
|
|
markdownError.setMessage(err.reason);
|
|
}
|
|
throw markdownError;
|
|
}
|
|
}
|
|
const globalContentConfigObserver = contentObservable({ status: "init" });
|
|
function hasContentFlag(viteId, flag) {
|
|
const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
|
|
return flags.has(flag);
|
|
}
|
|
function isDeferredModule(viteId) {
|
|
const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
|
|
return flags.has(CONTENT_MODULE_FLAG);
|
|
}
|
|
async function loadContentConfig({
|
|
fs,
|
|
settings,
|
|
viteServer
|
|
}) {
|
|
const contentPaths = getContentPaths(settings.config, fs);
|
|
let unparsedConfig;
|
|
if (!contentPaths.config.exists) {
|
|
return void 0;
|
|
}
|
|
const configPathname = fileURLToPath(contentPaths.config.url);
|
|
unparsedConfig = await viteServer.ssrLoadModule(configPathname);
|
|
const config = contentConfigParser.safeParse(unparsedConfig);
|
|
if (config.success) {
|
|
const hasher = await xxhash();
|
|
const digest = await hasher.h64ToString(await fs.promises.readFile(configPathname, "utf-8"));
|
|
return { ...config.data, digest };
|
|
} else {
|
|
return void 0;
|
|
}
|
|
}
|
|
async function autogenerateCollections({
|
|
config,
|
|
settings,
|
|
fs
|
|
}) {
|
|
if (settings.config.legacy.collections) {
|
|
return config;
|
|
}
|
|
const contentDir = new URL("./content/", settings.config.srcDir);
|
|
const collections = config?.collections ?? {};
|
|
const contentExts = getContentEntryExts(settings);
|
|
const dataExts = getDataEntryExts(settings);
|
|
const contentPattern = globWithUnderscoresIgnored("", contentExts);
|
|
const dataPattern = globWithUnderscoresIgnored("", dataExts);
|
|
let usesContentLayer = false;
|
|
for (const collectionName of Object.keys(collections)) {
|
|
if (collections[collectionName]?.type === "content_layer" || collections[collectionName]?.type === "live") {
|
|
usesContentLayer = true;
|
|
continue;
|
|
}
|
|
const isDataCollection = collections[collectionName]?.type === "data";
|
|
const base = new URL(`${collectionName}/`, contentDir);
|
|
const _legacy = !isDataCollection || void 0;
|
|
collections[collectionName] = {
|
|
...collections[collectionName],
|
|
type: "content_layer",
|
|
_legacy,
|
|
loader: glob({
|
|
base,
|
|
pattern: isDataCollection ? dataPattern : contentPattern,
|
|
_legacy,
|
|
// Legacy data collections IDs aren't slugified
|
|
generateId: isDataCollection ? ({ entry }) => getDataEntryId({
|
|
entry: new URL(entry, base),
|
|
collection: collectionName,
|
|
contentDir
|
|
}) : void 0
|
|
// Zod weirdness has trouble with typing the args to the load function
|
|
})
|
|
};
|
|
}
|
|
if (!usesContentLayer && fs.existsSync(contentDir)) {
|
|
const orphanedCollections = [];
|
|
for (const entry of await fs.promises.readdir(contentDir, { withFileTypes: true })) {
|
|
const collectionName = entry.name;
|
|
if (["_", "."].includes(collectionName.at(0) ?? "")) {
|
|
continue;
|
|
}
|
|
if (entry.isDirectory() && !(collectionName in collections)) {
|
|
orphanedCollections.push(collectionName);
|
|
const base = new URL(`${collectionName}/`, contentDir);
|
|
collections[collectionName] = {
|
|
type: "content_layer",
|
|
loader: glob({
|
|
base,
|
|
pattern: contentPattern,
|
|
_legacy: true
|
|
})
|
|
};
|
|
}
|
|
}
|
|
if (orphanedCollections.length > 0) {
|
|
console.warn(
|
|
`
|
|
Auto-generating collections for folders in "src/content/" that are not defined as collections.
|
|
This is deprecated, so you should define these collections yourself in "src/content.config.ts".
|
|
The following collections have been auto-generated: ${orphanedCollections.map((name) => green(name)).join(", ")}
|
|
`
|
|
);
|
|
}
|
|
}
|
|
return { ...config, collections };
|
|
}
|
|
async function reloadContentConfigObserver({
|
|
observer = globalContentConfigObserver,
|
|
...loadContentConfigOpts
|
|
}) {
|
|
observer.set({ status: "loading" });
|
|
try {
|
|
let config = await loadContentConfig(loadContentConfigOpts);
|
|
config = await autogenerateCollections({
|
|
config,
|
|
...loadContentConfigOpts
|
|
});
|
|
if (config) {
|
|
observer.set({ status: "loaded", config });
|
|
} else {
|
|
observer.set({ status: "does-not-exist" });
|
|
}
|
|
} catch (e) {
|
|
observer.set({
|
|
status: "error",
|
|
error: e instanceof Error ? e : new AstroError(AstroErrorData.UnknownContentCollectionError)
|
|
});
|
|
}
|
|
}
|
|
function contentObservable(initialCtx) {
|
|
const subscribers = /* @__PURE__ */ new Set();
|
|
let ctx = initialCtx;
|
|
function get() {
|
|
return ctx;
|
|
}
|
|
function set(_ctx) {
|
|
ctx = _ctx;
|
|
subscribers.forEach((fn) => fn(ctx));
|
|
}
|
|
function subscribe(fn) {
|
|
subscribers.add(fn);
|
|
return () => {
|
|
subscribers.delete(fn);
|
|
};
|
|
}
|
|
return {
|
|
get,
|
|
set,
|
|
subscribe
|
|
};
|
|
}
|
|
function getContentPaths({
|
|
srcDir,
|
|
legacy,
|
|
root,
|
|
experimental
|
|
}, fs = fsMod) {
|
|
const configStats = searchConfig(fs, srcDir, legacy?.collections);
|
|
const liveConfigStats = experimental?.liveContentCollections ? searchLiveConfig(fs, srcDir) : { exists: false, url: new URL("./", srcDir) };
|
|
const pkgBase = new URL("../../", import.meta.url);
|
|
return {
|
|
root: new URL("./", root),
|
|
contentDir: new URL("./content/", srcDir),
|
|
assetsDir: new URL("./assets/", srcDir),
|
|
typesTemplate: new URL("templates/content/types.d.ts", pkgBase),
|
|
virtualModTemplate: new URL("templates/content/module.mjs", pkgBase),
|
|
config: configStats,
|
|
liveConfig: liveConfigStats
|
|
};
|
|
}
|
|
function searchConfig(fs, srcDir, legacy) {
|
|
const paths = [
|
|
...legacy ? [] : ["content.config.mjs", "content.config.js", "content.config.mts", "content.config.ts"],
|
|
"content/config.mjs",
|
|
"content/config.js",
|
|
"content/config.mts",
|
|
"content/config.ts"
|
|
];
|
|
return search(fs, srcDir, paths);
|
|
}
|
|
function searchLiveConfig(fs, srcDir) {
|
|
const paths = ["live.config.mjs", "live.config.js", "live.config.mts", "live.config.ts"];
|
|
return search(fs, srcDir, paths);
|
|
}
|
|
function search(fs, srcDir, paths) {
|
|
const urls = paths.map((p) => new URL(`./${p}`, srcDir));
|
|
for (const file of urls) {
|
|
if (fs.existsSync(file)) {
|
|
return { exists: true, url: file };
|
|
}
|
|
}
|
|
return { exists: false, url: urls[0] };
|
|
}
|
|
async function getEntrySlug({
|
|
id,
|
|
collection,
|
|
generatedSlug,
|
|
contentEntryType,
|
|
fileUrl,
|
|
fs
|
|
}) {
|
|
let contents;
|
|
try {
|
|
contents = await fs.promises.readFile(fileUrl, "utf-8");
|
|
} catch (e) {
|
|
throw new AstroError(AstroErrorData.UnknownContentCollectionError, { cause: e });
|
|
}
|
|
const { slug: frontmatterSlug } = await contentEntryType.getEntryInfo({
|
|
fileUrl,
|
|
contents
|
|
});
|
|
return parseEntrySlug({ generatedSlug, frontmatterSlug, id, collection });
|
|
}
|
|
function getExtGlob(exts) {
|
|
return exts.length === 1 ? (
|
|
// Wrapping {...} breaks when there is only one extension
|
|
exts[0]
|
|
) : `{${exts.join(",")}}`;
|
|
}
|
|
function hasAssetPropagationFlag(id) {
|
|
try {
|
|
return new URL(id, "file://").searchParams.has(PROPAGATED_ASSET_FLAG);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function globWithUnderscoresIgnored(relContentDir, exts) {
|
|
const extGlob = getExtGlob(exts);
|
|
const contentDir = relContentDir.length > 0 ? appendForwardSlash(relContentDir) : relContentDir;
|
|
return [
|
|
`${contentDir}**/*${extGlob}`,
|
|
`!${contentDir}**/_*/**/*${extGlob}`,
|
|
`!${contentDir}**/_*${extGlob}`
|
|
];
|
|
}
|
|
function posixifyPath(filePath) {
|
|
return filePath.split(path.sep).join("/");
|
|
}
|
|
function posixRelative(from, to) {
|
|
return posixifyPath(path.relative(from, to));
|
|
}
|
|
function contentModuleToId(fileName) {
|
|
const params = new URLSearchParams(DEFERRED_MODULE);
|
|
params.set("fileName", fileName);
|
|
params.set(CONTENT_MODULE_FLAG, "true");
|
|
return `${DEFERRED_MODULE}?${params.toString()}`;
|
|
}
|
|
function safeStringifyReplacer(seen) {
|
|
return function(_key, value) {
|
|
if (!(value !== null && typeof value === "object")) {
|
|
return value;
|
|
}
|
|
if (seen.has(value)) {
|
|
return "[Circular]";
|
|
}
|
|
seen.add(value);
|
|
const newValue = Array.isArray(value) ? [] : {};
|
|
for (const [key2, value2] of Object.entries(value)) {
|
|
newValue[key2] = safeStringifyReplacer(seen)(key2, value2);
|
|
}
|
|
seen.delete(value);
|
|
return newValue;
|
|
};
|
|
}
|
|
function safeStringify(value) {
|
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
return JSON.stringify(value, safeStringifyReplacer(seen));
|
|
}
|
|
export {
|
|
contentModuleToId,
|
|
getContentEntryExts,
|
|
getContentEntryIdAndSlug,
|
|
getContentPaths,
|
|
getDataEntryExts,
|
|
getDataEntryId,
|
|
getEntryCollectionName,
|
|
getEntryConfigByExtMap,
|
|
getEntryData,
|
|
getEntryDataAndImages,
|
|
getEntrySlug,
|
|
getEntryType,
|
|
getExtGlob,
|
|
getSymlinkedContentCollections,
|
|
globWithUnderscoresIgnored,
|
|
globalContentConfigObserver,
|
|
hasAssetPropagationFlag,
|
|
hasContentFlag,
|
|
isDeferredModule,
|
|
loaderReturnSchema,
|
|
parseEntrySlug,
|
|
posixRelative,
|
|
reloadContentConfigObserver,
|
|
reverseSymlink,
|
|
safeParseFrontmatter,
|
|
safeStringify
|
|
};
|