0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-31 23:31:30 -05:00

wip: allow integrations to refresh contel layer

This commit is contained in:
Matt Kane 2024-08-19 09:43:51 +01:00
parent 89bab1e707
commit b8b5ca2934
5 changed files with 72 additions and 25 deletions

View file

@ -3298,6 +3298,11 @@ export interface SSRLoadedRenderer extends Pick<AstroRenderer, 'name' | 'clientE
ssr: SSRLoadedRendererValue;
}
export interface RefreshContentOptions {
loaders?: Array<string>;
context?: Record<string, any>;
}
export type HookParameters<
Hook extends keyof AstroIntegration['hooks'],
Fn = AstroIntegration['hooks'][Hook],
@ -3341,6 +3346,7 @@ declare global {
server: vite.ViteDevServer;
logger: AstroIntegrationLogger;
toolbar: ReturnType<typeof getToolbarServerCommunicationHelpers>;
refreshContent?: (options: RefreshContentOptions) => Promise<void>;
}) => void | Promise<void>;
'astro:server:start': (options: {
address: AddressInfo;

View file

@ -3,7 +3,7 @@ import { isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { FSWatcher } from 'vite';
import xxhash from 'xxhash-wasm';
import type { AstroSettings } from '../@types/astro.js';
import type { AstroSettings, RefreshContentOptions } from '../@types/astro.js';
import { AstroUserError } from '../core/errors/errors.js';
import type { Logger } from '../core/logger/core.js';
import {
@ -44,12 +44,6 @@ export class ContentLayer {
this.#watcher = watcher;
}
/**
* Whether the content layer is currently loading content
*/
get loading() {
return this.#loading;
}
/**
* Watch for changes to the content config and trigger a sync when it changes.
@ -71,22 +65,6 @@ export class ContentLayer {
this.#unsubscribe?.();
}
/**
* Run 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
* perform an incremental update. After the data is loaded, the data store is written to disk.
*/
async sync() {
if (this.#loading) {
return;
}
this.#loading = true;
try {
await this.#doSync();
} finally {
this.#loading = false;
}
}
async #getGenerateDigest() {
if (this.#generateDigest) {
@ -108,10 +86,12 @@ export class ContentLayer {
collectionName,
loaderName = 'content',
parseData,
refreshContextData,
}: {
collectionName: string;
loaderName: string;
parseData: LoaderContext['parseData'];
refreshContextData?: Record<string, unknown>;
}): Promise<LoaderContext> {
return {
collection: collectionName,
@ -122,10 +102,17 @@ export class ContentLayer {
parseData,
generateDigest: await this.#getGenerateDigest(),
watcher: this.#watcher,
refreshContextData
};
}
async #doSync() {
/**
* Run 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
* perform an incremental update. After the data is loaded, the data store is written to disk.
*/
async sync(options?: RefreshContentOptions) {
const contentConfig = globalContentConfigObserver.get();
const logger = this.#logger.forkIntegrationLogger('content');
if (contentConfig?.status !== 'loaded') {
@ -171,6 +158,14 @@ export class ContentLayer {
}
}
// If loaders are specified, only sync the specified loaders
if (
options?.loaders &&
(typeof collection.loader !== 'object' || !options.loaders.includes(collection.loader.name))
) {
return;
}
const collectionWithResolvedSchema = { ...collection, schema };
const parseData: LoaderContext['parseData'] = async ({ id, data, filePath = '' }) => {
@ -204,6 +199,7 @@ export class ContentLayer {
collectionName: name,
parseData,
loaderName: collection.loader.name,
refreshContextData: options?.context
});
if (typeof collection.loader === 'function') {

View file

@ -31,6 +31,9 @@ export interface LoaderContext {
/** 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>;
}
export interface Loader {

View file

@ -12,6 +12,7 @@ import type {
ContentEntryType,
DataEntryType,
HookParameters,
RefreshContentOptions,
RouteData,
RouteOptions,
} from '../@types/astro.js';
@ -22,6 +23,7 @@ import { mergeConfig } from '../core/config/index.js';
import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js';
import { isServerLikeOutput } from '../core/util.js';
import { validateSupportedFeatures } from './features-validation.js';
import { globalContentLayer } from '../content/content-layer.js';
async function withTakingALongTimeMsg<T>({
name,
@ -370,6 +372,14 @@ export async function runHookServerSetup({
server: ViteDevServer;
logger: Logger;
}) {
let refreshContent: undefined | ((options: RefreshContentOptions) => Promise<void>);
if (config.experimental?.contentLayer) {
refreshContent = async (options: RefreshContentOptions) => {
const contentLayer = await globalContentLayer.get();
await contentLayer.sync(options);
};
}
for (const integration of config.integrations) {
if (integration?.hooks?.['astro:server:setup']) {
await withTakingALongTimeMsg({
@ -379,6 +389,7 @@ export async function runHookServerSetup({
server,
logger: getLogger(integration, logger),
toolbar: getToolbarServerCommunicationHelpers(server),
refreshContent,
}),
logger,
});

View file

@ -3,7 +3,38 @@ import { defineConfig } from 'astro/config';
import { fileURLToPath } from 'node:url';
export default defineConfig({
integrations: [mdx()],
integrations: [mdx(), {
name: '@astrojs/my-integration',
hooks: {
'astro:server:setup': async ({ server, refreshContent }) => {
server.middlewares.use('/_refresh', async (req, res) => {
if(req.method !== 'POST') {
res.statusCode = 405
res.end('Method Not Allowed');
return
}
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const webhookBody = JSON.parse(body);
await refreshContent({
context: { webhookBody },
loaders: ['increment-loader']
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Content refreshed successfully' }));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to refresh content' }));
}
});
});
}
}
}],
vite: {
resolve: {
alias: {