full site update

This commit is contained in:
2025-07-24 18:46:24 +02:00
parent bfe2b90d8d
commit 37a6e0ab31
6912 changed files with 540482 additions and 361712 deletions

74
node_modules/astro/dist/content/config.d.ts generated vendored Normal file
View File

@@ -0,0 +1,74 @@
import type { ZodLiteral, ZodNumber, ZodObject, ZodString, ZodType, ZodUnion } from 'zod';
import type { LiveLoader, Loader } from './loaders/types.js';
export type ImageFunction = () => ZodObject<{
src: ZodString;
width: ZodNumber;
height: ZodNumber;
format: ZodUnion<[
ZodLiteral<'png'>,
ZodLiteral<'jpg'>,
ZodLiteral<'jpeg'>,
ZodLiteral<'tiff'>,
ZodLiteral<'webp'>,
ZodLiteral<'gif'>,
ZodLiteral<'svg'>,
ZodLiteral<'avif'>
]>;
}>;
export interface DataEntry {
id: string;
data: Record<string, unknown>;
filePath?: string;
body?: string;
}
export interface DataStore {
get: (key: string) => DataEntry;
entries: () => Array<[id: string, DataEntry]>;
set: (key: string, data: Record<string, unknown>, body?: string, filePath?: string) => void;
values: () => Array<DataEntry>;
keys: () => Array<string>;
delete: (key: string) => void;
clear: () => void;
has: (key: string) => boolean;
}
export interface MetaStore {
get: (key: string) => string | undefined;
set: (key: string, value: string) => void;
delete: (key: string) => void;
has: (key: string) => boolean;
}
export type BaseSchema = ZodType;
export type SchemaContext = {
image: ImageFunction;
};
type ContentLayerConfig<S extends BaseSchema, TData extends {
id: string;
} = {
id: string;
}> = {
type?: 'content_layer';
schema?: S | ((context: SchemaContext) => S);
loader: Loader | (() => Array<TData> | Promise<Array<TData>> | Record<string, Omit<TData, 'id'> & {
id?: string;
}> | Promise<Record<string, Omit<TData, 'id'> & {
id?: string;
}>>);
};
type DataCollectionConfig<S extends BaseSchema> = {
type: 'data';
schema?: S | ((context: SchemaContext) => S);
};
type ContentCollectionConfig<S extends BaseSchema> = {
type?: 'content';
schema?: S | ((context: SchemaContext) => S);
loader?: never;
};
export type LiveCollectionConfig<L extends LiveLoader, S extends BaseSchema | undefined = undefined> = {
type?: 'live';
schema?: S;
loader: L;
};
export type CollectionConfig<S extends BaseSchema> = ContentCollectionConfig<S> | DataCollectionConfig<S> | ContentLayerConfig<S>;
export declare function defineLiveCollection<L extends LiveLoader, S extends BaseSchema | undefined = undefined>(config: LiveCollectionConfig<L, S>): LiveCollectionConfig<L, S>;
export declare function defineCollection<S extends BaseSchema>(config: CollectionConfig<S>): CollectionConfig<S>;
export {};

93
node_modules/astro/dist/content/config.js generated vendored Normal file
View File

@@ -0,0 +1,93 @@
import { AstroError, AstroErrorData, AstroUserError } from "../core/errors/index.js";
import { CONTENT_LAYER_TYPE, LIVE_CONTENT_TYPE } from "./consts.js";
function getImporterFilename() {
const stackLine = new Error().stack?.split("\n").find(
(line) => !line.includes("defineCollection") && !line.includes("defineLiveCollection") && !line.includes("getImporterFilename") && !line.startsWith("Error")
);
if (!stackLine) {
return void 0;
}
const match = /\/((?:src|chunks)\/.*?):\d+:\d+/.exec(stackLine);
return match?.[1] ?? void 0;
}
function defineLiveCollection(config) {
const importerFilename = getImporterFilename();
if (importerFilename && !importerFilename.includes("live.config")) {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Live collections must be defined in a `src/live.config.ts` file.",
importerFilename ?? "your content config file"
)
});
}
config.type ??= LIVE_CONTENT_TYPE;
if (config.type !== LIVE_CONTENT_TYPE) {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Collections in a live config file must have a type of `live`.",
importerFilename
)
});
}
if (!config.loader) {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Live collections must have a `loader` defined.",
importerFilename
)
});
}
if (!config.loader.loadCollection || !config.loader.loadEntry) {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Live collection loaders must have `loadCollection()` and `loadEntry()` methods. Please check that you are not using a loader intended for build-time collections",
importerFilename
)
});
}
if (typeof config.schema === "function") {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"The schema cannot be a function for live collections. Please use a schema object instead.",
importerFilename
)
});
}
return config;
}
function defineCollection(config) {
const importerFilename = getImporterFilename();
if (importerFilename?.includes("live.config")) {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Collections in a live config file must use `defineLiveCollection`.",
importerFilename
)
});
}
if ("loader" in config) {
if (config.type && config.type !== CONTENT_LAYER_TYPE) {
throw new AstroUserError(
`Collections that use the Content Layer API must have a \`loader\` defined and no \`type\` set. Check your collection definitions in ${importerFilename ?? "your content config file"}.`
);
}
if (typeof config.loader === "object" && typeof config.loader.load !== "function" && ("loadEntry" in config.loader || "loadCollection" in config.loader)) {
throw new AstroUserError(
`Live content collections must be defined in "src/live.config.ts" file. Check your collection definitions in "${importerFilename ?? "your content config file"}" to ensure you are not using a live loader.`
);
}
config.type = CONTENT_LAYER_TYPE;
}
if (!config.type) config.type = "content";
return config;
}
export {
defineCollection,
defineLiveCollection
};

View File

@@ -15,11 +15,13 @@ export declare const ASSET_IMPORTS_VIRTUAL_ID = "astro:asset-imports";
export declare const ASSET_IMPORTS_RESOLVED_STUB_ID: string;
export declare const LINKS_PLACEHOLDER = "@@ASTRO-LINKS@@";
export declare const STYLES_PLACEHOLDER = "@@ASTRO-STYLES@@";
export declare const SCRIPTS_PLACEHOLDER = "@@ASTRO-SCRIPTS@@";
export declare const IMAGE_IMPORT_PREFIX = "__ASTRO_IMAGE_";
export declare const CONTENT_FLAGS: readonly ["astroContentCollectionEntry", "astroRenderContent", "astroDataCollectionEntry", "astroPropagatedAssets", "astroContentImageFlag", "astroContentModuleFlag"];
export declare const CONTENT_TYPES_FILE = "astro/content.d.ts";
export declare const CONTENT_TYPES_FILE = "content.d.ts";
export declare const DATA_STORE_FILE = "data-store.json";
export declare const ASSET_IMPORTS_FILE = "assets.mjs";
export declare const MODULES_IMPORTS_FILE = "modules.mjs";
export declare const ASSET_IMPORTS_FILE = "content-assets.mjs";
export declare const MODULES_IMPORTS_FILE = "content-modules.mjs";
export declare const COLLECTIONS_MANIFEST_FILE = "collections/collections.json";
export declare const COLLECTIONS_DIR = "collections/";
export declare const CONTENT_LAYER_TYPE = "content_layer";
export declare const LIVE_CONTENT_TYPE = "live";

View File

@@ -15,7 +15,6 @@ const ASSET_IMPORTS_VIRTUAL_ID = "astro:asset-imports";
const ASSET_IMPORTS_RESOLVED_STUB_ID = "\0" + ASSET_IMPORTS_VIRTUAL_ID;
const LINKS_PLACEHOLDER = "@@ASTRO-LINKS@@";
const STYLES_PLACEHOLDER = "@@ASTRO-STYLES@@";
const SCRIPTS_PLACEHOLDER = "@@ASTRO-SCRIPTS@@";
const IMAGE_IMPORT_PREFIX = "__ASTRO_IMAGE_";
const CONTENT_FLAGS = [
CONTENT_FLAG,
@@ -25,15 +24,20 @@ const CONTENT_FLAGS = [
CONTENT_IMAGE_FLAG,
CONTENT_MODULE_FLAG
];
const CONTENT_TYPES_FILE = "astro/content.d.ts";
const CONTENT_TYPES_FILE = "content.d.ts";
const DATA_STORE_FILE = "data-store.json";
const ASSET_IMPORTS_FILE = "assets.mjs";
const MODULES_IMPORTS_FILE = "modules.mjs";
const ASSET_IMPORTS_FILE = "content-assets.mjs";
const MODULES_IMPORTS_FILE = "content-modules.mjs";
const COLLECTIONS_MANIFEST_FILE = "collections/collections.json";
const COLLECTIONS_DIR = "collections/";
const CONTENT_LAYER_TYPE = "content_layer";
const LIVE_CONTENT_TYPE = "live";
export {
ASSET_IMPORTS_FILE,
ASSET_IMPORTS_RESOLVED_STUB_ID,
ASSET_IMPORTS_VIRTUAL_ID,
COLLECTIONS_DIR,
COLLECTIONS_MANIFEST_FILE,
CONTENT_FLAG,
CONTENT_FLAGS,
CONTENT_IMAGE_FLAG,
@@ -47,13 +51,13 @@ export {
DEFERRED_MODULE,
IMAGE_IMPORT_PREFIX,
LINKS_PLACEHOLDER,
LIVE_CONTENT_TYPE,
MODULES_IMPORTS_FILE,
MODULES_MJS_ID,
MODULES_MJS_VIRTUAL_ID,
PROPAGATED_ASSET_FLAG,
RESOLVED_DATA_STORE_VIRTUAL_ID,
RESOLVED_VIRTUAL_MODULE_ID,
SCRIPTS_PLACEHOLDER,
STYLES_PLACEHOLDER,
VIRTUAL_MODULE_ID
};

View File

@@ -1,15 +1,15 @@
import type { FSWatcher } from 'vite';
import type { AstroSettings, RefreshContentOptions } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import type { LoaderContext } from './loaders/types.js';
import type { AstroSettings } from '../types/astro.js';
import type { RefreshContentOptions } from '../types/public/content.js';
import type { MutableDataStore } from './mutable-data-store.js';
export interface ContentLayerOptions {
interface ContentLayerOptions {
store: MutableDataStore;
settings: AstroSettings;
logger: Logger;
watcher?: FSWatcher;
}
export declare class ContentLayer {
declare class ContentLayer {
#private;
constructor({ settings, logger, store, watcher }: ContentLayerOptions);
/**
@@ -21,6 +21,7 @@ export declare class ContentLayer {
*/
watchContentConfig(): void;
unwatchContentConfig(): void;
dispose(): void;
/**
* Enqueues a sync job that runs the `load()` method of each collection's loader, which will load the data and save it in the data store.
* The loader itself is responsible for deciding whether this will clear and reload the full collection, or
@@ -30,17 +31,15 @@ export declare class ContentLayer {
sync(options?: RefreshContentOptions): Promise<void>;
regenerateCollectionFileManifest(): Promise<void>;
}
export declare function simpleLoader<TData extends {
id: string;
}>(handler: () => Array<TData> | Promise<Array<TData>>, context: LoaderContext): Promise<void>;
/**
* Get the path to the data store file.
* During development, this is in the `.astro` directory so that the Vite watcher can see it.
* In production, it's in the cache directory so that it's preserved between builds.
*/
export declare function getDataStoreFile(settings: AstroSettings, isDev?: boolean): URL;
export declare function getDataStoreFile(settings: AstroSettings, isDev: boolean): URL;
export declare const globalContentLayer: {
init: (options: ContentLayerOptions) => ContentLayer;
get: () => ContentLayer | null;
dispose: () => void;
};
export {};

View File

@@ -1,9 +1,11 @@
import { promises as fs, existsSync } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import { createMarkdownProcessor } from "@astrojs/markdown-remark";
import PQueue from "p-queue";
import xxhash from "xxhash-wasm";
import { AstroUserError } from "../core/errors/errors.js";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import {
ASSET_IMPORTS_FILE,
COLLECTIONS_MANIFEST_FILE,
CONTENT_LAYER_TYPE,
DATA_STORE_FILE,
MODULES_IMPORTS_FILE
@@ -11,8 +13,11 @@ import {
import {
getEntryConfigByExtMap,
getEntryDataAndImages,
globalContentConfigObserver
globalContentConfigObserver,
loaderReturnSchema,
safeStringify
} from "./utils.js";
import { createWatcherWrapper } from "./watcher.js";
class ContentLayer {
#logger;
#store;
@@ -20,6 +25,7 @@ class ContentLayer {
#watcher;
#lastConfigDigest;
#unsubscribe;
#markdownProcessor;
#generateDigest;
#queue;
constructor({ settings, logger, store, watcher }) {
@@ -27,7 +33,9 @@ class ContentLayer {
this.#logger = logger;
this.#store = store;
this.#settings = settings;
this.#watcher = watcher;
if (watcher) {
this.#watcher = createWatcherWrapper(watcher);
}
this.#queue = new PQueue({ concurrency: 1 });
}
/**
@@ -50,6 +58,11 @@ class ContentLayer {
unwatchContentConfig() {
this.#unsubscribe?.();
}
dispose() {
this.#queue.clear();
this.#unsubscribe?.();
this.#watcher?.removeAllTrackedListeners();
}
async #getGenerateDigest() {
if (this.#generateDigest) {
return this.#generateDigest;
@@ -74,6 +87,7 @@ class ContentLayer {
logger: this.#logger.forkIntegrationLogger(loaderName),
config: this.#settings.config,
parseData,
renderMarkdown: this.#processMarkdown.bind(this),
generateDigest: await this.#getGenerateDigest(),
watcher: this.#watcher,
refreshContextData,
@@ -83,6 +97,14 @@ class ContentLayer {
])
};
}
async #processMarkdown(content) {
this.#markdownProcessor ??= await createMarkdownProcessor(this.#settings.config.markdown);
const { code, metadata } = await this.#markdownProcessor.render(content);
return {
html: code,
metadata
};
}
/**
* Enqueues a sync job that runs the `load()` method of each collection's loader, which will load the data and save it in the data store.
* The loader itself is responsible for deciding whether this will clear and reload the full collection, or
@@ -93,35 +115,56 @@ class ContentLayer {
return this.#queue.add(() => this.#doSync(options));
}
async #doSync(options) {
const contentConfig = globalContentConfigObserver.get();
let contentConfig = globalContentConfigObserver.get();
const logger = this.#logger.forkIntegrationLogger("content");
if (contentConfig?.status !== "loaded") {
logger.debug("Content config not loaded, skipping sync");
if (contentConfig?.status === "loading") {
contentConfig = await Promise.race([
new Promise((resolve) => {
const unsub = globalContentConfigObserver.subscribe((ctx) => {
unsub();
resolve(ctx);
});
}),
new Promise(
(resolve) => setTimeout(
() => resolve({ status: "error", error: new Error("Content config loading timed out") }),
5e3
)
)
]);
}
if (contentConfig?.status === "error") {
logger.error(`Error loading content config. Skipping sync.
${contentConfig.error.message}`);
return;
}
if (!this.#settings.config.experimental.contentLayer) {
const contentLayerCollections = Object.entries(contentConfig.config.collections).filter(
([_, collection]) => collection.type === CONTENT_LAYER_TYPE
);
if (contentLayerCollections.length > 0) {
throw new AstroUserError(
`The following collections have a loader defined, but the content layer is not enabled: ${contentLayerCollections.map(([title]) => title).join(", ")}.`,
"To enable the Content Layer API, set `experimental: { contentLayer: true }` in your Astro config file."
);
}
if (contentConfig?.status !== "loaded") {
logger.error(`Content config not loaded, skipping sync. Status was ${contentConfig?.status}`);
return;
}
logger.info("Syncing content");
const {
vite: _vite,
integrations: _integrations,
adapter: _adapter,
...hashableConfig
} = this.#settings.config;
const astroConfigDigest = safeStringify(hashableConfig);
const { digest: currentConfigDigest } = contentConfig.config;
this.#lastConfigDigest = currentConfigDigest;
let shouldClear = false;
const previousConfigDigest = await this.#store.metaStore().get("config-digest");
const previousConfigDigest = await this.#store.metaStore().get("content-config-digest");
const previousAstroConfigDigest = await this.#store.metaStore().get("astro-config-digest");
const previousAstroVersion = await this.#store.metaStore().get("astro-version");
if (currentConfigDigest && previousConfigDigest !== currentConfigDigest) {
if (previousAstroConfigDigest && previousAstroConfigDigest !== astroConfigDigest) {
logger.info("Astro config changed");
shouldClear = true;
}
if (previousConfigDigest && previousConfigDigest !== currentConfigDigest) {
logger.info("Content config changed");
shouldClear = true;
}
if (previousAstroVersion !== "4.16.18") {
if (previousAstroVersion && previousAstroVersion !== "5.12.3") {
logger.info("Astro version changed");
shouldClear = true;
}
@@ -129,11 +172,17 @@ class ContentLayer {
logger.info("Clearing content store");
this.#store.clearAll();
}
if ("4.16.18") {
await this.#store.metaStore().set("astro-version", "4.16.18");
if ("5.12.3") {
await this.#store.metaStore().set("astro-version", "5.12.3");
}
if (currentConfigDigest) {
await this.#store.metaStore().set("config-digest", currentConfigDigest);
await this.#store.metaStore().set("content-config-digest", currentConfigDigest);
}
if (astroConfigDigest) {
await this.#store.metaStore().set("astro-config-digest", astroConfigDigest);
}
if (!options?.loaders?.length) {
this.#watcher?.removeAllTrackedListeners();
}
await Promise.all(
Object.entries(contentConfig.config.collections).map(async ([name, collection]) => {
@@ -163,7 +212,9 @@ class ContentLayer {
}
},
collectionWithResolvedSchema,
false
false,
// FUTURE: Remove in this in v6
id.endsWith(".svg")
);
return parsedData;
};
@@ -182,25 +233,20 @@ class ContentLayer {
return collection.loader.load(context);
})
);
if (!existsSync(this.#settings.config.cacheDir)) {
await fs.mkdir(this.#settings.config.cacheDir, { recursive: true });
}
const cacheFile = getDataStoreFile(this.#settings);
await this.#store.writeToDisk(cacheFile);
if (!existsSync(this.#settings.dotAstroDir)) {
await fs.mkdir(this.#settings.dotAstroDir, { recursive: true });
}
await fs.mkdir(this.#settings.config.cacheDir, { recursive: true });
await fs.mkdir(this.#settings.dotAstroDir, { recursive: true });
const assetImportsFile = new URL(ASSET_IMPORTS_FILE, this.#settings.dotAstroDir);
await this.#store.writeAssetImports(assetImportsFile);
const modulesImportsFile = new URL(MODULES_IMPORTS_FILE, this.#settings.dotAstroDir);
await this.#store.writeModuleImports(modulesImportsFile);
await this.#store.waitUntilSaveComplete();
logger.info("Synced content");
if (this.#settings.config.experimental.contentIntellisense) {
await this.regenerateCollectionFileManifest();
}
}
async regenerateCollectionFileManifest() {
const collectionsManifest = new URL("collections/collections.json", this.#settings.dotAstroDir);
const collectionsManifest = new URL(COLLECTIONS_MANIFEST_FILE, this.#settings.dotAstroDir);
this.#logger.debug("content", "Regenerating collection file manifest");
if (existsSync(collectionsManifest)) {
try {
@@ -232,36 +278,82 @@ class ContentLayer {
}
}
async function simpleLoader(handler, context) {
const data = await handler();
context.store.clear();
for (const raw of data) {
const item = await context.parseData({ id: raw.id, data: raw });
context.store.set({ id: raw.id, data: item });
const unsafeData = await handler();
const parsedData = loaderReturnSchema.safeParse(unsafeData);
if (!parsedData.success) {
const issue = parsedData.error.issues[0];
const parseIssue = Array.isArray(unsafeData) ? issue.unionErrors[0] : issue.unionErrors[1];
const error = parseIssue.errors[0];
const firstPathItem = error.path[0];
const entry = Array.isArray(unsafeData) ? unsafeData[firstPathItem] : unsafeData[firstPathItem];
throw new AstroError({
...AstroErrorData.ContentLoaderReturnsInvalidId,
message: AstroErrorData.ContentLoaderReturnsInvalidId.message(context.collection, entry)
});
}
const data = parsedData.data;
context.store.clear();
if (Array.isArray(data)) {
for (const raw of data) {
if (!raw.id) {
throw new AstroError({
...AstroErrorData.ContentLoaderInvalidDataError,
message: AstroErrorData.ContentLoaderInvalidDataError.message(
context.collection,
`Entry missing ID:
${JSON.stringify({ ...raw, id: void 0 }, null, 2)}`
)
});
}
const item = await context.parseData({ id: raw.id, data: raw });
context.store.set({ id: raw.id, data: item });
}
return;
}
if (typeof data === "object") {
for (const [id, raw] of Object.entries(data)) {
if (raw.id && raw.id !== id) {
throw new AstroError({
...AstroErrorData.ContentLoaderInvalidDataError,
message: AstroErrorData.ContentLoaderInvalidDataError.message(
context.collection,
`Object key ${JSON.stringify(id)} does not match ID ${JSON.stringify(raw.id)}`
)
});
}
const item = await context.parseData({ id, data: raw });
context.store.set({ id, data: item });
}
return;
}
throw new AstroError({
...AstroErrorData.ExpectedImageOptions,
message: AstroErrorData.ContentLoaderInvalidDataError.message(
context.collection,
`Invalid data type: ${typeof data}`
)
});
}
function getDataStoreFile(settings, isDev) {
isDev ??= process?.env.NODE_ENV === "development";
return new URL(DATA_STORE_FILE, isDev ? settings.dotAstroDir : settings.config.cacheDir);
}
function contentLayerSingleton() {
let instance = null;
return {
init: (options) => {
instance?.unwatchContentConfig();
instance?.dispose();
instance = new ContentLayer(options);
return instance;
},
get: () => instance,
dispose: () => {
instance?.unwatchContentConfig();
instance?.dispose();
instance = null;
}
};
}
const globalContentLayer = contentLayerSingleton();
export {
ContentLayer,
getDataStoreFile,
globalContentLayer,
simpleLoader
globalContentLayer
};

View File

@@ -31,12 +31,14 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
*/
deferredRender?: boolean;
assetImports?: Array<string>;
/** @deprecated */
legacyId?: string;
}
/**
* A read-only data store for content collections. This is used to retrieve data from the content layer at runtime.
* To add or modify data, use {@link MutableDataStore} instead.
*/
export declare class DataStore {
export declare class ImmutableDataStore {
protected _collections: Map<string, Map<string, any>>;
constructor();
get<T = DataEntry>(collectionName: string, key: string): T | undefined;
@@ -50,6 +52,6 @@ export declare class DataStore {
* Attempts to load a DataStore from the virtual module.
* This only works in Vite.
*/
static fromModule(): Promise<DataStore>;
static fromMap(data: Map<string, Map<string, any>>): Promise<DataStore>;
static fromModule(): Promise<ImmutableDataStore>;
static fromMap(data: Map<string, Map<string, any>>): Promise<ImmutableDataStore>;
}

View File

@@ -1,5 +1,5 @@
import * as devalue from "devalue";
class DataStore {
class ImmutableDataStore {
_collections = /* @__PURE__ */ new Map();
constructor() {
this._collections = /* @__PURE__ */ new Map();
@@ -40,16 +40,16 @@ class DataStore {
try {
const data = await import("astro:data-layer-content");
if (data.default instanceof Map) {
return DataStore.fromMap(data.default);
return ImmutableDataStore.fromMap(data.default);
}
const map = devalue.unflatten(data.default);
return DataStore.fromMap(map);
return ImmutableDataStore.fromMap(map);
} catch {
}
return new DataStore();
return new ImmutableDataStore();
}
static async fromMap(data) {
const store = new DataStore();
const store = new ImmutableDataStore();
store._collections = data;
return store;
}
@@ -59,7 +59,7 @@ function dataStoreSingleton() {
return {
get: async () => {
if (!instance) {
instance = DataStore.fromModule();
instance = ImmutableDataStore.fromModule();
}
return instance;
},
@@ -70,6 +70,6 @@ function dataStoreSingleton() {
}
const globalDataStore = dataStoreSingleton();
export {
DataStore,
ImmutableDataStore,
globalDataStore
};

View File

@@ -1,7 +1,6 @@
export { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from './consts.js';
export { attachContentServerListeners } from './server-listeners.js';
export { createContentTypesGenerator } from './types-generator.js';
export { contentObservable, getContentPaths, hasAssetPropagationFlag } from './utils.js';
export { getContentPaths, hasAssetPropagationFlag } from './utils.js';
export { astroContentAssetPropagationPlugin } from './vite-plugin-content-assets.js';
export { astroContentImportPlugin } from './vite-plugin-content-imports.js';
export { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js';

View File

@@ -1,18 +1,14 @@
import { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from "./consts.js";
import { attachContentServerListeners } from "./server-listeners.js";
import { createContentTypesGenerator } from "./types-generator.js";
import { contentObservable, getContentPaths, hasAssetPropagationFlag } from "./utils.js";
import { getContentPaths, hasAssetPropagationFlag } from "./utils.js";
import { astroContentAssetPropagationPlugin } from "./vite-plugin-content-assets.js";
import { astroContentImportPlugin } from "./vite-plugin-content-imports.js";
import { astroContentVirtualModPlugin } from "./vite-plugin-content-virtual-mod.js";
export {
CONTENT_FLAG,
PROPAGATED_ASSET_FLAG,
astroContentAssetPropagationPlugin,
astroContentImportPlugin,
astroContentVirtualModPlugin,
attachContentServerListeners,
contentObservable,
createContentTypesGenerator,
getContentPaths,
hasAssetPropagationFlag

20
node_modules/astro/dist/content/loaders/errors.d.ts generated vendored Normal file
View File

@@ -0,0 +1,20 @@
import type { ZodError } from 'zod';
export declare class LiveCollectionError extends Error {
readonly collection: string;
readonly message: string;
readonly cause?: Error | undefined;
constructor(collection: string, message: string, cause?: Error | undefined);
static is(error: unknown): error is LiveCollectionError;
}
export declare class LiveEntryNotFoundError extends LiveCollectionError {
constructor(collection: string, entryFilter: string | Record<string, unknown>);
static is(error: unknown): error is LiveEntryNotFoundError;
}
export declare class LiveCollectionValidationError extends LiveCollectionError {
constructor(collection: string, entryId: string, error: ZodError);
static is(error: unknown): error is LiveCollectionValidationError;
}
export declare class LiveCollectionCacheHintError extends LiveCollectionError {
constructor(collection: string, entryId: string | undefined, error: ZodError);
static is(error: unknown): error is LiveCollectionCacheHintError;
}

67
node_modules/astro/dist/content/loaders/errors.js generated vendored Normal file
View File

@@ -0,0 +1,67 @@
class LiveCollectionError extends Error {
constructor(collection, message, cause) {
super(message);
this.collection = collection;
this.message = message;
this.cause = cause;
this.name = "LiveCollectionError";
if (cause?.stack) {
this.stack = cause.stack;
}
}
static is(error) {
return error instanceof LiveCollectionError;
}
}
class LiveEntryNotFoundError extends LiveCollectionError {
constructor(collection, entryFilter) {
super(
collection,
`Entry ${collection} \u2192 ${typeof entryFilter === "string" ? entryFilter : JSON.stringify(entryFilter)} was not found.`
);
this.name = "LiveEntryNotFoundError";
}
static is(error) {
return error?.name === "LiveEntryNotFoundError";
}
}
class LiveCollectionValidationError extends LiveCollectionError {
constructor(collection, entryId, error) {
super(
collection,
[
`**${collection} \u2192 ${entryId}** data does not match the collection schema.
`,
...error.errors.map((zodError) => ` **${zodError.path.join(".")}**: ${zodError.message}`),
""
].join("\n")
);
this.name = "LiveCollectionValidationError";
}
static is(error) {
return error?.name === "LiveCollectionValidationError";
}
}
class LiveCollectionCacheHintError extends LiveCollectionError {
constructor(collection, entryId, error) {
super(
collection,
[
`**${String(collection)}${entryId ? ` \u2192 ${String(entryId)}` : ""}** returned an invalid cache hint.
`,
...error.errors.map((zodError) => ` **${zodError.path.join(".")}**: ${zodError.message}`),
""
].join("\n")
);
this.name = "LiveCollectionCacheHintError";
}
static is(error) {
return error?.name === "LiveCollectionCacheHintError";
}
}
export {
LiveCollectionCacheHintError,
LiveCollectionError,
LiveCollectionValidationError,
LiveEntryNotFoundError
};

View File

@@ -1,7 +1,15 @@
import type { Loader } from './types.js';
interface FileOptions {
/**
* the parsing function to use for this data
* @default JSON.parse or yaml.load, depending on the extension of the file
* */
parser?: (text: string) => Record<string, Record<string, unknown>> | Array<Record<string, unknown>>;
}
/**
* Loads entries from a JSON file. The file must contain an array of objects that contain unique `id` fields, or an object with string keys.
* @todo Add support for other file types, such as YAML, CSV etc.
* @param fileName The path to the JSON file to load, relative to the content directory.
* @param options Additional options for the file loader
*/
export declare function file(fileName: string): Loader;
export declare function file(fileName: string, options?: FileOptions): Loader;
export {};

View File

@@ -1,43 +1,72 @@
import { promises as fs, existsSync } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import { fileURLToPath } from "node:url";
import yaml from "js-yaml";
import toml from "smol-toml";
import { FileGlobNotSupported, FileParserNotFound } from "../../core/errors/errors-data.js";
import { AstroError } from "../../core/errors/index.js";
import { posixRelative } from "../utils.js";
function file(fileName) {
function file(fileName, options) {
if (fileName.includes("*")) {
throw new Error("Glob patterns are not supported in `file` loader. Use `glob` loader instead.");
throw new AstroError(FileGlobNotSupported);
}
let parse = null;
const ext = fileName.split(".").at(-1);
if (ext === "json") {
parse = JSON.parse;
} else if (ext === "yml" || ext === "yaml") {
parse = (text) => yaml.load(text, {
filename: fileName
});
} else if (ext === "toml") {
parse = toml.parse;
}
if (options?.parser) parse = options.parser;
if (parse === null) {
throw new AstroError({
...FileParserNotFound,
message: FileParserNotFound.message(fileName)
});
}
async function syncData(filePath, { logger, parseData, store, config }) {
let json;
let data;
try {
const data = await fs.readFile(filePath, "utf-8");
json = JSON.parse(data);
const contents = await fs.readFile(filePath, "utf-8");
data = parse(contents);
} catch (error) {
logger.error(`Error reading data from ${fileName}`);
logger.debug(error.message);
return;
}
const normalizedFilePath = posixRelative(fileURLToPath(config.root), filePath);
if (Array.isArray(json)) {
if (json.length === 0) {
if (Array.isArray(data)) {
if (data.length === 0) {
logger.warn(`No items found in ${fileName}`);
}
logger.debug(`Found ${json.length} item array in ${fileName}`);
logger.debug(`Found ${data.length} item array in ${fileName}`);
store.clear();
for (const rawItem of json) {
const idList = /* @__PURE__ */ new Set();
for (const rawItem of data) {
const id = (rawItem.id ?? rawItem.slug)?.toString();
if (!id) {
logger.error(`Item in ${fileName} is missing an id or slug field.`);
continue;
}
const data = await parseData({ id, data: rawItem, filePath });
store.set({ id, data, filePath: normalizedFilePath });
if (idList.has(id)) {
logger.warn(
`Duplicate id "${id}" found in ${fileName}. Later items with the same id will overwrite earlier ones.`
);
}
idList.add(id);
const parsedData = await parseData({ id, data: rawItem, filePath });
store.set({ id, data: parsedData, filePath: normalizedFilePath });
}
} else if (typeof json === "object") {
const entries = Object.entries(json);
} else if (typeof data === "object") {
const entries = Object.entries(data);
logger.debug(`Found object with ${entries.length} entries in ${fileName}`);
store.clear();
for (const [id, rawItem] of entries) {
const data = await parseData({ id, data: rawItem, filePath });
store.set({ id, data, filePath: normalizedFilePath });
const parsedData = await parseData({ id, data: rawItem, filePath });
store.set({ id, data: parsedData, filePath: normalizedFilePath });
}
} else {
logger.error(`Invalid data in ${fileName}. Must be an array or object.`);
@@ -55,6 +84,7 @@ function file(fileName) {
}
const filePath = fileURLToPath(url);
await syncData(filePath, context);
watcher?.add(filePath);
watcher?.on("change", async (changedPath) => {
if (changedPath === filePath) {
logger.info(`Reloading data from ${fileName}`);

View File

@@ -1,5 +1,5 @@
import type { Loader } from './types.js';
export interface GenerateIdOptions {
interface GenerateIdOptions {
/** The path to the entry file, relative to the base directory. */
entry: string;
/** The base directory URL. */
@@ -7,9 +7,9 @@ export interface GenerateIdOptions {
/** The parsed, unvalidated data of the entry. */
data: Record<string, unknown>;
}
export interface GlobOptions {
interface GlobOptions {
/** The glob pattern to match files, relative to the base directory */
pattern: string;
pattern: string | Array<string>;
/** The base directory to resolve the glob pattern from. Relative to the root directory, or an absolute file URL. Defaults to `.` */
base?: string | URL;
/**
@@ -23,3 +23,9 @@ export interface GlobOptions {
* @param pattern A glob pattern to match files, relative to the content directory.
*/
export declare function glob(globOptions: GlobOptions): Loader;
/** @private */
export declare function glob(globOptions: GlobOptions & {
/** @deprecated */
_legacy?: true;
}): Loader;
export {};

View File

@@ -1,15 +1,16 @@
import { promises as fs } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import { relative } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import fastGlob from "fast-glob";
import { bold, green } from "kleur/colors";
import micromatch from "micromatch";
import pLimit from "p-limit";
import picomatch from "picomatch";
import { glob as tinyglobby } from "tinyglobby";
import { getContentEntryIdAndSlug, posixRelative } from "../utils.js";
function generateIdDefault({ entry, base, data }) {
if (data.slug) {
return data.slug;
}
const entryURL = new URL(entry, base);
const entryURL = new URL(encodeURI(entry), base);
const { slug } = getContentEntryIdAndSlug({
entry: entryURL,
contentDir: base,
@@ -17,13 +18,19 @@ function generateIdDefault({ entry, base, data }) {
});
return slug;
}
function checkPrefix(pattern, prefix) {
if (Array.isArray(pattern)) {
return pattern.some((p) => p.startsWith(prefix));
}
return pattern.startsWith(prefix);
}
function glob(globOptions) {
if (globOptions.pattern.startsWith("../")) {
if (checkPrefix(globOptions.pattern, "../")) {
throw new Error(
"Glob patterns cannot start with `../`. Set the `base` option to a parent directory instead."
);
}
if (globOptions.pattern.startsWith("/")) {
if (checkPrefix(globOptions.pattern, "/")) {
throw new Error(
"Glob patterns cannot start with `/`. Set the `base` option to a parent directory or use a relative path instead."
);
@@ -35,17 +42,19 @@ function glob(globOptions) {
load: async ({ config, logger, watcher, parseData, store, generateDigest, entryTypes }) => {
const renderFunctionByContentType = /* @__PURE__ */ new WeakMap();
const untouchedEntries = new Set(store.keys());
async function syncData(entry, base, entryType) {
const isLegacy = globOptions._legacy;
const emulateLegacyCollections = !config.legacy.collections;
async function syncData(entry, base, entryType, oldId) {
if (!entryType) {
logger.warn(`No entry type found for ${entry}`);
return;
}
const fileUrl = new URL(entry, base);
const fileUrl = new URL(encodeURI(entry), base);
const contents = await fs.readFile(fileUrl, "utf-8").catch((err) => {
logger.error(`Error reading ${entry}: ${err.message}`);
return;
});
if (!contents) {
if (!contents && contents !== "") {
logger.warn(`No contents found for ${entry}`);
return;
}
@@ -54,10 +63,23 @@ function glob(globOptions) {
fileUrl
});
const id = generateId({ entry, base, data });
if (oldId && oldId !== id) {
store.delete(oldId);
}
let legacyId;
if (isLegacy) {
const entryURL = new URL(encodeURI(entry), base);
const legacyOptions = getContentEntryIdAndSlug({
entry: entryURL,
contentDir: base,
collection: ""
});
legacyId = legacyOptions.id;
}
untouchedEntries.delete(id);
const existingEntry = store.get(id);
const digest = generateDigest(contents);
const filePath = fileURLToPath(fileUrl);
const filePath2 = fileURLToPath(fileUrl);
if (existingEntry && existingEntry.digest === digest && existingEntry.filePath) {
if (existingEntry.deferredRender) {
store.addModuleImport(existingEntry.filePath);
@@ -65,16 +87,21 @@ function glob(globOptions) {
if (existingEntry.assetImports?.length) {
store.addAssetImports(existingEntry.assetImports, existingEntry.filePath);
}
fileToIdMap.set(filePath, id);
fileToIdMap.set(filePath2, id);
return;
}
const relativePath = posixRelative(fileURLToPath(config.root), filePath);
const relativePath2 = posixRelative(fileURLToPath(config.root), filePath2);
const parsedData = await parseData({
id,
data,
filePath
filePath: filePath2
});
if (entryType.getRenderFunction) {
if (isLegacy && data.layout) {
logger.error(
`The Markdown "layout" field is not supported in content collections in Astro 5. Ignoring layout for ${JSON.stringify(entry)}. Enable "legacy.collections" if you need to use the layout field.`
);
}
let render = renderFunctionByContentType.get(entryType);
if (!render) {
render = await entryType.getRenderFunction(config);
@@ -84,9 +111,9 @@ function glob(globOptions) {
try {
rendered = await render?.({
id,
data: parsedData,
data,
body,
filePath,
filePath: filePath2,
digest
});
} catch (error) {
@@ -96,32 +123,47 @@ function glob(globOptions) {
id,
data: parsedData,
body,
filePath: relativePath,
filePath: relativePath2,
digest,
rendered,
assetImports: rendered?.metadata?.imagePaths
assetImports: rendered?.metadata?.imagePaths,
legacyId
});
} else if ("contentModuleTypes" in entryType) {
store.set({
id,
data: parsedData,
body,
filePath: relativePath,
filePath: relativePath2,
digest,
deferredRender: true
deferredRender: true,
legacyId
});
} else {
store.set({ id, data: parsedData, body, filePath: relativePath, digest });
store.set({ id, data: parsedData, body, filePath: relativePath2, digest, legacyId });
}
fileToIdMap.set(filePath, id);
fileToIdMap.set(filePath2, id);
}
const baseDir = globOptions.base ? new URL(globOptions.base, config.root) : config.root;
if (!baseDir.pathname.endsWith("/")) {
baseDir.pathname = `${baseDir.pathname}/`;
}
const files = await fastGlob(globOptions.pattern, {
cwd: fileURLToPath(baseDir)
const filePath = fileURLToPath(baseDir);
const relativePath = relative(fileURLToPath(config.root), filePath);
const exists = existsSync(baseDir);
if (!exists) {
logger.warn(`The base directory "${fileURLToPath(baseDir)}" does not exist.`);
}
const files = await tinyglobby(globOptions.pattern, {
cwd: fileURLToPath(baseDir),
expandDirectories: false
});
if (exists && files.length === 0) {
logger.warn(
`No files found matching "${globOptions.pattern}" in directory "${relativePath}"`
);
return;
}
function configForFile(file) {
const ext = file.split(".").at(-1);
if (!ext) {
@@ -149,7 +191,7 @@ function glob(globOptions) {
if (isConfigFile(entry)) {
return;
}
if (isInContentDir(entry)) {
if (!emulateLegacyCollections && isInContentDir(entry)) {
skippedFiles.push(entry);
return;
}
@@ -161,13 +203,16 @@ function glob(globOptions) {
);
const skipCount = skippedFiles.length;
if (skipCount > 0) {
logger.warn(`The glob() loader cannot be used for files in ${bold("src/content")}.`);
const patternList = Array.isArray(globOptions.pattern) ? globOptions.pattern.join(", ") : globOptions.pattern;
logger.warn(
`The glob() loader cannot be used for files in ${bold("src/content")} when legacy mode is enabled.`
);
if (skipCount > 10) {
logger.warn(
`Skipped ${green(skippedFiles.length)} files that matched ${green(globOptions.pattern)}.`
`Skipped ${green(skippedFiles.length)} files that matched ${green(patternList)}.`
);
} else {
logger.warn(`Skipped the following files that matched ${green(globOptions.pattern)}:`);
logger.warn(`Skipped the following files that matched ${green(patternList)}:`);
skippedFiles.forEach((file) => logger.warn(`\u2022 ${green(file)}`));
}
}
@@ -175,8 +220,8 @@ function glob(globOptions) {
if (!watcher) {
return;
}
const matcher = micromatch.makeRe(globOptions.pattern);
const matchesGlob = (entry) => !entry.startsWith("../") && matcher.test(entry);
watcher.add(filePath);
const matchesGlob = (entry) => !entry.startsWith("../") && picomatch.isMatch(entry, globOptions.pattern);
const basePath = fileURLToPath(baseDir);
async function onChange(changedPath) {
const entry = posixRelative(basePath, changedPath);
@@ -185,7 +230,8 @@ function glob(globOptions) {
}
const entryType = configForFile(changedPath);
const baseUrl = pathToFileURL(basePath);
await syncData(entry, baseUrl, entryType);
const oldId = fileToIdMap.get(changedPath);
await syncData(entry, baseUrl, entryType, oldId);
logger.info(`Reloaded data from ${green(entry)}`);
}
watcher.on("change", onChange);

View File

@@ -1,7 +1,11 @@
import type { FSWatcher } from 'vite';
import type { ZodSchema } from 'zod';
import type { AstroConfig, AstroIntegrationLogger, ContentEntryType } from '../../@types/astro.js';
import type { MetaStore, ScopedDataStore } from '../mutable-data-store.js';
import type { AstroIntegrationLogger } from '../../core/logger/core.js';
import type { AstroConfig } from '../../types/public/config.js';
import type { LiveDataCollection, LiveDataEntry } from '../../types/public/content.js';
import type { RenderedContent } from '../data-store.js';
import type { DataStore, MetaStore } from '../mutable-data-store.js';
export type { DataStore, MetaStore };
export interface ParseDataOptions<TData extends Record<string, unknown>> {
/** The ID of the entry. Unique per collection */
id: string;
@@ -13,8 +17,8 @@ export interface ParseDataOptions<TData extends Record<string, unknown>> {
export interface LoaderContext {
/** The unique name of the collection */
collection: string;
/** A database abstraction to store the actual data */
store: ScopedDataStore;
/** A database to store the actual data */
store: DataStore;
/** A simple KV store, designed for things like sync tokens */
meta: MetaStore;
logger: AstroIntegrationLogger;
@@ -22,12 +26,14 @@ export interface LoaderContext {
config: AstroConfig;
/** Validates and parses the data according to the collection schema */
parseData<TData extends Record<string, unknown>>(props: ParseDataOptions<TData>): Promise<TData>;
/** Renders markdown content to HTML and metadata */
renderMarkdown(content: string): Promise<RenderedContent>;
/** Generates a non-cryptographic content digest. This can be used to check if the data has changed */
generateDigest(data: Record<string, unknown> | string): string;
/** When running in dev, this is a filesystem watcher that can be used to trigger updates */
watcher?: FSWatcher;
/** If the loader has been triggered by an integration, this may optionally contain extra data set by that integration */
refreshContextData?: Record<string, unknown>;
entryTypes: Map<string, ContentEntryType>;
}
export interface Loader {
/** Unique name of the loader, e.g. the npm package name */
@@ -37,3 +43,23 @@ export interface Loader {
/** Optionally, define the schema of the data. Will be overridden by user-defined schema */
schema?: ZodSchema | Promise<ZodSchema> | (() => ZodSchema | Promise<ZodSchema>);
}
export interface LoadEntryContext<TEntryFilter = never> {
filter: TEntryFilter extends never ? {
id: string;
} : TEntryFilter;
}
export interface LoadCollectionContext<TCollectionFilter = unknown> {
filter?: TCollectionFilter;
}
export interface LiveLoader<TData extends Record<string, any> = Record<string, unknown>, TEntryFilter extends Record<string, any> | never = never, TCollectionFilter extends Record<string, any> | never = never, TError extends Error = Error> {
/** Unique name of the loader, e.g. the npm package name */
name: string;
/** Load a single entry */
loadEntry: (context: LoadEntryContext<TEntryFilter>) => Promise<LiveDataEntry<TData> | undefined | {
error: TError;
}>;
/** Load a collection of entries */
loadCollection: (context: LoadCollectionContext<TCollectionFilter>) => Promise<LiveDataCollection<TData> | {
error: TError;
}>;
}

View File

@@ -1,10 +1,10 @@
import { type PathLike } from 'node:fs';
import { type DataEntry, DataStore, type RenderedContent } from './data-store.js';
import { type DataEntry, ImmutableDataStore } from './data-store.js';
/**
* Extends the DataStore with the ability to change entries and write them to disk.
* This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed.
*/
export declare class MutableDataStore extends DataStore {
export declare class MutableDataStore extends ImmutableDataStore {
#private;
set(collectionName: string, key: string, value: unknown): void;
delete(collectionName: string, key: string): void;
@@ -15,13 +15,18 @@ export declare class MutableDataStore extends DataStore {
addModuleImport(fileName: string): void;
writeAssetImports(filePath: PathLike): Promise<void>;
writeModuleImports(filePath: PathLike): Promise<void>;
scopedStore(collectionName: string): ScopedDataStore;
scopedStore(collectionName: string): DataStore;
/**
* Returns a MetaStore for a given collection, or if no collection is provided, the default meta collection.
*/
metaStore(collectionName?: string): MetaStore;
/**
* Returns a promise that resolves when all pending saves are complete.
* This includes any in-progress debounced saves for the data store, asset imports, and module imports.
*/
waitUntilSaveComplete(): Promise<void>;
toString(): string;
writeToDisk(filePath: PathLike): Promise<void>;
writeToDisk(): Promise<void>;
/**
* Attempts to load a MutableDataStore from the virtual module.
* This only works in Vite.
@@ -31,27 +36,10 @@ export declare class MutableDataStore extends DataStore {
static fromString(data: string): Promise<MutableDataStore>;
static fromFile(filePath: string | URL): Promise<MutableDataStore>;
}
export interface ScopedDataStore {
export interface DataStore {
get: <TData extends Record<string, unknown> = Record<string, unknown>>(key: string) => DataEntry<TData> | undefined;
entries: () => Array<[id: string, DataEntry]>;
set: <TData extends Record<string, unknown>>(opts: {
/** The ID of the entry. Must be unique per collection. */
id: string;
/** The data to store. */
data: TData;
/** The raw body of the content, if applicable. */
body?: string;
/** The file path of the content, if applicable. Relative to the site root. */
filePath?: string;
/** A content digest, to check if the content has changed. */
digest?: number | string;
/** The rendered content, if applicable. */
rendered?: RenderedContent;
/**
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase.
*/
deferredRender?: boolean;
}) => boolean;
set: <TData extends Record<string, unknown>>(opts: DataEntry<TData>) => boolean;
values: () => Array<DataEntry>;
keys: () => Array<string>;
delete: (key: string) => void;

View File

@@ -1,19 +1,22 @@
import { promises as fs, existsSync } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import * as devalue from "devalue";
import { Traverse } from "neotraverse/modern";
import { imageSrcToImportId, importIdToSymbolName } from "../assets/utils/resolveImports.js";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { IMAGE_IMPORT_PREFIX } from "./consts.js";
import { DataStore } from "./data-store.js";
import { ImmutableDataStore } from "./data-store.js";
import { contentModuleToId } from "./utils.js";
const SAVE_DEBOUNCE_MS = 500;
class MutableDataStore extends DataStore {
const MAX_DEPTH = 10;
class MutableDataStore extends ImmutableDataStore {
#file;
#assetsFile;
#modulesFile;
#saveTimeout;
#assetsSaveTimeout;
#modulesSaveTimeout;
#savePromise;
#savePromiseResolve;
#dirty = false;
#assetsDirty = false;
#modulesDirty = false;
@@ -61,7 +64,7 @@ class MutableDataStore extends DataStore {
this.#assetsFile = filePath;
if (this.#assetImports.size === 0) {
try {
await fs.writeFile(filePath, "export default new Map();");
await this.#writeFileAtomic(filePath, "export default new Map();");
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@@ -73,7 +76,7 @@ class MutableDataStore extends DataStore {
const exports = [];
this.#assetImports.forEach((id) => {
const symbol = importIdToSymbolName(id);
imports.push(`import ${symbol} from '${id}';`);
imports.push(`import ${symbol} from ${JSON.stringify(id)};`);
exports.push(`[${JSON.stringify(id)}, ${symbol}]`);
});
const code = (
@@ -84,7 +87,7 @@ export default new Map([${exports.join(", ")}]);
`
);
try {
await fs.writeFile(filePath, code);
await this.#writeFileAtomic(filePath, code);
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@@ -94,7 +97,7 @@ export default new Map([${exports.join(", ")}]);
this.#modulesFile = filePath;
if (this.#moduleImports.size === 0) {
try {
await fs.writeFile(filePath, "export default new Map();");
await this.#writeFileAtomic(filePath, "export default new Map();");
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@@ -104,28 +107,41 @@ export default new Map([${exports.join(", ")}]);
}
const lines = [];
for (const [fileName, specifier] of this.#moduleImports) {
lines.push(`['${fileName}', () => import('${specifier}')]`);
lines.push(`[${JSON.stringify(fileName)}, () => import(${JSON.stringify(specifier)})]`);
}
const code = `
export default new Map([
${lines.join(",\n")}]);
`;
try {
await fs.writeFile(filePath, code);
await this.#writeFileAtomic(filePath, code);
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
this.#modulesDirty = false;
}
#maybeResolveSavePromise() {
if (!this.#saveTimeout && !this.#assetsSaveTimeout && !this.#modulesSaveTimeout && this.#savePromiseResolve) {
this.#savePromiseResolve();
this.#savePromiseResolve = void 0;
this.#savePromise = void 0;
}
}
#writeAssetsImportsDebounced() {
this.#assetsDirty = true;
if (this.#assetsFile) {
if (this.#assetsSaveTimeout) {
clearTimeout(this.#assetsSaveTimeout);
}
this.#assetsSaveTimeout = setTimeout(() => {
if (!this.#savePromise) {
this.#savePromise = new Promise((resolve) => {
this.#savePromiseResolve = resolve;
});
}
this.#assetsSaveTimeout = setTimeout(async () => {
this.#assetsSaveTimeout = void 0;
this.writeAssetImports(this.#assetsFile);
await this.writeAssetImports(this.#assetsFile);
this.#maybeResolveSavePromise();
}, SAVE_DEBOUNCE_MS);
}
}
@@ -135,22 +151,73 @@ ${lines.join(",\n")}]);
if (this.#modulesSaveTimeout) {
clearTimeout(this.#modulesSaveTimeout);
}
this.#modulesSaveTimeout = setTimeout(() => {
if (!this.#savePromise) {
this.#savePromise = new Promise((resolve) => {
this.#savePromiseResolve = resolve;
});
}
this.#modulesSaveTimeout = setTimeout(async () => {
this.#modulesSaveTimeout = void 0;
this.writeModuleImports(this.#modulesFile);
await this.writeModuleImports(this.#modulesFile);
this.#maybeResolveSavePromise();
}, SAVE_DEBOUNCE_MS);
}
}
// Skips the debounce and writes to disk immediately
async #saveToDiskNow() {
if (this.#saveTimeout) {
clearTimeout(this.#saveTimeout);
}
this.#saveTimeout = void 0;
if (this.#file) {
await this.writeToDisk();
}
this.#maybeResolveSavePromise();
}
#saveToDiskDebounced() {
this.#dirty = true;
if (this.#file) {
if (this.#saveTimeout) {
clearTimeout(this.#saveTimeout);
if (this.#saveTimeout) {
clearTimeout(this.#saveTimeout);
}
if (!this.#savePromise) {
this.#savePromise = new Promise((resolve) => {
this.#savePromiseResolve = resolve;
});
}
this.#saveTimeout = setTimeout(async () => {
this.#saveTimeout = void 0;
if (this.#file) {
await this.writeToDisk();
}
this.#maybeResolveSavePromise();
}, SAVE_DEBOUNCE_MS);
}
#writing = /* @__PURE__ */ new Set();
#pending = /* @__PURE__ */ new Set();
async #writeFileAtomic(filePath, data, depth = 0) {
if (depth > MAX_DEPTH) {
return;
}
const fileKey = filePath.toString();
if (this.#writing.has(fileKey)) {
this.#pending.add(fileKey);
return;
}
this.#writing.add(fileKey);
const tempFile = filePath instanceof URL ? new URL(`${filePath.href}.tmp`) : `${filePath}.tmp`;
try {
const oldData = await fs.readFile(filePath, "utf-8").catch(() => "");
if (oldData === data) {
return;
}
await fs.writeFile(tempFile, data);
await fs.rename(tempFile, filePath);
} finally {
this.#writing.delete(fileKey);
if (this.#pending.has(fileKey)) {
this.#pending.delete(fileKey);
await this.#writeFileAtomic(filePath, data, depth + 1);
}
this.#saveTimeout = setTimeout(() => {
this.#saveTimeout = void 0;
this.writeToDisk(this.#file);
}, SAVE_DEBOUNCE_MS);
}
}
scopedStore(collectionName) {
@@ -159,7 +226,17 @@ ${lines.join(",\n")}]);
entries: () => this.entries(collectionName),
values: () => this.values(collectionName),
keys: () => this.keys(collectionName),
set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
set: ({
id: key,
data,
body,
filePath,
deferredRender,
digest,
rendered,
assetImports,
legacyId
}) => {
if (!key) {
throw new Error(`ID must be a non-empty string`);
}
@@ -200,6 +277,9 @@ ${lines.join(",\n")}]);
if (rendered) {
entry.rendered = rendered;
}
if (legacyId) {
entry.legacyId = legacyId;
}
if (deferredRender) {
entry.deferredRender = deferredRender;
if (filePath) {
@@ -229,17 +309,30 @@ ${lines.join(",\n")}]);
has: (key) => this.has(collectionKey, key)
};
}
/**
* Returns a promise that resolves when all pending saves are complete.
* This includes any in-progress debounced saves for the data store, asset imports, and module imports.
*/
async waitUntilSaveComplete() {
if (!this.#savePromise) {
return Promise.resolve();
}
await this.#saveToDiskNow();
return this.#savePromise;
}
toString() {
return devalue.stringify(this._collections);
}
async writeToDisk(filePath) {
async writeToDisk() {
if (!this.#dirty) {
return;
}
if (!this.#file) {
throw new AstroError(AstroErrorData.UnknownFilesystemError);
}
try {
await fs.writeFile(filePath, this.toString());
this.#file = filePath;
this.#dirty = false;
await this.#writeFileAtomic(this.#file, this.toString());
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@@ -270,11 +363,17 @@ ${lines.join(",\n")}]);
try {
if (existsSync(filePath)) {
const data = await fs.readFile(filePath, "utf-8");
return MutableDataStore.fromString(data);
const store2 = await MutableDataStore.fromString(data);
store2.#file = filePath;
return store2;
} else {
await fs.mkdir(new URL("./", filePath), { recursive: true });
}
} catch {
}
return new MutableDataStore();
const store = new MutableDataStore();
store.#file = filePath;
return store;
}
}
export {

View File

@@ -1,11 +1,11 @@
import type { PluginContext } from 'rollup';
import { z } from 'zod';
export declare function createImage(pluginContext: PluginContext, shouldEmitFile: boolean, entryFilePath: string): () => z.ZodEffects<z.ZodString, z.ZodNever | {
export declare function createImage(pluginContext: PluginContext, shouldEmitFile: boolean, entryFilePath: string, experimentalSvgEnabled: boolean): () => z.ZodEffects<z.ZodString, z.ZodNever | {
ASTRO_ASSET: string;
src: string;
format: import("../assets/types.js").ImageInputFormat;
width: number;
height: number;
format: import("../assets/types.js").ImageInputFormat;
src: string;
fsPath: string;
orientation?: number | undefined;
}, string>;

View File

@@ -1,12 +1,14 @@
import { z } from "zod";
import { emitESMImage } from "../assets/utils/node/emitAsset.js";
function createImage(pluginContext, shouldEmitFile, entryFilePath) {
function createImage(pluginContext, shouldEmitFile, entryFilePath, experimentalSvgEnabled) {
return () => {
return z.string().transform(async (imagePath, ctx) => {
const resolvedFilePath = (await pluginContext.resolve(imagePath, entryFilePath))?.id;
const metadata = await emitESMImage(
resolvedFilePath,
pluginContext.meta.watchMode,
// FUTURE: Remove in this in v6
experimentalSvgEnabled,
shouldEmitFile ? pluginContext.emitFile : void 0
);
if (!metadata) {

View File

@@ -1,27 +1,38 @@
import type { MarkdownHeading } from '@astrojs/markdown-remark';
import { z } from 'zod';
import { type AstroComponentFactory } from '../runtime/server/index.js';
import type { LiveDataCollectionResult, LiveDataEntryResult } from '../types/public/content.js';
import { type LIVE_CONTENT_TYPE } from './consts.js';
import { type DataEntry } from './data-store.js';
import { LiveCollectionCacheHintError, LiveCollectionError, LiveCollectionValidationError, LiveEntryNotFoundError } from './loaders/errors.js';
import type { LiveLoader } from './loaders/types.js';
import type { ContentLookupMap } from './utils.js';
export { LiveCollectionError, LiveCollectionCacheHintError, LiveEntryNotFoundError, LiveCollectionValidationError, };
type LazyImport = () => Promise<any>;
type GlobResult = Record<string, LazyImport>;
type CollectionToEntryMap = Record<string, GlobResult>;
type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;
export declare function defineCollection(config: any): any;
type LiveCollectionConfigMap = Record<string, {
loader: LiveLoader;
type: typeof LIVE_CONTENT_TYPE;
schema?: z.ZodType;
}>;
export declare function createCollectionToGlobResultMap({ globResult, contentDir, }: {
globResult: GlobResult;
contentDir: string;
}): CollectionToEntryMap;
export declare function createGetCollection({ contentCollectionToEntryMap, dataCollectionToEntryMap, getRenderEntryImport, cacheEntriesByCollection, }: {
export declare function createGetCollection({ contentCollectionToEntryMap, dataCollectionToEntryMap, getRenderEntryImport, cacheEntriesByCollection, liveCollections, }: {
contentCollectionToEntryMap: CollectionToEntryMap;
dataCollectionToEntryMap: CollectionToEntryMap;
getRenderEntryImport: GetEntryImport;
cacheEntriesByCollection: Map<string, any[]>;
}): (collection: string, filter?: (entry: any) => unknown) => Promise<any[]>;
export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImport, collectionNames, }: {
liveCollections: LiveCollectionConfigMap;
}): (collection: string, filter?: ((entry: any) => unknown) | Record<string, unknown>) => Promise<any[]>;
export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImport, collectionNames, getEntry, }: {
getEntryImport: GetEntryImport;
getRenderEntryImport: GetEntryImport;
collectionNames: Set<string>;
getEntry: ReturnType<typeof createGetEntry>;
}): (collection: string, slug: string) => Promise<{
id: any;
slug: any;
@@ -30,10 +41,11 @@ export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImp
data: any;
render(): Promise<RenderResult>;
} | undefined>;
export declare function createGetDataEntryById({ getEntryImport, collectionNames, }: {
export declare function createGetDataEntryById({ getEntryImport, collectionNames, getEntry, }: {
getEntryImport: GetEntryImport;
collectionNames: Set<string>;
}): (collection: string, id: string) => Promise<{
getEntry: ReturnType<typeof createGetEntry>;
}): (collection: string, id: string) => Promise<ContentEntryResult | {
id: any;
collection: any;
data: any;
@@ -58,11 +70,12 @@ type EntryLookupObject = {
collection: string;
slug: string;
};
export declare function createGetEntry({ getEntryImport, getRenderEntryImport, collectionNames, }: {
export declare function createGetEntry({ getEntryImport, getRenderEntryImport, collectionNames, liveCollections, }: {
getEntryImport: GetEntryImport;
getRenderEntryImport: GetEntryImport;
collectionNames: Set<string>;
}): (collectionOrLookupObject: string | EntryLookupObject, _lookupId?: string) => Promise<ContentEntryResult | DataEntryResult | undefined>;
liveCollections: LiveCollectionConfigMap;
}): (collectionOrLookupObject: string | EntryLookupObject, lookup?: string | Record<string, unknown>) => Promise<ContentEntryResult | DataEntryResult | undefined>;
export declare function createGetEntries(getEntry: ReturnType<typeof createGetEntry>): (entries: {
collection: string;
id: string;
@@ -70,6 +83,12 @@ export declare function createGetEntries(getEntry: ReturnType<typeof createGetEn
collection: string;
slug: string;
}[]) => Promise<(ContentEntryResult | DataEntryResult | undefined)[]>;
export declare function createGetLiveCollection({ liveCollections, }: {
liveCollections: LiveCollectionConfigMap;
}): (collection: string, filter?: Record<string, unknown>) => Promise<LiveDataCollectionResult>;
export declare function createGetLiveEntry({ liveCollections, }: {
liveCollections: LiveCollectionConfigMap;
}): (collection: string, lookup: string | Record<string, unknown>) => Promise<LiveDataEntryResult>;
type RenderResult = {
Content: AstroComponentFactory;
headings: MarkdownHeading[];
@@ -79,7 +98,11 @@ export declare function renderEntry(entry: DataEntry | {
render: () => Promise<{
Content: AstroComponentFactory;
}>;
}): Promise<{
} | (DataEntry & {
render: () => Promise<{
Content: AstroComponentFactory;
}>;
})): Promise<{
Content: AstroComponentFactory;
}>;
export declare function createReference({ lookupMap }: {
@@ -97,11 +120,11 @@ export declare function createReference({ lookupMap }: {
slug: z.ZodString;
collection: z.ZodString;
}, "strip", z.ZodTypeAny, {
collection: string;
slug: string;
collection: string;
}, {
collection: string;
slug: string;
collection: string;
}>]>, {
id: string;
collection: string;
@@ -112,7 +135,8 @@ export declare function createReference({ lookupMap }: {
id: string;
collection: string;
} | {
collection: string;
slug: string;
collection: string;
}>;
export {};
export declare function defineCollection(config: any): import("./config.js").CollectionConfig<import("./config.js").BaseSchema>;
export declare function defineLiveCollection(): void;

View File

@@ -1,8 +1,9 @@
import { escape } from "html-escaper";
import { Traverse } from "neotraverse/modern";
import pLimit from "p-limit";
import { ZodIssueCode, z } from "zod";
import { imageSrcToImportId } from "../assets/utils/resolveImports.js";
import { AstroError, AstroErrorData, AstroUserError } from "../core/errors/index.js";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { prependForwardSlash } from "../core/path.js";
import {
createComponent,
@@ -14,21 +15,15 @@ import {
render as serverRender,
unescapeHTML
} from "../runtime/server/index.js";
import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from "./consts.js";
import { defineCollection as defineCollectionOrig } from "./config.js";
import { IMAGE_IMPORT_PREFIX } from "./consts.js";
import { globalDataStore } from "./data-store.js";
function defineCollection(config) {
if ("loader" in config) {
if (config.type && config.type !== CONTENT_LAYER_TYPE) {
throw new AstroUserError(
"Collections that use the Content Layer API must have a `loader` defined and no `type` set.",
"Check your collection definitions in `src/content/config.*`.'"
);
}
config.type = CONTENT_LAYER_TYPE;
}
if (!config.type) config.type = "content";
return config;
}
import {
LiveCollectionCacheHintError,
LiveCollectionError,
LiveCollectionValidationError,
LiveEntryNotFoundError
} from "./loaders/errors.js";
function createCollectionToGlobResultMap({
globResult,
contentDir
@@ -44,13 +39,58 @@ function createCollectionToGlobResultMap({
}
return collectionToGlobResultMap;
}
const cacheHintSchema = z.object({
tags: z.array(z.string()).optional(),
maxAge: z.number().optional(),
lastModified: z.date().optional()
});
async function parseLiveEntry(entry, schema, collection) {
try {
const parsed = await schema.safeParseAsync(entry.data);
if (!parsed.success) {
return {
error: new LiveCollectionValidationError(collection, entry.id, parsed.error)
};
}
if (entry.cacheHint) {
const cacheHint = cacheHintSchema.safeParse(entry.cacheHint);
if (!cacheHint.success) {
return {
error: new LiveCollectionCacheHintError(collection, entry.id, cacheHint.error)
};
}
entry.cacheHint = cacheHint.data;
}
return {
entry: {
...entry,
data: parsed.data
}
};
} catch (error) {
return {
error: new LiveCollectionError(
collection,
`Unexpected error parsing entry ${entry.id} in collection ${collection}`,
error
)
};
}
}
function createGetCollection({
contentCollectionToEntryMap,
dataCollectionToEntryMap,
getRenderEntryImport,
cacheEntriesByCollection
cacheEntriesByCollection,
liveCollections
}) {
return async function getCollection(collection, filter) {
if (collection in liveCollections) {
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `Collection "${collection}" is a live collection. Use getLiveCollection() instead of getCollection().`
});
}
const hasFilter = typeof filter === "function";
const store = await globalDataStore.get();
let type;
@@ -63,11 +103,14 @@ function createGetCollection({
const result = [];
for (const rawEntry of store.values(collection)) {
const data = updateImageReferencesInData(rawEntry.data, rawEntry.filePath, imageAssetMap);
const entry = {
let entry = {
...rawEntry,
data,
collection
};
if (entry.legacyId) {
entry = emulateLegacyEntry(entry);
}
if (hasFilter && !filter(entry)) {
continue;
}
@@ -78,7 +121,7 @@ function createGetCollection({
console.warn(
`The collection ${JSON.stringify(
collection
)} does not exist or is empty. Ensure a collection directory with this name exists.`
)} does not exist or is empty. Please check your content config file for errors.`
);
return [];
}
@@ -127,18 +170,25 @@ function createGetCollection({
function createGetEntryBySlug({
getEntryImport,
getRenderEntryImport,
collectionNames
collectionNames,
getEntry
}) {
return async function getEntryBySlug(collection, slug) {
const store = await globalDataStore.get();
if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
const entry2 = await getEntry(collection, slug);
if (entry2 && "slug" in entry2) {
return entry2;
}
throw new AstroError({
...AstroErrorData.GetEntryDeprecationError,
message: AstroErrorData.GetEntryDeprecationError.message(collection, "getEntryBySlug")
});
}
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
);
return void 0;
}
const entryImport = await getEntryImport(collection, slug);
@@ -162,18 +212,18 @@ function createGetEntryBySlug({
}
function createGetDataEntryById({
getEntryImport,
collectionNames
collectionNames,
getEntry
}) {
return async function getDataEntryById(collection, id) {
const store = await globalDataStore.get();
if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
throw new AstroError({
...AstroErrorData.GetEntryDeprecationError,
message: AstroErrorData.GetEntryDeprecationError.message(collection, "getDataEntryById")
});
return getEntry(collection, id);
}
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
);
return void 0;
}
const lazyImport = await getEntryImport(collection, id);
@@ -186,25 +236,50 @@ function createGetDataEntryById({
};
};
}
function emulateLegacyEntry({ legacyId, ...entry }) {
const legacyEntry = {
...entry,
id: legacyId,
slug: entry.id
};
return {
...legacyEntry,
// Define separately so the render function isn't included in the object passed to `renderEntry()`
render: () => renderEntry(legacyEntry)
};
}
function createGetEntry({
getEntryImport,
getRenderEntryImport,
collectionNames
collectionNames,
liveCollections
}) {
return async function getEntry(collectionOrLookupObject, _lookupId) {
return async function getEntry(collectionOrLookupObject, lookup) {
let collection, lookupId;
if (typeof collectionOrLookupObject === "string") {
collection = collectionOrLookupObject;
if (!_lookupId)
if (!lookup)
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: "`getEntry()` requires an entry identifier as the second argument."
});
lookupId = _lookupId;
lookupId = lookup;
} else {
collection = collectionOrLookupObject.collection;
lookupId = "id" in collectionOrLookupObject ? collectionOrLookupObject.id : collectionOrLookupObject.slug;
}
if (collection in liveCollections) {
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `Collection "${collection}" is a live collection. Use getLiveEntry() instead of getEntry().`
});
}
if (typeof lookupId === "object") {
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `The entry identifier must be a string. Received object.`
});
}
const store = await globalDataStore.get();
if (store.hasCollection(collection)) {
const entry2 = store.get(collection, lookupId);
@@ -214,13 +289,18 @@ function createGetEntry({
}
const { default: imageAssetMap } = await import("astro:asset-imports");
entry2.data = updateImageReferencesInData(entry2.data, entry2.filePath, imageAssetMap);
if (entry2.legacyId) {
return emulateLegacyEntry({ ...entry2, collection });
}
return {
...entry2,
collection
};
}
if (!collectionNames.has(collection)) {
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
);
return void 0;
}
const entryImport = await getEntryImport(collection, lookupId);
@@ -256,6 +336,149 @@ function createGetEntries(getEntry) {
return Promise.all(entries.map((e) => getEntry(e)));
};
}
function createGetLiveCollection({
liveCollections
}) {
return async function getLiveCollection(collection, filter) {
if (!(collection in liveCollections)) {
return {
error: new LiveCollectionError(
collection,
`Collection "${collection}" is not a live collection. Use getCollection() instead of getLiveCollection() to load regular content collections.`
)
};
}
try {
const context = {
filter
};
const response = await liveCollections[collection].loader?.loadCollection?.(context);
if (response && "error" in response) {
return { error: response.error };
}
const { schema } = liveCollections[collection];
let processedEntries = response.entries;
if (schema) {
const entryResults = await Promise.all(
response.entries.map((entry) => parseLiveEntry(entry, schema, collection))
);
for (const result of entryResults) {
if (result.error) {
return { error: result.error };
}
}
processedEntries = entryResults.map((result) => result.entry);
}
let cacheHint = response.cacheHint;
if (cacheHint) {
const cacheHintResult = cacheHintSchema.safeParse(cacheHint);
if (!cacheHintResult.success) {
return {
error: new LiveCollectionCacheHintError(collection, void 0, cacheHintResult.error)
};
}
cacheHint = cacheHintResult.data;
}
if (processedEntries.length > 0) {
const entryTags = /* @__PURE__ */ new Set();
let minMaxAge;
let latestModified;
for (const entry of processedEntries) {
if (entry.cacheHint) {
if (entry.cacheHint.tags) {
entry.cacheHint.tags.forEach((tag) => entryTags.add(tag));
}
if (typeof entry.cacheHint.maxAge === "number") {
if (minMaxAge === void 0 || entry.cacheHint.maxAge < minMaxAge) {
minMaxAge = entry.cacheHint.maxAge;
}
}
if (entry.cacheHint.lastModified instanceof Date) {
if (latestModified === void 0 || entry.cacheHint.lastModified > latestModified) {
latestModified = entry.cacheHint.lastModified;
}
}
}
}
if (entryTags.size > 0 || minMaxAge !== void 0 || latestModified || cacheHint) {
const mergedCacheHint = {};
if (cacheHint?.tags || entryTags.size > 0) {
mergedCacheHint.tags = [.../* @__PURE__ */ new Set([...cacheHint?.tags || [], ...entryTags])];
}
if (cacheHint?.maxAge !== void 0 || minMaxAge !== void 0) {
mergedCacheHint.maxAge = cacheHint?.maxAge !== void 0 && minMaxAge !== void 0 ? Math.min(cacheHint.maxAge, minMaxAge) : cacheHint?.maxAge ?? minMaxAge;
}
if (cacheHint?.lastModified && latestModified) {
mergedCacheHint.lastModified = cacheHint.lastModified > latestModified ? cacheHint.lastModified : latestModified;
} else if (cacheHint?.lastModified || latestModified) {
mergedCacheHint.lastModified = cacheHint?.lastModified ?? latestModified;
}
cacheHint = mergedCacheHint;
}
}
return {
entries: processedEntries,
cacheHint
};
} catch (error) {
return {
error: new LiveCollectionError(
collection,
`Unexpected error loading collection ${collection}${error instanceof Error ? `: ${error.message}` : ""}`,
error
)
};
}
};
}
function createGetLiveEntry({
liveCollections
}) {
return async function getLiveEntry(collection, lookup) {
if (!(collection in liveCollections)) {
return {
error: new LiveCollectionError(
collection,
`Collection "${collection}" is not a live collection. Use getCollection() instead of getLiveEntry() to load regular content collections.`
)
};
}
try {
const lookupObject = {
filter: typeof lookup === "string" ? { id: lookup } : lookup
};
let entry = await liveCollections[collection].loader?.loadEntry?.(lookupObject);
if (entry && "error" in entry) {
return { error: entry.error };
}
if (!entry) {
return {
error: new LiveEntryNotFoundError(collection, lookup)
};
}
const { schema } = liveCollections[collection];
if (schema) {
const result = await parseLiveEntry(entry, schema, collection);
if (result.error) {
return { error: result.error };
}
entry = result.entry;
}
return {
entry,
cacheHint: entry.cacheHint
};
} catch (error) {
return {
error: new LiveCollectionError(
collection,
`Unexpected error loading entry ${collection} \u2192 ${typeof lookup === "string" ? lookup : JSON.stringify(lookup)}`,
error
)
};
}
};
}
const CONTENT_LAYER_IMAGE_REGEX = /__ASTRO_IMAGE_="([^"]+)"/g;
async function updateImageReferencesInBody(html, fileName) {
const { default: imageAssetMap } = await import("astro:asset-imports");
@@ -264,12 +487,17 @@ async function updateImageReferencesInBody(html, fileName) {
for (const [_full, imagePath] of html.matchAll(CONTENT_LAYER_IMAGE_REGEX)) {
try {
const decodedImagePath = JSON.parse(imagePath.replaceAll("&#x22;", '"'));
const id = imageSrcToImportId(decodedImagePath.src, fileName);
const imported = imageAssetMap.get(id);
if (!id || imageObjects.has(id) || !imported) {
continue;
let image;
if (URL.canParse(decodedImagePath.src)) {
image = await getImage(decodedImagePath);
} else {
const id = imageSrcToImportId(decodedImagePath.src, fileName);
const imported = imageAssetMap.get(id);
if (!id || imageObjects.has(id) || !imported) {
continue;
}
image = await getImage({ ...decodedImagePath, src: imported });
}
const image = await getImage({ ...decodedImagePath, src: imported });
imageObjects.set(imagePath, image);
} catch {
throw new Error(`Failed to parse image reference: ${imagePath}`);
@@ -284,8 +512,10 @@ async function updateImageReferencesInBody(html, fileName) {
return Object.entries({
...attributes,
src: image.src,
srcset: image.srcSet.attribute
}).map(([key, value]) => value ? `${key}=${JSON.stringify(String(value))}` : "").join(" ");
srcset: image.srcSet.attribute,
// This attribute is used by the toolbar audit
...import.meta.env.DEV ? { "data-image-component": "true" } : {}
}).map(([key, value]) => value ? `${key}="${escape(value)}"` : "").join(" ");
});
}
function updateImageReferencesInData(data, fileName, imageAssetMap) {
@@ -310,7 +540,7 @@ async function renderEntry(entry) {
if (!entry) {
throw new AstroError(AstroErrorData.RenderUndefinedEntryError);
}
if ("render" in entry) {
if ("render" in entry && !("legacyId" in entry)) {
return entry.render();
}
if (entry.deferredRender) {
@@ -422,10 +652,8 @@ function createReference({ lookupMap }) {
collection: z.string()
})
]).transform(
async (lookup, ctx) => {
(lookup, ctx) => {
const flattenedErrorPath = ctx.path.join(".");
const store = await globalDataStore.get();
const collectionIsInStore = store.hasCollection(collection);
if (typeof lookup === "object") {
if (lookup.collection !== collection) {
ctx.addIssue({
@@ -436,18 +664,7 @@ function createReference({ lookupMap }) {
}
return lookup;
}
if (collectionIsInStore) {
const entry2 = store.get(collection, lookup);
if (!entry2) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Entry ${lookup} does not exist.`
});
return;
}
return { id: lookup, collection };
}
if (!lookupMap[collection] && store.collections().size <= 1) {
if (!lookupMap[collection]) {
return { id: lookup, collection };
}
const { type, entries } = lookupMap[collection];
@@ -472,14 +689,40 @@ function createReference({ lookupMap }) {
function isPropagatedAssetsModule(module) {
return typeof module === "object" && module != null && "__astroPropagation" in module;
}
function defineCollection(config) {
if (config.type === "live") {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Collections with type `live` must be defined in a `src/live.config.ts` file."
)
});
}
return defineCollectionOrig(config);
}
function defineLiveCollection() {
throw new AstroError({
...AstroErrorData.LiveContentConfigError,
message: AstroErrorData.LiveContentConfigError.message(
"Live collections must be defined in a `src/live.config.ts` file."
)
});
}
export {
LiveCollectionCacheHintError,
LiveCollectionError,
LiveCollectionValidationError,
LiveEntryNotFoundError,
createCollectionToGlobResultMap,
createGetCollection,
createGetDataEntryById,
createGetEntries,
createGetEntry,
createGetEntryBySlug,
createGetLiveCollection,
createGetLiveEntry,
createReference,
defineCollection,
defineLiveCollection,
renderEntry
};

View File

@@ -1,7 +1,7 @@
import type fsMod from 'node:fs';
import type { ViteDevServer } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import type { AstroSettings } from '../types/astro.js';
interface ContentServerListenerParams {
fs: typeof fsMod;
logger: Logger;

View File

@@ -12,7 +12,9 @@ async function attachContentServerListeners({
settings
}) {
const contentPaths = getContentPaths(settings.config, fs);
if (fs.existsSync(contentPaths.contentDir)) {
if (!settings.config.legacy?.collections) {
await attachListeners();
} else if (fs.existsSync(contentPaths.contentDir)) {
logger.debug(
"content",
`Watching ${cyan(
@@ -49,10 +51,9 @@ async function attachContentServerListeners({
"addDir",
(entry) => contentGenerator.queueEvent({ name: "addDir", entry })
);
viteServer.watcher.on(
"change",
(entry) => contentGenerator.queueEvent({ name: "change", entry })
);
viteServer.watcher.on("change", (entry) => {
contentGenerator.queueEvent({ name: "change", entry });
});
viteServer.watcher.on("unlink", (entry) => {
contentGenerator.queueEvent({ name: "unlink", entry });
});

View File

@@ -1,7 +1,7 @@
import type fsMod from 'node:fs';
import { type ViteDevServer } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import type { AstroSettings } from '../types/astro.js';
import { type ContentObservable } from './utils.js';
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
type RawContentEvent = {

View File

@@ -1,14 +1,20 @@
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import glob from "fast-glob";
import { bold, cyan } from "kleur/colors";
import { glob } from "tinyglobby";
import { normalizePath } from "vite";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { AstroError } from "../core/errors/errors.js";
import { AstroErrorData } from "../core/errors/index.js";
import { AstroErrorData, AstroUserError } from "../core/errors/index.js";
import { isRelativePath } from "../core/path.js";
import { CONTENT_LAYER_TYPE, CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from "./consts.js";
import {
COLLECTIONS_DIR,
CONTENT_LAYER_TYPE,
CONTENT_TYPES_FILE,
LIVE_CONTENT_TYPE,
VIRTUAL_MODULE_ID
} from "./consts.js";
import {
getContentEntryIdAndSlug,
getContentPaths,
@@ -36,27 +42,24 @@ async function createContentTypesGenerator({
let debounceTimeout;
const typeTemplateContent = await fs.promises.readFile(contentPaths.typesTemplate, "utf-8");
async function init() {
if (!fs.existsSync(contentPaths.contentDir)) {
return { typesGenerated: false, reason: "no-content-dir" };
}
events.push({ name: "add", entry: contentPaths.config.url });
const globResult = await glob("**", {
cwd: fileURLToPath(contentPaths.contentDir),
fs: {
readdir: fs.readdir.bind(fs),
readdirSync: fs.readdirSync.bind(fs)
},
onlyFiles: false,
objectMode: true
});
for (const entry of globResult) {
const fullPath = path.join(fileURLToPath(contentPaths.contentDir), entry.path);
const entryURL = pathToFileURL(fullPath);
if (entryURL.href.startsWith(contentPaths.config.url.href)) continue;
if (entry.dirent.isFile()) {
events.push({ name: "add", entry: entryURL });
} else if (entry.dirent.isDirectory()) {
events.push({ name: "addDir", entry: entryURL });
if (settings.config.legacy.collections) {
if (!fs.existsSync(contentPaths.contentDir)) {
return { typesGenerated: false, reason: "no-content-dir" };
}
const globResult = await glob("**", {
cwd: fileURLToPath(contentPaths.contentDir),
absolute: true
});
for (const fullPath of globResult) {
const entryURL = pathToFileURL(fullPath);
if (entryURL.href.startsWith(contentPaths.config.url.href)) continue;
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
events.push({ name: "add", entry: entryURL });
} else if (stat.isDirectory()) {
events.push({ name: "addDir", entry: entryURL });
}
}
}
await runEvents();
@@ -219,7 +222,13 @@ async function createContentTypesGenerator({
entry: pathToFileURL(rawEvent.entry),
name: rawEvent.name
};
if (!event.entry.pathname.startsWith(contentPaths.contentDir.pathname)) return;
if (settings.config.legacy.collections) {
if (!event.entry.pathname.startsWith(contentPaths.contentDir.pathname)) {
return;
}
} else if (contentPaths.config.url.pathname !== event.entry.pathname) {
return;
}
events.push(event);
debounceTimeout && clearTimeout(debounceTimeout);
const runEventsSafe = async () => {
@@ -320,10 +329,8 @@ async function writeContentFiles({
}) {
let contentTypesStr = "";
let dataTypesStr = "";
const collectionSchemasDir = new URL("./collections/", settings.dotAstroDir);
if (!fs.existsSync(collectionSchemasDir)) {
fs.mkdirSync(collectionSchemasDir, { recursive: true });
}
const collectionSchemasDir = new URL(COLLECTIONS_DIR, settings.dotAstroDir);
fs.mkdirSync(collectionSchemasDir, { recursive: true });
for (const [collection, config] of Object.entries(contentConfig?.collections ?? {})) {
collectionEntryMap[JSON.stringify(collection)] ??= {
type: config.type,
@@ -360,6 +367,10 @@ async function writeContentFiles({
const collectionEntryKeys = Object.keys(collection.entries).sort();
const dataType = await typeForCollection(collectionConfig, collectionKey);
switch (resolvedType) {
case LIVE_CONTENT_TYPE:
throw new AstroUserError(
`Invalid definition for collection ${collectionKey}: Live content collections must be defined in "src/live.config.ts"`
);
case "content":
if (collectionEntryKeys.length === 0) {
contentTypesStr += `${collectionKey}: Record<string, {
@@ -394,13 +405,13 @@ async function writeContentFiles({
`;
break;
case CONTENT_LAYER_TYPE:
const legacyTypes = collectionConfig?._legacy ? 'render(): Render[".md"];\n slug: string;\n body: string;\n' : "body?: string;\n";
dataTypesStr += `${collectionKey}: Record<string, {
id: string;
collection: ${collectionKey};
${legacyTypes} collection: ${collectionKey};
data: ${dataType};
rendered?: RenderedContent;
filePath?: string;
body?: string
}>;
`;
break;
@@ -426,18 +437,9 @@ async function writeContentFiles({
dataTypesStr += `};
`;
}
if (collectionConfig?.schema) {
await generateJSONSchema(
fs,
collectionConfig,
collectionKey,
collectionSchemasDir,
logger
);
}
break;
}
if (settings.config.experimental.contentIntellisense && collectionConfig && (collectionConfig.schema || await getContentLayerSchema(collectionConfig, collectionKey))) {
if (collectionConfig && (collectionConfig.schema || await getContentLayerSchema(collectionConfig, collectionKey))) {
await generateJSONSchema(fs, collectionConfig, collectionKey, collectionSchemasDir, logger);
contentCollectionsMap[collectionKey] = collection;
}
@@ -467,28 +469,29 @@ async function writeContentFiles({
JSON.stringify(contentCollectionManifest, null, 2)
);
}
if (!fs.existsSync(settings.dotAstroDir)) {
fs.mkdirSync(settings.dotAstroDir, { recursive: true });
}
const configPathRelativeToCacheDir = normalizeConfigPath(
new URL("astro", settings.dotAstroDir).pathname,
settings.dotAstroDir.pathname,
contentPaths.config.url.pathname
);
const liveConfigPathRelativeToCacheDir = contentPaths.liveConfig?.exists ? normalizeConfigPath(settings.dotAstroDir.pathname, contentPaths.liveConfig.url.pathname) : void 0;
for (const contentEntryType of contentEntryTypes) {
if (contentEntryType.contentModuleTypes) {
typeTemplateContent = contentEntryType.contentModuleTypes + "\n" + typeTemplateContent;
}
}
typeTemplateContent = typeTemplateContent.replace("// @@CONTENT_ENTRY_MAP@@", contentTypesStr);
typeTemplateContent = typeTemplateContent.replace("// @@DATA_ENTRY_MAP@@", dataTypesStr);
typeTemplateContent = typeTemplateContent.replace(
typeTemplateContent = typeTemplateContent.replace("// @@CONTENT_ENTRY_MAP@@", contentTypesStr).replace("// @@DATA_ENTRY_MAP@@", dataTypesStr).replace(
"'@@CONTENT_CONFIG_TYPE@@'",
contentConfig ? `typeof import(${configPathRelativeToCacheDir})` : "never"
).replace(
"'@@LIVE_CONTENT_CONFIG_TYPE@@'",
liveConfigPathRelativeToCacheDir ? `typeof import(${liveConfigPathRelativeToCacheDir})` : "never"
);
if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) {
const filePath = fileURLToPath(new URL(CONTENT_TYPES_FILE, settings.dotAstroDir));
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
await fs.promises.writeFile(filePath, typeTemplateContent, "utf-8");
await fs.promises.writeFile(
new URL(CONTENT_TYPES_FILE, settings.dotAstroDir),
typeTemplateContent,
"utf-8"
);
} else {
settings.injectedTypes.push({
filename: CONTENT_TYPES_FILE,

View File

@@ -1,13 +1,14 @@
import fsMod from 'node:fs';
import matter from 'gray-matter';
import type { PluginContext } from 'rollup';
import type { ViteDevServer } from 'vite';
import { z } from 'zod';
import type { AstroConfig, AstroSettings, ContentEntryType, DataEntryType } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import { CONTENT_FLAGS } from './consts.js';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import type { ContentEntryType, DataEntryType } from '../types/public/content.js';
import { type CONTENT_FLAGS } from './consts.js';
/**
* Amap from a collection + slug to the local file path.
* A map from a collection + slug to the local file path.
* This is used internally to resolve entry imports when using `getEntry()`.
* @see `templates/content/module.mjs`
*/
@@ -19,6 +20,19 @@ export type ContentLookupMap = {
};
};
};
export declare const loaderReturnSchema: z.ZodUnion<[z.ZodArray<z.ZodObject<{
id: z.ZodString;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
}, z.ZodTypeAny, "passthrough">>, "many">, z.ZodRecord<z.ZodString, z.ZodObject<{
id: z.ZodOptional<z.ZodString>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodOptional<z.ZodString>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodOptional<z.ZodString>;
}, z.ZodTypeAny, "passthrough">>>]>;
declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"content">>>;
schema: z.ZodOptional<z.ZodAny>;
@@ -40,19 +54,7 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
}>, z.ZodObject<{
type: z.ZodLiteral<"content_layer">;
schema: z.ZodOptional<z.ZodAny>;
loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnion<[z.ZodArray<z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodUnknown, z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">, z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">>, "many">, z.ZodPromise<z.ZodArray<z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodUnknown, z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">, z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">>, "many">>]>>, z.ZodObject<{
loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>, z.ZodObject<{
name: z.ZodString;
load: z.ZodFunction<z.ZodTuple<[z.ZodObject<{
collection: z.ZodString;
@@ -62,28 +64,34 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
config: z.ZodAny;
entryTypes: z.ZodAny;
parseData: z.ZodAny;
renderMarkdown: z.ZodAny;
generateDigest: z.ZodFunction<z.ZodTuple<[z.ZodAny], null>, z.ZodUnknown>;
watcher: z.ZodOptional<z.ZodAny>;
refreshContextData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "strip", z.ZodTypeAny, {
collection: string;
generateDigest: (args_0: any) => unknown;
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}, {
collection: string;
generateDigest: (args_0: any) => unknown;
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}>], null>, z.ZodUnknown>;
schema: z.ZodOptional<z.ZodAny>;
render: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodAny], null>, z.ZodUnknown>>;
@@ -95,10 +103,12 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
@@ -110,21 +120,21 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
}>]>;
/** deprecated */
_legacy: z.ZodOptional<z.ZodBoolean>;
}, "strip", z.ZodTypeAny, {
type: "content_layer";
loader: ((...args: unknown[]) => z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[] | Promise<z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[]>) | {
loader: ((...args: unknown[]) => unknown) | {
name: string;
load: (args_0: {
collection: string;
@@ -132,22 +142,21 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
};
schema?: any;
_legacy?: boolean | undefined;
}, {
type: "content_layer";
loader: ((...args: unknown[]) => z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[] | Promise<z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[]>) | {
loader: ((...args: unknown[]) => unknown) | {
name: string;
load: (args_0: {
collection: string;
@@ -155,15 +164,30 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
};
schema?: any;
_legacy?: boolean | undefined;
}>, z.ZodObject<{
type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"live">>>;
schema: z.ZodOptional<z.ZodAny>;
loader: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>;
}, "strip", z.ZodTypeAny, {
type: "live";
loader: (...args: unknown[]) => unknown;
schema?: any;
}, {
loader: (...args: unknown[]) => unknown;
type?: "live" | undefined;
schema?: any;
}>]>;
declare const contentConfigParser: z.ZodObject<{
collections: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodObject<{
@@ -187,19 +211,7 @@ declare const contentConfigParser: z.ZodObject<{
}>, z.ZodObject<{
type: z.ZodLiteral<"content_layer">;
schema: z.ZodOptional<z.ZodAny>;
loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnion<[z.ZodArray<z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodUnknown, z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">, z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">>, "many">, z.ZodPromise<z.ZodArray<z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodUnknown, z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">, z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">>, "many">>]>>, z.ZodObject<{
loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>, z.ZodObject<{
name: z.ZodString;
load: z.ZodFunction<z.ZodTuple<[z.ZodObject<{
collection: z.ZodString;
@@ -209,28 +221,34 @@ declare const contentConfigParser: z.ZodObject<{
config: z.ZodAny;
entryTypes: z.ZodAny;
parseData: z.ZodAny;
renderMarkdown: z.ZodAny;
generateDigest: z.ZodFunction<z.ZodTuple<[z.ZodAny], null>, z.ZodUnknown>;
watcher: z.ZodOptional<z.ZodAny>;
refreshContextData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "strip", z.ZodTypeAny, {
collection: string;
generateDigest: (args_0: any) => unknown;
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}, {
collection: string;
generateDigest: (args_0: any) => unknown;
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}>], null>, z.ZodUnknown>;
schema: z.ZodOptional<z.ZodAny>;
render: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodAny], null>, z.ZodUnknown>>;
@@ -242,10 +260,12 @@ declare const contentConfigParser: z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
@@ -257,21 +277,21 @@ declare const contentConfigParser: z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
}>]>;
/** deprecated */
_legacy: z.ZodOptional<z.ZodBoolean>;
}, "strip", z.ZodTypeAny, {
type: "content_layer";
loader: ((...args: unknown[]) => z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[] | Promise<z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[]>) | {
loader: ((...args: unknown[]) => unknown) | {
name: string;
load: (args_0: {
collection: string;
@@ -279,22 +299,21 @@ declare const contentConfigParser: z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
};
schema?: any;
_legacy?: boolean | undefined;
}, {
type: "content_layer";
loader: ((...args: unknown[]) => z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[] | Promise<z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[]>) | {
loader: ((...args: unknown[]) => unknown) | {
name: string;
load: (args_0: {
collection: string;
@@ -302,15 +321,30 @@ declare const contentConfigParser: z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
};
schema?: any;
_legacy?: boolean | undefined;
}>, z.ZodObject<{
type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"live">>>;
schema: z.ZodOptional<z.ZodAny>;
loader: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>;
}, "strip", z.ZodTypeAny, {
type: "live";
loader: (...args: unknown[]) => unknown;
schema?: any;
}, {
loader: (...args: unknown[]) => unknown;
type?: "live" | undefined;
schema?: any;
}>]>>;
}, "strip", z.ZodTypeAny, {
collections: Record<string, {
@@ -321,11 +355,7 @@ declare const contentConfigParser: z.ZodObject<{
schema?: any;
} | {
type: "content_layer";
loader: ((...args: unknown[]) => z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[] | Promise<z.objectOutputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[]>) | {
loader: ((...args: unknown[]) => unknown) | {
name: string;
load: (args_0: {
collection: string;
@@ -333,15 +363,22 @@ declare const contentConfigParser: z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
};
schema?: any;
_legacy?: boolean | undefined;
} | {
type: "live";
loader: (...args: unknown[]) => unknown;
schema?: any;
}>;
}, {
collections: Record<string, {
@@ -352,11 +389,7 @@ declare const contentConfigParser: z.ZodObject<{
schema?: any;
} | {
type: "content_layer";
loader: ((...args: unknown[]) => z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[] | Promise<z.objectInputType<{
id: z.ZodString;
}, z.ZodUnknown, "strip">[]>) | {
loader: ((...args: unknown[]) => unknown) | {
name: string;
load: (args_0: {
collection: string;
@@ -364,15 +397,22 @@ declare const contentConfigParser: z.ZodObject<{
config?: any;
meta?: any;
logger?: any;
watcher?: any;
parseData?: any;
store?: any;
entryTypes?: any;
parseData?: any;
watcher?: any;
renderMarkdown?: any;
refreshContextData?: Record<string, unknown> | undefined;
}) => unknown;
schema?: any;
render?: ((args_0: any) => unknown) | undefined;
};
schema?: any;
_legacy?: boolean | undefined;
} | {
loader: (...args: unknown[]) => unknown;
type?: "live" | undefined;
schema?: any;
}>;
}>;
export type CollectionConfig = z.infer<typeof collectionConfigParser>;
@@ -394,7 +434,7 @@ export declare function getEntryDataAndImages<TInputData extends Record<string,
collection: string;
unvalidatedData: TInputData;
_internal: EntryInternal;
}, collectionConfig: CollectionConfig, shouldEmitFile: boolean, pluginContext?: PluginContext): Promise<{
}, collectionConfig: CollectionConfig, shouldEmitFile: boolean, experimentalSvgEnabled: boolean, pluginContext?: PluginContext): Promise<{
data: TOutputData;
imageImports: Array<string>;
}>;
@@ -403,7 +443,7 @@ export declare function getEntryData(entry: {
collection: string;
unvalidatedData: Record<string, unknown>;
_internal: EntryInternal;
}, collectionConfig: CollectionConfig, shouldEmitFile: boolean, pluginContext?: PluginContext): Promise<Record<string, unknown>>;
}, collectionConfig: CollectionConfig, shouldEmitFile: boolean, experimentalSvgEnabled: boolean, pluginContext?: PluginContext): Promise<Record<string, unknown>>;
export declare function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryTypes'>): string[];
export declare function getDataEntryExts(settings: Pick<AstroSettings, 'dataEntryTypes'>): string[];
export declare function getEntryConfigByExtMap<TEntryType extends ContentEntryType | DataEntryType>(entryTypes: TEntryType[]): Map<string, TEntryType>;
@@ -431,15 +471,14 @@ export declare function getContentEntryIdAndSlug({ entry, contentDir, collection
id: string;
slug: string;
};
export declare function getEntryType(entryPath: string, paths: Pick<ContentPaths, 'config' | 'contentDir'>, contentFileExts: string[], dataFileExts: string[]): 'content' | 'data' | 'config' | 'ignored';
export declare function safeParseFrontmatter(source: string, id?: string): matter.GrayMatterFile<string>;
export declare function getEntryType(entryPath: string, paths: Pick<ContentPaths, 'config' | 'contentDir' | 'root'>, contentFileExts: string[], dataFileExts: string[]): 'content' | 'data' | 'config' | 'ignored';
export declare function safeParseFrontmatter(source: string, id?: string): import("@astrojs/markdown-remark").ParseFrontmatterResult;
/**
* The content config is loaded separately from other `src/` files.
* This global observable lets dependent plugins (like the content flag plugin)
* subscribe to changes during dev server updates.
*/
export declare const globalContentConfigObserver: ContentObservable;
export declare function hasAnyContentFlag(viteId: string): boolean;
export declare function hasContentFlag(viteId: string, flag: (typeof CONTENT_FLAGS)[number]): boolean;
export declare function isDeferredModule(viteId: string): boolean;
export declare function reloadContentConfigObserver({ observer, ...loadContentConfigOpts }: {
@@ -467,8 +506,8 @@ type Observable<C> = {
subscribe: (fn: (ctx: C) => void) => () => void;
};
export type ContentObservable = Observable<ContentCtx>;
export declare function contentObservable(initialCtx: ContentCtx): ContentObservable;
export type ContentPaths = {
root: URL;
contentDir: URL;
assetsDir: URL;
typesTemplate: URL;
@@ -477,8 +516,12 @@ export type ContentPaths = {
exists: boolean;
url: URL;
};
liveConfig: {
exists: boolean;
url: URL;
};
};
export declare function getContentPaths({ srcDir }: Pick<AstroConfig, 'root' | 'srcDir'>, fs?: typeof fsMod): ContentPaths;
export declare function getContentPaths({ srcDir, legacy, root, experimental, }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy' | 'experimental'>, fs?: typeof fsMod): ContentPaths;
/**
* Check for slug in content entry frontmatter and validate the type,
* falling back to the `generatedSlug` if none is found.
@@ -493,13 +536,11 @@ export declare function getEntrySlug({ id, collection, generatedSlug, contentEnt
}): Promise<string>;
export declare function getExtGlob(exts: string[]): string;
export declare function hasAssetPropagationFlag(id: string): boolean;
/**
* Convert a platform path to a posix path.
*/
export declare function posixifyPath(filePath: string): string;
export declare function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): string[];
/**
* Unlike `path.posix.relative`, this function will accept a platform path and return a posix path.
*/
export declare function posixRelative(from: string, to: string): string;
export declare function contentModuleToId(fileName: string): string;
export declare function safeStringify(value: unknown): string;
export {};

View File

@@ -1,22 +1,42 @@
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 matter from "gray-matter";
import { green } from "kleur/colors";
import xxhash from "xxhash-wasm";
import { z } from "zod";
import { AstroError, AstroErrorData, MarkdownError, errorMap } from "../core/errors/index.js";
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_FLAGS,
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"),
@@ -30,22 +50,7 @@ const collectionConfigParser = z.union([
type: z.literal(CONTENT_LAYER_TYPE),
schema: z.any().optional(),
loader: z.union([
z.function().returns(
z.union([
z.array(
z.object({
id: z.string()
}).catchall(z.unknown())
),
z.promise(
z.array(
z.object({
id: z.string()
}).catchall(z.unknown())
)
)
])
),
z.function(),
z.object({
name: z.string(),
load: z.function(
@@ -59,8 +64,10 @@ const collectionConfigParser = z.union([
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()
watcher: z.any().optional(),
refreshContextData: z.record(z.unknown()).optional()
})
],
z.unknown()
@@ -69,7 +76,14 @@ const collectionConfigParser = z.union([
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({
@@ -90,20 +104,25 @@ function parseEntrySlug({
});
}
}
async function getEntryDataAndImages(entry, collectionConfig, shouldEmitFile, pluginContext) {
async function getEntryDataAndImages(entry, collectionConfig, shouldEmitFile, experimentalSvgEnabled, pluginContext) {
let data;
if (collectionConfig.type === "data" || collectionConfig.type === CONTENT_LAYER_TYPE) {
data = entry.unvalidatedData;
} else {
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)
image: createImage(
pluginContext,
shouldEmitFile,
entry._internal.filePath,
experimentalSvgEnabled
)
});
} else if (collectionConfig.type === CONTENT_LAYER_TYPE) {
schema = schema({
@@ -134,16 +153,16 @@ async function getEntryDataAndImages(entry, collectionConfig, shouldEmitFile, pl
data = parsed.data;
} else {
if (!formattedError) {
const errorType = collectionConfig.type === "content" ? AstroErrorData.InvalidContentEntryFrontmatterError : AstroErrorData.InvalidContentEntryDataError;
formattedError = new AstroError({
...AstroErrorData.InvalidContentEntryFrontmatterError,
message: AstroErrorData.InvalidContentEntryFrontmatterError.message(
entry.collection,
entry.id,
parsed.error
),
...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])),
file: entry._internal?.filePath,
line: getYAMLErrorLine(
entry._internal?.rawData,
String(parsed.error.errors[0].path[0])
),
column: 0
}
});
@@ -153,11 +172,12 @@ async function getEntryDataAndImages(entry, collectionConfig, shouldEmitFile, pl
}
return { data, imageImports: Array.from(imageImports) };
}
async function getEntryData(entry, collectionConfig, shouldEmitFile, pluginContext) {
async function getEntryData(entry, collectionConfig, shouldEmitFile, experimentalSvgEnabled, pluginContext) {
const { data } = await getEntryDataAndImages(
entry,
collectionConfig,
shouldEmitFile,
experimentalSvgEnabled,
pluginContext
);
return data;
@@ -269,17 +289,24 @@ function getRelativeEntryPath(entry, collection, contentDir) {
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);
if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir)) {
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 if (fileUrl.href === paths.config.url.href) {
return "config";
} else {
return "ignored";
}
@@ -306,7 +333,7 @@ function getYAMLErrorLine(rawData, objectKey) {
}
function safeParseFrontmatter(source, id) {
try {
return matter(source);
return parseFrontmatter(source, { frontmatter: "empty-with-spaces" });
} catch (err) {
const markdownError = new MarkdownError({
name: "MarkdownError",
@@ -328,14 +355,6 @@ function safeParseFrontmatter(source, id) {
}
}
const globalContentConfigObserver = contentObservable({ status: "init" });
function hasAnyContentFlag(viteId) {
const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
const flag = Array.from(flags.keys()).at(0);
if (typeof flag !== "string") {
return false;
}
return CONTENT_FLAGS.includes(flag);
}
function hasContentFlag(viteId, flag) {
const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
return flags.has(flag);
@@ -365,13 +384,90 @@ async function loadContentConfig({
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 {
const config = await loadContentConfig(loadContentConfigOpts);
let config = await loadContentConfig(loadContentConfigOpts);
config = await autogenerateCollections({
config,
...loadContentConfigOpts
});
if (config) {
observer.set({ status: "loaded", config });
} else {
@@ -406,27 +502,47 @@ function contentObservable(initialCtx) {
subscribe
};
}
function getContentPaths({ srcDir }, fs = fsMod) {
const configStats = search(fs, srcDir);
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
config: configStats,
liveConfig: liveConfigStats
};
}
function search(fs, srcDir) {
const paths = ["config.mjs", "config.js", "config.mts", "config.ts"].map(
(p) => new URL(`./content/${p}`, srcDir)
);
for (const file of paths) {
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: paths[0] };
return { exists: false, url: urls[0] };
}
async function getEntrySlug({
id,
@@ -461,6 +577,15 @@ function hasAssetPropagationFlag(id) {
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("/");
}
@@ -473,9 +598,29 @@ function contentModuleToId(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,
contentObservable,
getContentEntryExts,
getContentEntryIdAndSlug,
getContentPaths,
@@ -489,15 +634,16 @@ export {
getEntryType,
getExtGlob,
getSymlinkedContentCollections,
globWithUnderscoresIgnored,
globalContentConfigObserver,
hasAnyContentFlag,
hasAssetPropagationFlag,
hasContentFlag,
isDeferredModule,
loaderReturnSchema,
parseEntrySlug,
posixRelative,
posixifyPath,
reloadContentConfigObserver,
reverseSymlink,
safeParseFrontmatter
safeParseFrontmatter,
safeStringify
};

View File

@@ -1,10 +1,9 @@
import type { Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import type { BuildInternals } from '../core/build/internal.js';
import type { AstroBuildPlugin } from '../core/build/plugin.js';
import type { StaticBuildOptions } from '../core/build/types.js';
export declare function astroContentAssetPropagationPlugin({ mode, settings, }: {
mode: string;
import type { AstroSettings } from '../types/astro.js';
export declare function astroContentAssetPropagationPlugin({ settings, }: {
settings: AstroSettings;
}): Plugin;
export declare function astroConfigBuildPlugin(options: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin;

View File

@@ -5,18 +5,15 @@ import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { createViteLoader } from "../core/module-loader/vite.js";
import { joinPaths, prependForwardSlash } from "../core/path.js";
import { getStylesForURL } from "../vite-plugin-astro-server/css.js";
import { getScriptsForURL } from "../vite-plugin-astro-server/scripts.js";
import {
CONTENT_IMAGE_FLAG,
CONTENT_RENDER_FLAG,
LINKS_PLACEHOLDER,
PROPAGATED_ASSET_FLAG,
SCRIPTS_PLACEHOLDER,
STYLES_PLACEHOLDER
} from "./consts.js";
import { hasContentFlag } from "./utils.js";
function astroContentAssetPropagationPlugin({
mode,
settings
}) {
let devModuleLoader;
@@ -29,7 +26,7 @@ function astroContentAssetPropagationPlugin({
const params = new URLSearchParams(query);
const importerParam = params.get("importer");
const importerPath = importerParam ? fileURLToPath(new URL(importerParam, settings.config.root)) : importer;
const resolved = this.resolve(base, importerPath, { skipSelf: true, ...opts });
const resolved = await this.resolve(base, importerPath, { skipSelf: true, ...opts });
if (!resolved) {
throw new AstroError({
...AstroErrorData.ImageNotFound,
@@ -52,14 +49,12 @@ function astroContentAssetPropagationPlugin({
}
},
configureServer(server) {
if (mode === "dev") {
devModuleLoader = createViteLoader(server);
}
devModuleLoader = createViteLoader(server);
},
async transform(_, id, options) {
if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) {
const basePath = id.split("?")[0];
let stringifiedLinks, stringifiedStyles, stringifiedScripts;
let stringifiedLinks, stringifiedStyles;
if (options?.ssr && devModuleLoader) {
if (!devModuleLoader.getModuleById(basePath)?.ssrModule) {
await devModuleLoader.import(basePath);
@@ -69,28 +64,16 @@ function astroContentAssetPropagationPlugin({
urls,
crawledFiles: styleCrawledFiles
} = await getStylesForURL(pathToFileURL(basePath), devModuleLoader);
const { scripts: hoistedScripts, crawledFiles: scriptCrawledFiles } = settings.config.experimental.directRenderScript ? { scripts: /* @__PURE__ */ new Set(), crawledFiles: /* @__PURE__ */ new Set() } : await getScriptsForURL(
pathToFileURL(basePath),
settings.config.root,
devModuleLoader
);
for (const file of styleCrawledFiles) {
if (!file.includes("node_modules")) {
this.addWatchFile(file);
}
}
for (const file of scriptCrawledFiles) {
if (!file.includes("node_modules")) {
this.addWatchFile(file);
}
}
stringifiedLinks = JSON.stringify([...urls]);
stringifiedStyles = JSON.stringify(styles.map((s) => s.content));
stringifiedScripts = JSON.stringify([...hoistedScripts]);
} else {
stringifiedLinks = JSON.stringify(LINKS_PLACEHOLDER);
stringifiedStyles = JSON.stringify(STYLES_PLACEHOLDER);
stringifiedScripts = JSON.stringify(SCRIPTS_PLACEHOLDER);
}
const code = `
async function getMod() {
@@ -98,8 +81,7 @@ function astroContentAssetPropagationPlugin({
}
const collectedLinks = ${stringifiedLinks};
const collectedStyles = ${stringifiedStyles};
const collectedScripts = ${stringifiedScripts};
const defaultMod = { __astroPropagation: true, getMod, collectedLinks, collectedStyles, collectedScripts };
const defaultMod = { __astroPropagation: true, getMod, collectedLinks, collectedStyles, collectedScripts: [] };
export default defaultMod;
`;
return { code, map: { mappings: "" } };
@@ -111,7 +93,7 @@ function astroConfigBuildPlugin(options, internals) {
return {
targets: ["server"],
hooks: {
"build:post": ({ ssrOutputs, clientOutputs, mutate }) => {
"build:post": ({ ssrOutputs, mutate }) => {
const outputs = ssrOutputs.flatMap((o) => o.output);
const prependBase = (src) => {
const { assetsPrefix } = options.settings.config.build;
@@ -124,24 +106,17 @@ function astroConfigBuildPlugin(options, internals) {
}
};
for (const chunk of outputs) {
if (chunk.type === "chunk" && (chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER))) {
if (chunk.type === "chunk" && chunk.code.includes(LINKS_PLACEHOLDER)) {
const entryStyles = /* @__PURE__ */ new Set();
const entryLinks = /* @__PURE__ */ new Set();
const entryScripts = /* @__PURE__ */ new Set();
for (const id of chunk.moduleIds) {
const _entryCss = internals.propagatedStylesMap.get(id);
const _entryScripts = internals.propagatedScriptsMap.get(id);
if (_entryCss) {
for (const value of _entryCss) {
if (value.type === "inline") entryStyles.add(value.content);
if (value.type === "external") entryLinks.add(value.src);
}
}
if (_entryScripts) {
for (const value of _entryScripts) {
entryScripts.add(value);
}
}
}
let newCode = chunk.code;
if (entryStyles.size) {
@@ -160,33 +135,6 @@ function astroConfigBuildPlugin(options, internals) {
} else {
newCode = newCode.replace(JSON.stringify(LINKS_PLACEHOLDER), "[]");
}
if (entryScripts.size) {
const entryFileNames = /* @__PURE__ */ new Set();
for (const output of clientOutputs) {
for (const clientChunk of output.output) {
if (clientChunk.type !== "chunk") continue;
for (const [id] of Object.entries(clientChunk.modules)) {
if (entryScripts.has(id)) {
entryFileNames.add(clientChunk.fileName);
}
}
}
}
newCode = newCode.replace(
JSON.stringify(SCRIPTS_PLACEHOLDER),
JSON.stringify(
[...entryFileNames].map((src) => ({
props: {
src: prependBase(src),
type: "module"
},
children: ""
}))
)
);
} else {
newCode = newCode.replace(JSON.stringify(SCRIPTS_PLACEHOLDER), "[]");
}
mutate(chunk, ["server"], newCode);
}
}

View File

@@ -1,7 +1,7 @@
import type fsMod from 'node:fs';
import type { Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import type { AstroSettings } from '../types/astro.js';
export declare function astroContentImportPlugin({ fs, settings, logger, }: {
fs: typeof fsMod;
settings: AstroSettings;

View File

@@ -4,7 +4,6 @@ import * as devalue from "devalue";
import { getProxyCode } from "../assets/utils/proxy.js";
import { AstroError } from "../core/errors/errors.js";
import { AstroErrorData } from "../core/errors/index.js";
import { isServerLikeOutput } from "../core/util.js";
import { CONTENT_FLAG, DATA_FLAG } from "./consts.js";
import {
getContentEntryExts,
@@ -76,7 +75,7 @@ function astroContentImportPlugin({
const code = `
export const id = ${JSON.stringify(id)};
export const collection = ${JSON.stringify(collection)};
export const data = ${stringifyEntryData(data, isServerLikeOutput(settings.config))};
export const data = ${stringifyEntryData(data, settings.buildOutput === "server")};
export const _internal = {
type: 'data',
filePath: ${JSON.stringify(_internal.filePath)},
@@ -100,7 +99,7 @@ export const _internal = {
export const collection = ${JSON.stringify(collection)};
export const slug = ${JSON.stringify(slug)};
export const body = ${JSON.stringify(body)};
export const data = ${stringifyEntryData(data, isServerLikeOutput(settings.config))};
export const data = ${stringifyEntryData(data, settings.buildOutput === "server")};
export const _internal = {
type: 'content',
filePath: ${JSON.stringify(_internal.filePath)},
@@ -172,6 +171,8 @@ async function getContentEntryModule(params) {
{ id, collection, _internal, unvalidatedData },
collectionConfig,
params.shouldEmitFile,
// FUTURE: Remove in this in v6
id.endsWith(".svg"),
pluginContext
) : unvalidatedData;
const contentEntryModule = {
@@ -197,6 +198,8 @@ async function getDataEntryModule(params) {
{ id, collection, _internal, unvalidatedData },
collectionConfig,
params.shouldEmitFile,
// FUTURE: Remove in this in v6
id.endsWith(".svg"),
pluginContext
) : unvalidatedData;
const dataEntryModule = {

View File

@@ -1,27 +1,9 @@
import nodeFs from 'node:fs';
import type { Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { type ContentLookupMap } from './utils.js';
import { type Plugin } from 'vite';
import type { AstroSettings } from '../types/astro.js';
interface AstroContentVirtualModPluginParams {
settings: AstroSettings;
fs: typeof nodeFs;
}
export declare function astroContentVirtualModPlugin({ settings, fs, }: AstroContentVirtualModPluginParams): Plugin;
export declare function generateContentEntryFile({ settings, lookupMap, IS_DEV, IS_SERVER, isClient, }: {
settings: AstroSettings;
fs: typeof nodeFs;
lookupMap: ContentLookupMap;
IS_DEV: boolean;
IS_SERVER: boolean;
isClient: boolean;
}): Promise<string>;
/**
* Generate a map from a collection + slug to the local file path.
* This is used internally to resolve entry imports when using `getEntry()`.
* @see `templates/content/module.mjs`
*/
export declare function generateLookupMap({ settings, fs, }: {
settings: AstroSettings;
fs: typeof nodeFs;
}): Promise<ContentLookupMap>;
export {};

View File

@@ -2,12 +2,10 @@ import nodeFs from "node:fs";
import { extname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dataToEsm } from "@rollup/pluginutils";
import glob from "fast-glob";
import pLimit from "p-limit";
import { encodeName } from "../core/build/util.js";
import { glob } from "tinyglobby";
import { normalizePath } from "vite";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { appendForwardSlash, removeFileExtension } from "../core/path.js";
import { isServerLikeOutput } from "../core/util.js";
import { rootRelativePath } from "../core/viteUtils.js";
import { createDefaultAstroMetadata } from "../vite-plugin-astro/metadata.js";
import {
@@ -36,32 +34,50 @@ import {
getEntrySlug,
getEntryType,
getExtGlob,
globWithUnderscoresIgnored,
isDeferredModule
} from "./utils.js";
function invalidateDataStore(server) {
const module = server.moduleGraph.getModuleById(RESOLVED_DATA_STORE_VIRTUAL_ID);
if (module) {
server.moduleGraph.invalidateModule(module);
}
server.ws.send({
type: "full-reload",
path: "*"
});
}
function astroContentVirtualModPlugin({
settings,
fs
}) {
let IS_DEV = false;
const IS_SERVER = isServerLikeOutput(settings.config);
let dataStoreFile;
let devServer;
let liveConfig;
return {
name: "astro-content-virtual-mod-plugin",
enforce: "pre",
configResolved(config) {
IS_DEV = config.mode === "development";
dataStoreFile = getDataStoreFile(settings, IS_DEV);
config(_, env) {
dataStoreFile = getDataStoreFile(settings, env.command === "serve");
const contentPaths = getContentPaths(settings.config);
if (contentPaths.liveConfig.exists) {
liveConfig = normalizePath(fileURLToPath(contentPaths.liveConfig.url));
}
},
async resolveId(id) {
buildStart() {
if (devServer) {
devServer.watcher.add(fileURLToPath(dataStoreFile));
invalidateDataStore(devServer);
}
},
async resolveId(id, importer) {
if (id === VIRTUAL_MODULE_ID) {
if (!settings.config.experimental.contentCollectionCache) {
return RESOLVED_VIRTUAL_MODULE_ID;
}
if (IS_DEV || IS_SERVER) {
return RESOLVED_VIRTUAL_MODULE_ID;
} else {
return { id: RESOLVED_VIRTUAL_MODULE_ID, external: true };
if (liveConfig && importer && liveConfig === normalizePath(importer)) {
return this.resolve("astro/virtual-modules/live-config", importer, {
skipSelf: true
});
}
return RESOLVED_VIRTUAL_MODULE_ID;
}
if (id === DATA_STORE_VIRTUAL_ID) {
return RESOLVED_DATA_STORE_VIRTUAL_ID;
@@ -95,17 +111,15 @@ function astroContentVirtualModPlugin({
},
async load(id, args) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
const lookupMap = await generateLookupMap({
const lookupMap = settings.config.legacy.collections ? await generateLookupMap({
settings,
fs
});
}) : {};
const isClient = !args?.ssr;
const code = await generateContentEntryFile({
settings,
fs,
lookupMap,
IS_DEV,
IS_SERVER,
isClient
});
const astro = createDefaultAstroMetadata();
@@ -119,7 +133,7 @@ function astroContentVirtualModPlugin({
}
if (id === RESOLVED_DATA_STORE_VIRTUAL_ID) {
if (!fs.existsSync(dataStoreFile)) {
return "export default new Map()";
return { code: "export default new Map()" };
}
const jsonData = await fs.promises.readFile(dataStoreFile, "utf-8");
try {
@@ -137,50 +151,28 @@ function astroContentVirtualModPlugin({
}
if (id === ASSET_IMPORTS_RESOLVED_STUB_ID) {
const assetImportsFile = new URL(ASSET_IMPORTS_FILE, settings.dotAstroDir);
if (!fs.existsSync(assetImportsFile)) {
return "export default new Map()";
}
return fs.readFileSync(assetImportsFile, "utf-8");
return {
code: fs.existsSync(assetImportsFile) ? fs.readFileSync(assetImportsFile, "utf-8") : "export default new Map()"
};
}
if (id === MODULES_MJS_VIRTUAL_ID) {
const modules = new URL(MODULES_IMPORTS_FILE, settings.dotAstroDir);
if (!fs.existsSync(modules)) {
return "export default new Map()";
}
return fs.readFileSync(modules, "utf-8");
}
},
renderChunk(code, chunk) {
if (!settings.config.experimental.contentCollectionCache) {
return;
}
if (code.includes(RESOLVED_VIRTUAL_MODULE_ID)) {
const depth = chunk.fileName.split("/").length - 1;
const prefix = depth > 0 ? "../".repeat(depth) : "./";
return code.replaceAll(RESOLVED_VIRTUAL_MODULE_ID, `${prefix}content/entry.mjs`);
return {
code: fs.existsSync(modules) ? fs.readFileSync(modules, "utf-8") : "export default new Map()"
};
}
},
configureServer(server) {
devServer = server;
const dataStorePath = fileURLToPath(dataStoreFile);
server.watcher.add(dataStorePath);
function invalidateDataStore() {
const module = server.moduleGraph.getModuleById(RESOLVED_DATA_STORE_VIRTUAL_ID);
if (module) {
server.moduleGraph.invalidateModule(module);
}
server.ws.send({
type: "full-reload",
path: "*"
});
}
server.watcher.on("add", (addedPath) => {
if (addedPath === dataStorePath) {
invalidateDataStore();
invalidateDataStore(server);
}
});
server.watcher.on("change", (changedPath) => {
if (changedPath === dataStorePath) {
invalidateDataStore();
invalidateDataStore(server);
}
});
}
@@ -189,16 +181,14 @@ function astroContentVirtualModPlugin({
async function generateContentEntryFile({
settings,
lookupMap,
IS_DEV,
IS_SERVER,
isClient
}) {
const contentPaths = getContentPaths(settings.config);
const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);
let contentEntryGlobResult;
let dataEntryGlobResult;
let renderEntryGlobResult;
if (IS_DEV || IS_SERVER || !settings.config.experimental.contentCollectionCache) {
let contentEntryGlobResult = '""';
let dataEntryGlobResult = '""';
let renderEntryGlobResult = '""';
if (settings.config.legacy.collections) {
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
const contentEntryExts = [...contentEntryConfigByExt.keys()];
const dataEntryExts = getDataEntryExts(settings);
@@ -215,47 +205,25 @@ async function generateContentEntryFile({
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
CONTENT_RENDER_FLAG
);
} else {
contentEntryGlobResult = getStringifiedCollectionFromLookup(
"content",
relContentDir,
lookupMap
);
dataEntryGlobResult = getStringifiedCollectionFromLookup("data", relContentDir, lookupMap);
renderEntryGlobResult = getStringifiedCollectionFromLookup("render", relContentDir, lookupMap);
}
let virtualModContents = nodeFs.readFileSync(contentPaths.virtualModTemplate, "utf-8").replace("@@CONTENT_DIR@@", relContentDir).replace("'@@CONTENT_ENTRY_GLOB_PATH@@'", contentEntryGlobResult).replace("'@@DATA_ENTRY_GLOB_PATH@@'", dataEntryGlobResult).replace("'@@RENDER_ENTRY_GLOB_PATH@@'", renderEntryGlobResult).replace("/* @@LOOKUP_MAP_ASSIGNMENT@@ */", `lookupMap = ${JSON.stringify(lookupMap)};`) + (isClient ? `
console.warn('astro:content is only supported running server-side. Using it in the browser will lead to bloated bundles and slow down page load. In the future it will not be supported.');` : "");
let virtualModContents;
if (isClient) {
throw new AstroError({
...AstroErrorData.ServerOnlyModule,
message: AstroErrorData.ServerOnlyModule.message("astro:content")
});
} else {
virtualModContents = nodeFs.readFileSync(contentPaths.virtualModTemplate, "utf-8").replace("@@CONTENT_DIR@@", relContentDir).replace("'@@CONTENT_ENTRY_GLOB_PATH@@'", contentEntryGlobResult).replace("'@@DATA_ENTRY_GLOB_PATH@@'", dataEntryGlobResult).replace("'@@RENDER_ENTRY_GLOB_PATH@@'", renderEntryGlobResult).replace("/* @@LOOKUP_MAP_ASSIGNMENT@@ */", `lookupMap = ${JSON.stringify(lookupMap)};`).replace(
"/* @@LIVE_CONTENT_CONFIG@@ */",
contentPaths.liveConfig.exists ? (
// Dynamic import so it extracts the chunk and avoids a circular import
`const liveCollections = (await import(${JSON.stringify(fileURLToPath(contentPaths.liveConfig.url))})).collections;`
) : "const liveCollections = {};"
);
}
return virtualModContents;
}
function getStringifiedCollectionFromLookup(wantedType, relContentDir, lookupMap) {
let str = "{";
let normalize = (slug) => slug;
if (process.env.NODE_ENV === "production") {
const suffix = wantedType === "render" ? ".entry.mjs" : ".mjs";
normalize = (slug) => `${removeFileExtension(encodeName(slug)).replace(relContentDir, "./")}${suffix}`;
} else {
let suffix = "";
if (wantedType === "content") suffix = CONTENT_FLAG;
else if (wantedType === "data") suffix = DATA_FLAG;
else if (wantedType === "render") suffix = CONTENT_RENDER_FLAG;
normalize = (slug) => `${slug}?${suffix}`;
}
for (const { type, entries } of Object.values(lookupMap)) {
if (type === wantedType || wantedType === "render" && type === "content") {
for (const slug of Object.values(entries)) {
str += `
"${slug}": () => import("${normalize(slug)}"),`;
}
}
}
str += "\n}";
return str;
}
async function generateLookupMap({
settings,
fs
}) {
async function generateLookupMap({ settings, fs }) {
const { root } = settings.config;
const contentPaths = getContentPaths(settings.config);
const relContentDir = rootRelativePath(root, contentPaths.contentDir, false);
@@ -269,7 +237,7 @@ async function generateLookupMap({
{
absolute: true,
cwd: fileURLToPath(root),
fs
expandDirectories: false
}
);
const limit = pLimit(10);
@@ -290,7 +258,7 @@ async function generateLookupMap({
if (entryType === "content") {
const contentEntryType = contentEntryConfigByExt.get(extname(filePath));
if (!contentEntryType) throw UnexpectedLookupMapError;
const { id, slug: generatedSlug } = await getContentEntryIdAndSlug({
const { id, slug: generatedSlug } = getContentEntryIdAndSlug({
entry: pathToFileURL(filePath),
contentDir,
collection
@@ -338,21 +306,10 @@ async function generateLookupMap({
await Promise.all(promises);
return lookupMap;
}
function globWithUnderscoresIgnored(relContentDir, exts) {
const extGlob = getExtGlob(exts);
const contentDir = appendForwardSlash(relContentDir);
return [
`${contentDir}**/*${extGlob}`,
`!${contentDir}**/_*/**/*${extGlob}`,
`!${contentDir}**/_*${extGlob}`
];
}
const UnexpectedLookupMapError = new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `Unexpected error while parsing content entry IDs and slugs.`
});
export {
astroContentVirtualModPlugin,
generateContentEntryFile,
generateLookupMap
astroContentVirtualModPlugin
};

5
node_modules/astro/dist/content/watcher.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
import type { FSWatcher } from 'vite';
export type WrappedWatcher = FSWatcher & {
removeAllTrackedListeners(): void;
};
export declare function createWatcherWrapper(watcher: FSWatcher): WrappedWatcher;

38
node_modules/astro/dist/content/watcher.js generated vendored Normal file
View File

@@ -0,0 +1,38 @@
function createWatcherWrapper(watcher) {
const listeners = /* @__PURE__ */ new Map();
const handler = {
get(target, prop, receiver) {
if (prop === "on") {
return function(event, callback) {
if (!listeners.has(event)) {
listeners.set(event, /* @__PURE__ */ new Set());
}
listeners.get(event).add(callback);
return Reflect.get(target, prop, receiver).call(target, event, callback);
};
}
if (prop === "off") {
return function(event, callback) {
listeners.get(event)?.delete(callback);
return Reflect.get(target, prop, receiver).call(target, event, callback);
};
}
if (prop === "removeAllTrackedListeners") {
return function() {
for (const [event, callbacks] of listeners.entries()) {
for (const callback of callbacks) {
target.off(event, callback);
}
callbacks.clear();
}
listeners.clear();
};
}
return Reflect.get(target, prop, receiver);
}
};
return new Proxy(watcher, handler);
}
export {
createWatcherWrapper
};