summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/metal-terms-push.md5
-rw-r--r--packages/astro/src/@types/astro.ts1
-rw-r--r--packages/astro/src/core/build/consts.ts1
-rw-r--r--packages/astro/src/core/build/index.ts1
-rw-r--r--packages/astro/src/core/build/internal.ts2
-rw-r--r--packages/astro/src/core/build/plugins/plugin-content.ts191
-rw-r--r--packages/astro/src/core/build/static-build.ts5
-rw-r--r--packages/astro/src/core/config/config.ts20
-rw-r--r--packages/astro/src/core/config/index.ts2
-rw-r--r--packages/astro/src/integrations/index.ts4
-rw-r--r--packages/astro/src/vite-plugin-astro/index.ts22
-rw-r--r--packages/astro/test/experimental-content-collections-cache-invalidation.test.js98
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/.gitignore1
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/astro.config.mjs12
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/cache/lockfile-mismatch/content/manifest.json1
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/cache/version-mismatch/content/manifest.json1
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/package.json8
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/blog/one.md5
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/config.ts10
-rw-r--r--packages/astro/test/fixtures/content-collections-cache-invalidation/src/pages/index.astro10
-rw-r--r--pnpm-lock.yaml6
21 files changed, 355 insertions, 51 deletions
diff --git a/.changeset/metal-terms-push.md b/.changeset/metal-terms-push.md
new file mode 100644
index 000000000..d9cbf378b
--- /dev/null
+++ b/.changeset/metal-terms-push.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Invalidate CC cache manifest when lockfile or config changes
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 21630ec1e..52bf7d397 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -2779,6 +2779,7 @@ export interface AstroIntegration {
dir: URL;
routes: RouteData[];
logger: AstroIntegrationLogger;
+ cacheManifest: boolean;
}) => void | Promise<void>;
};
}
diff --git a/packages/astro/src/core/build/consts.ts b/packages/astro/src/core/build/consts.ts
new file mode 100644
index 000000000..bf3162fc4
--- /dev/null
+++ b/packages/astro/src/core/build/consts.ts
@@ -0,0 +1 @@
+export const CHUNKS_PATH = 'chunks/';
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index d77e69fd2..2b71feaf9 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -218,6 +218,7 @@ class AstroBuilder {
.flat()
.map((pageData) => pageData.route),
logging: this.logger,
+ cacheManifest: internals.cacheManifestUsed,
});
if (this.logger.level && levels[this.logger.level()] <= levels['info']) {
diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts
index c2c53df11..a5e456627 100644
--- a/packages/astro/src/core/build/internal.ts
+++ b/packages/astro/src/core/build/internal.ts
@@ -89,6 +89,7 @@ export interface BuildInternals {
discoveredScripts: Set<string>;
cachedClientEntries: string[];
+ cacheManifestUsed: boolean;
propagatedStylesMap: Map<string, Set<StylesheetAsset>>;
propagatedScriptsMap: Map<string, Set<string>>;
@@ -140,6 +141,7 @@ export function createBuildInternals(): BuildInternals {
componentMetadata: new Map(),
ssrSplitEntryChunks: new Map(),
entryPoints: new Map(),
+ cacheManifestUsed: false,
};
}
diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts
index f9c9ba7e4..ce11359d8 100644
--- a/packages/astro/src/core/build/plugins/plugin-content.ts
+++ b/packages/astro/src/core/build/plugins/plugin-content.ts
@@ -3,6 +3,7 @@ import fsMod from 'node:fs';
import { fileURLToPath } from 'node:url';
import pLimit from 'p-limit';
import { type Plugin as VitePlugin, normalizePath } from 'vite';
+import { configPaths } from '../../config/index.js';
import { CONTENT_RENDER_FLAG, PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
import { type ContentLookupMap, hasContentFlag } from '../../../content/utils.js';
import {
@@ -10,7 +11,7 @@ import {
generateLookupMap,
} from '../../../content/vite-plugin-content-virtual-mod.js';
import { isServerLikeOutput } from '../../../prerender/utils.js';
-import { joinPaths, removeFileExtension, removeLeadingForwardSlash } from '../../path.js';
+import { joinPaths, removeFileExtension, removeLeadingForwardSlash, appendForwardSlash } from '../../path.js';
import { addRollupInput } from '../add-rollup-input.js';
import { type BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
@@ -18,12 +19,14 @@ import { copyFiles } from '../static-build.js';
import type { StaticBuildOptions } from '../types.js';
import { encodeName } from '../util.js';
import { extendManualChunks } from './util.js';
+import { emptyDir } from '../../fs/index.js';
+import { CHUNKS_PATH } from '../consts.js';
const CONTENT_CACHE_DIR = './content/';
const CONTENT_MANIFEST_FILE = './manifest.json';
// IMPORTANT: Update this version when making significant changes to the manifest format.
// Only manifests generated with the same version number can be compared.
-const CONTENT_MANIFEST_VERSION = 0;
+const CONTENT_MANIFEST_VERSION = 1;
interface ContentManifestKey {
collection: string;
@@ -39,40 +42,44 @@ interface ContentManifest {
// Tracks components that should be passed to the client build
// When the cache is restored, these might no longer be referenced
clientEntries: string[];
+ // Hash of the lockfiles, pnpm-lock.yaml, package-lock.json, etc.
+ // Kept so that installing new packages results in a full rebuild.
+ lockfiles: string;
+ // Hash of the Astro config. Changing options results in invalidating the cache.
+ configs: string;
}
const virtualEmptyModuleId = `virtual:empty-content`;
const resolvedVirtualEmptyModuleId = `\0${virtualEmptyModuleId}`;
+const NO_MANIFEST_VERSION = -1 as const;
function createContentManifest(): ContentManifest {
- return { version: -1, entries: [], serverEntries: [], clientEntries: [] };
+ return { version: NO_MANIFEST_VERSION, entries: [], serverEntries: [], clientEntries: [], lockfiles: "", configs: "" };
}
function vitePluginContent(
opts: StaticBuildOptions,
lookupMap: ContentLookupMap,
- internals: BuildInternals
+ internals: BuildInternals,
+ cachedBuildOutput: Array<{ cached: URL; dist: URL; }>
): VitePlugin {
const { config } = opts.settings;
const { cacheDir } = config;
const distRoot = config.outDir;
const distContentRoot = new URL('./content/', distRoot);
- const cachedChunks = new URL('./chunks/', opts.settings.config.cacheDir);
- const distChunks = new URL('./chunks/', opts.settings.config.outDir);
const contentCacheDir = new URL(CONTENT_CACHE_DIR, cacheDir);
const contentManifestFile = new URL(CONTENT_MANIFEST_FILE, contentCacheDir);
- const cache = contentCacheDir;
- const cacheTmp = new URL('./.tmp/', cache);
+ const cacheTmp = new URL('./.tmp/', contentCacheDir);
let oldManifest = createContentManifest();
let newManifest = createContentManifest();
let entries: ContentEntries;
let injectedEmptyFile = false;
+ let currentManifestState: ReturnType<typeof manifestState> = 'valid';
if (fsMod.existsSync(contentManifestFile)) {
try {
const data = fsMod.readFileSync(contentManifestFile, { encoding: 'utf8' });
oldManifest = JSON.parse(data);
- internals.cachedClientEntries = oldManifest.clientEntries;
} catch {}
}
@@ -84,6 +91,32 @@ function vitePluginContent(
newManifest = await generateContentManifest(opts, lookupMap);
entries = getEntriesFromManifests(oldManifest, newManifest);
+ // If the manifest is valid, use the cached client entries as nothing has changed
+ currentManifestState = manifestState(oldManifest, newManifest);
+ if(currentManifestState === 'valid') {
+ internals.cachedClientEntries = oldManifest.clientEntries;
+ } else {
+ let logReason = '';
+ switch(currentManifestState) {
+ case 'config-mismatch':
+ logReason = 'Astro config has changed';
+ break;
+ case 'lockfile-mismatch':
+ logReason = 'Lockfiles have changed';
+ break;
+ case 'no-entries':
+ logReason = 'No content collections entries cached';
+ break;
+ case 'version-mismatch':
+ logReason = 'The cache manifest version has changed';
+ break;
+ case 'no-manifest':
+ logReason = 'No content manifest was found in the cache';
+ break;
+ }
+ opts.logger.info('build', `Cache invalid, rebuilding from source. Reason: ${logReason}.`);
+ }
+
// Of the cached entries, these ones need to be rebuilt
for (const { type, entry } of entries.buildFromSource) {
const fileURL = encodeURI(joinPaths(opts.settings.config.root.toString(), entry));
@@ -96,10 +129,18 @@ function vitePluginContent(
}
newOptions = addRollupInput(newOptions, inputs);
}
- // Restores cached chunks from the previous build
- if (fsMod.existsSync(cachedChunks)) {
- await copyFiles(cachedChunks, distChunks, true);
+
+ // Restores cached chunks and assets from the previous build
+ // If the manifest state is not valid then it needs to rebuild everything
+ // so don't do that in this case.
+ if(currentManifestState === 'valid') {
+ for(const { cached, dist } of cachedBuildOutput) {
+ if (fsMod.existsSync(cached)) {
+ await copyFiles(cached, dist, true);
+ }
+ }
}
+
// If nothing needs to be rebuilt, we inject a fake entrypoint to appease Rollup
if (entries.buildFromSource.length === 0) {
newOptions = addRollupInput(newOptions, [virtualEmptyModuleId]);
@@ -199,16 +240,20 @@ function vitePluginContent(
]);
newManifest.serverEntries = Array.from(serverComponents);
newManifest.clientEntries = Array.from(clientComponents);
+
+ const cacheExists = fsMod.existsSync(contentCacheDir);
+ // If the manifest is invalid, empty the cache so that we can create a new one.
+ if(cacheExists && currentManifestState !== 'valid') {
+ emptyDir(contentCacheDir);
+ }
+
await fsMod.promises.mkdir(contentCacheDir, { recursive: true });
await fsMod.promises.writeFile(contentManifestFile, JSON.stringify(newManifest), {
encoding: 'utf8',
});
-
- const cacheExists = fsMod.existsSync(cache);
- fsMod.mkdirSync(cache, { recursive: true });
await fsMod.promises.mkdir(cacheTmp, { recursive: true });
await copyFiles(distContentRoot, cacheTmp, true);
- if (cacheExists) {
+ if (cacheExists && currentManifestState === 'valid') {
await copyFiles(contentCacheDir, distContentRoot, false);
}
await copyFiles(cacheTmp, contentCacheDir);
@@ -242,12 +287,12 @@ function getEntriesFromManifests(
oldManifest: ContentManifest,
newManifest: ContentManifest
): ContentEntries {
- const { version: oldVersion, entries: oldEntries } = oldManifest;
- const { version: newVersion, entries: newEntries } = newManifest;
+ const { entries: oldEntries } = oldManifest;
+ const { entries: newEntries } = newManifest;
let entries: ContentEntries = { restoreFromCache: [], buildFromSource: [] };
const newEntryMap = new Map<ContentManifestKey, string>(newEntries);
- if (oldVersion !== newVersion || oldEntries.length === 0) {
+ if (manifestState(oldManifest, newManifest) !== 'valid') {
entries.buildFromSource = Array.from(newEntryMap.keys());
return entries;
}
@@ -265,16 +310,37 @@ function getEntriesFromManifests(
return entries;
}
+type ManifestState = 'valid' | 'no-manifest' | 'version-mismatch' | 'no-entries' | 'lockfile-mismatch' | 'config-mismatch';
+
+function manifestState(oldManifest: ContentManifest, newManifest: ContentManifest): ManifestState {
+ // There isn't an existing manifest.
+ if(oldManifest.version === NO_MANIFEST_VERSION) {
+ return 'no-manifest';
+ }
+ // Version mismatch, always invalid
+ if (oldManifest.version !== newManifest.version) {
+ return 'version-mismatch';
+ }
+ if(oldManifest.entries.length === 0) {
+ return 'no-entries';
+ }
+ // Lockfiles have changed or there is no lockfile at all.
+ if((oldManifest.lockfiles !== newManifest.lockfiles) || newManifest.lockfiles === '') {
+ return 'lockfile-mismatch';
+ }
+ // Config has changed.
+ if(oldManifest.configs !== newManifest.configs) {
+ return 'config-mismatch';
+ }
+ return 'valid';
+}
+
async function generateContentManifest(
opts: StaticBuildOptions,
lookupMap: ContentLookupMap
): Promise<ContentManifest> {
- let manifest: ContentManifest = {
- version: CONTENT_MANIFEST_VERSION,
- entries: [],
- serverEntries: [],
- clientEntries: [],
- };
+ let manifest = createContentManifest();
+ manifest.version = CONTENT_MANIFEST_VERSION;
const limit = pLimit(10);
const promises: Promise<void>[] = [];
@@ -290,13 +356,63 @@ async function generateContentManifest(
);
}
}
+
+ const [lockfiles, configs] = await Promise.all([
+ lockfilesHash(opts.settings.config.root),
+ configHash(opts.settings.config.root)
+ ]);
+
+ manifest.lockfiles = lockfiles;
+ manifest.configs = configs;
await Promise.all(promises);
return manifest;
}
-function checksum(data: string): string {
- return createHash('sha1').update(data).digest('base64');
+async function pushBufferInto(fileURL: URL, buffers: Uint8Array[]) {
+ try {
+ const handle = await fsMod.promises.open(fileURL, 'r');
+ const data = await handle.readFile();
+ buffers.push(data);
+ await handle.close();
+ } catch {
+ // File doesn't exist, ignore
+ }
+}
+
+async function lockfilesHash(root: URL) {
+ // Order is important so don't change this.
+ const lockfiles = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb'];
+ const datas: Uint8Array[] = [];
+ const promises: Promise<void>[] = [];
+ for(const lockfileName of lockfiles) {
+ const fileURL = new URL(`./${lockfileName}`, root);
+ promises.push(pushBufferInto(fileURL, datas));
+ }
+ await Promise.all(promises);
+ return checksum(...datas);
+}
+
+async function configHash(root: URL) {
+ const configFileNames = configPaths;
+ for(const configPath of configFileNames) {
+ try {
+ const fileURL = new URL(`./${configPath}`, root);
+ const data = await fsMod.promises.readFile(fileURL);
+ const hash = checksum(data);
+ return hash;
+ } catch {
+ // File doesn't exist
+ }
+ }
+ // No config file, still create a hash since we can compare nothing against nothing.
+ return checksum(`export default {}`);
+}
+
+function checksum(...datas: string[] | Uint8Array[]): string {
+ const hash = createHash('sha1');
+ datas.forEach(data => hash.update(data));
+ return hash.digest('base64');
}
function collectionTypeToFlag(type: 'content' | 'data') {
@@ -308,8 +424,15 @@ export function pluginContent(
opts: StaticBuildOptions,
internals: BuildInternals
): AstroBuildPlugin {
- const cachedChunks = new URL('./chunks/', opts.settings.config.cacheDir);
- const distChunks = new URL('./chunks/', opts.settings.config.outDir);
+ const { cacheDir, outDir } = opts.settings.config;
+
+ const chunksFolder = './' + CHUNKS_PATH;
+ const assetsFolder = './' + appendForwardSlash(opts.settings.config.build.assets);
+ // These are build output that is kept in the cache.
+ const cachedBuildOutput = [
+ { cached: new URL(chunksFolder, cacheDir), dist: new URL(chunksFolder, outDir) },
+ { cached: new URL(assetsFolder, cacheDir), dist: new URL(assetsFolder, outDir) },
+ ];
return {
targets: ['server'],
@@ -321,10 +444,9 @@ export function pluginContent(
if (isServerLikeOutput(opts.settings.config)) {
return { vitePlugin: undefined };
}
-
const lookupMap = await generateLookupMap({ settings: opts.settings, fs: fsMod });
return {
- vitePlugin: vitePluginContent(opts, lookupMap, internals),
+ vitePlugin: vitePluginContent(opts, lookupMap, internals, cachedBuildOutput),
};
},
@@ -335,8 +457,11 @@ export function pluginContent(
if (isServerLikeOutput(opts.settings.config)) {
return;
}
- if (fsMod.existsSync(distChunks)) {
- await copyFiles(distChunks, cachedChunks, true);
+ // Cache build output of chunks and assets
+ for(const { cached, dist } of cachedBuildOutput) {
+ if (fsMod.existsSync(dist)) {
+ await copyFiles(dist, cached, true);
+ }
}
},
},
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 5eebc5429..ede0e36e3 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -34,6 +34,7 @@ import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plug
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
import type { StaticBuildOptions } from './types.js';
import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js';
+import { CHUNKS_PATH } from './consts.js';
export async function viteBuild(opts: StaticBuildOptions) {
const { allPages, settings } = opts;
@@ -196,7 +197,7 @@ async function ssrBuild(
// We need to keep these separate
chunkFileNames(chunkInfo) {
const { name } = chunkInfo;
- let prefix = 'chunks/';
+ let prefix = CHUNKS_PATH;
let suffix = '_[hash].mjs';
if (isContentCache) {
@@ -454,7 +455,7 @@ export async function copyFiles(fromFolder: URL, toFolder: URL, includeDotfiles
dot: includeDotfiles,
});
if (files.length === 0) return;
- await Promise.all(
+ return await Promise.all(
files.map(async function copyFile(filename) {
const from = new URL(filename, fromFolder);
const to = new URL(filename, toFolder);
diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts
index f4ce11720..5bb2eda77 100644
--- a/packages/astro/src/core/config/config.ts
+++ b/packages/astro/src/core/config/config.ts
@@ -78,15 +78,19 @@ export function resolveRoot(cwd?: string | URL): string {
return cwd ? path.resolve(cwd) : process.cwd();
}
+// Config paths to search for. In order of likely appearance
+// to speed up the check.
+export const configPaths = Object.freeze([
+ 'astro.config.mjs',
+ 'astro.config.js',
+ 'astro.config.ts',
+ 'astro.config.mts',
+ 'astro.config.cjs',
+ 'astro.config.cts',
+]);
+
async function search(fsMod: typeof fs, root: string) {
- const paths = [
- 'astro.config.mjs',
- 'astro.config.js',
- 'astro.config.ts',
- 'astro.config.mts',
- 'astro.config.cjs',
- 'astro.config.cts',
- ].map((p) => path.join(root, p));
+ const paths = configPaths.map((p) => path.join(root, p));
for (const file of paths) {
if (fsMod.existsSync(file)) {
diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts
index 4bb1f0537..0f697ddd6 100644
--- a/packages/astro/src/core/config/index.ts
+++ b/packages/astro/src/core/config/index.ts
@@ -1,4 +1,4 @@
-export { resolveConfig, resolveConfigPath, resolveFlags, resolveRoot } from './config.js';
+export { configPaths, resolveConfig, resolveConfigPath, resolveFlags, resolveRoot } from './config.js';
export { createNodeLogger } from './logging.js';
export { mergeConfig } from './merge.js';
export type { AstroConfigType } from './schema.js';
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index 88072b20d..7603f24a6 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -477,9 +477,10 @@ type RunHookBuildDone = {
pages: string[];
routes: RouteData[];
logging: Logger;
+ cacheManifest: boolean;
};
-export async function runHookBuildDone({ config, pages, routes, logging }: RunHookBuildDone) {
+export async function runHookBuildDone({ config, pages, routes, logging, cacheManifest }: RunHookBuildDone) {
const dir = isServerLikeOutput(config) ? config.build.client : config.outDir;
await fs.promises.mkdir(dir, { recursive: true });
@@ -495,6 +496,7 @@ export async function runHookBuildDone({ config, pages, routes, logging }: RunHo
dir,
routes,
logger,
+ cacheManifest,
}),
logger: logging,
});
diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts
index 2f4e256b7..c33f5dd0c 100644
--- a/packages/astro/src/vite-plugin-astro/index.ts
+++ b/packages/astro/src/vite-plugin-astro/index.ts
@@ -89,12 +89,22 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
// modules are compiled first, then its virtual modules.
const filename = normalizePath(normalizeFilename(parsedId.filename, config.root));
let compileMetadata = astroFileToCompileMetadata.get(filename);
- // If `compileMetadata` doesn't exist in dev, that means the virtual module may have been invalidated.
- // We try to re-compile the main Astro module (`filename`) first before retrieving the metadata again.
- if (!compileMetadata && server) {
- const code = await loadId(server.pluginContainer, filename);
- // `compile` should re-set `filename` in `astroFileToCompileMetadata`
- if (code != null) await compile(code, filename);
+ if (!compileMetadata) {
+ // If `compileMetadata` doesn't exist in dev, that means the virtual module may have been invalidated.
+ // We try to re-compile the main Astro module (`filename`) first before retrieving the metadata again.
+ if(server) {
+ const code = await loadId(server.pluginContainer, filename);
+ // `compile` should re-set `filename` in `astroFileToCompileMetadata`
+ if (code != null) await compile(code, filename);
+ }
+ // When cached we might load client-side scripts during the build
+ else if(config.experimental.contentCollectionCache) {
+ await this.load({
+ id: filename,
+ resolveDependencies: false,
+ });
+ }
+
compileMetadata = astroFileToCompileMetadata.get(filename);
}
// If the metadata still doesn't exist, that means the virtual modules are somehow compiled first,
diff --git a/packages/astro/test/experimental-content-collections-cache-invalidation.test.js b/packages/astro/test/experimental-content-collections-cache-invalidation.test.js
new file mode 100644
index 000000000..5ec688a91
--- /dev/null
+++ b/packages/astro/test/experimental-content-collections-cache-invalidation.test.js
@@ -0,0 +1,98 @@
+import assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { loadFixture } from './test-utils.js';
+import fs from 'node:fs';
+import { copyFiles } from '../dist/core/build/static-build.js';
+
+describe('Experimental Content Collections cache - invalidation', () => {
+ class CacheBackup {
+ constructor(root, relCacheDir) {
+ this.root = new URL(root, import.meta.url);
+ this.cacheDir = new URL(relCacheDir, this.root);
+ this.tmpDir = new URL(`./tmp` + relCacheDir.slice(1), this.root);
+ }
+ backup() {
+ this.rmTmp();
+ copyFiles(this.cacheDir, this.tmpDir);
+ }
+ restore() {
+ fs.rmSync(this.cacheDir, { recursive: true });
+ copyFiles(this.tmpDir, this.cacheDir);
+ }
+ rmTmp() {
+ fs.rmSync(this.tmpDir, { force: true, recursive: true });
+ }
+ }
+
+ class ManifestTestPlugin {
+ used = false;
+ plugin() {
+ return {
+ name: '@test/manifest-used',
+ hooks: {
+ 'astro:build:done': ({ cacheManifest }) => {
+ this.used = cacheManifest;
+ }
+ }
+ }
+ }
+ }
+
+ describe('manifest version', () => {
+ let fixture, backup,
+ /** @type {ManifestTestPlugin} */
+ testPlugin;
+ before(async () => {
+ testPlugin = new ManifestTestPlugin();
+ fixture = await loadFixture({
+ root: './fixtures/content-collections-cache-invalidation/',
+ cacheDir: './cache/version-mismatch/',
+ experimental: { contentCollectionCache: true },
+ integrations: [
+ testPlugin.plugin()
+ ]
+ });
+ backup = new CacheBackup('./fixtures/content-collections-cache-invalidation/', './cache/version-mismatch/');
+ backup.backup();
+ await fixture.build();
+ });
+
+ after(async () => {
+ backup.restore();
+ //await fixture.clean();
+ });
+
+ it('Manifest was not used', () => {
+ assert.equal(testPlugin.used, false, 'manifest not used because of version mismatch');
+ });
+ });
+
+ describe('lockfiles', () => {
+ let fixture, backup,
+ /** @type {ManifestTestPlugin} */
+ testPlugin;
+ before(async () => {
+ testPlugin = new ManifestTestPlugin();
+ fixture = await loadFixture({
+ root: './fixtures/content-collections-cache-invalidation/',
+ cacheDir: './cache/lockfile-mismatch/',
+ experimental: { contentCollectionCache: true },
+ integrations: [
+ testPlugin.plugin()
+ ]
+ });
+ backup = new CacheBackup('./fixtures/content-collections-cache-invalidation/', './cache/lockfile-mismatch/');
+ backup.backup();
+ await fixture.build();
+ });
+
+ after(async () => {
+ backup.restore();
+ //await fixture.clean();
+ });
+
+ it('Manifest was not used', () => {
+ assert.equal(testPlugin.used, false, 'manifest not used because of lockfile mismatch');
+ });
+ });
+});
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/.gitignore b/packages/astro/test/fixtures/content-collections-cache-invalidation/.gitignore
new file mode 100644
index 000000000..3fec32c84
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/.gitignore
@@ -0,0 +1 @@
+tmp/
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/astro.config.mjs b/packages/astro/test/fixtures/content-collections-cache-invalidation/astro.config.mjs
new file mode 100644
index 000000000..a74151f32
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/astro.config.mjs
@@ -0,0 +1,12 @@
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ base: '/docs',
+ compressHTML: false,
+ vite: {
+ build: {
+ assetsInlineLimit: 0,
+ }
+ }
+});
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/cache/lockfile-mismatch/content/manifest.json b/packages/astro/test/fixtures/content-collections-cache-invalidation/cache/lockfile-mismatch/content/manifest.json
new file mode 100644
index 000000000..6b5f19749
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/cache/lockfile-mismatch/content/manifest.json
@@ -0,0 +1 @@
+{"version":1,"entries":[[{"collection":"blog","type":"content","entry":"/src/content/blog/one.md"},"No8AlxYwy8HK3dH9W3Mj/6SeHMI="]],"serverEntries":[],"clientEntries":[],"lockfiles":"2jmj7l5rSw0yVb/vlWAYkK/YBwk=","configs":"h80ch7FwzpG2BXKQM39ZqFpU3dg="} \ No newline at end of file
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/cache/version-mismatch/content/manifest.json b/packages/astro/test/fixtures/content-collections-cache-invalidation/cache/version-mismatch/content/manifest.json
new file mode 100644
index 000000000..20a905210
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/cache/version-mismatch/content/manifest.json
@@ -0,0 +1 @@
+{"version":1111111,"entries":[[{"collection":"blog","type":"content","entry":"/src/content/blog/one.md"},"No8AlxYwy8HK3dH9W3Mj/6SeHMI="]],"serverEntries":[],"clientEntries":[],"lockfiles":"2jmj7l5rSw0yVb/vlWAYkK/YBwk=","configs":"h80ch7FwzpG2BXKQM39ZqFpU3dg="}
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/package.json b/packages/astro/test/fixtures/content-collections-cache-invalidation/package.json
new file mode 100644
index 000000000..865550ef3
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/content-collections-cache-invalidation",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/blog/one.md b/packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/blog/one.md
new file mode 100644
index 000000000..fec6f5277
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/blog/one.md
@@ -0,0 +1,5 @@
+---
+title: One
+---
+
+Hello world
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/config.ts b/packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/config.ts
new file mode 100644
index 000000000..db96db2ea
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/src/content/config.ts
@@ -0,0 +1,10 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ type: 'collection',
+ schema: z.object({
+ title: z.string()
+ })
+});
+
+export const collections = { blog };
diff --git a/packages/astro/test/fixtures/content-collections-cache-invalidation/src/pages/index.astro b/packages/astro/test/fixtures/content-collections-cache-invalidation/src/pages/index.astro
new file mode 100644
index 000000000..e06d49b85
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections-cache-invalidation/src/pages/index.astro
@@ -0,0 +1,10 @@
+---
+---
+<html>
+ <head>
+ <title>Testing</title>
+ </head>
+ <body>
+ <h1>Testing</h1>
+ </body>
+</html>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8b81958b2..95d3b4649 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2466,6 +2466,12 @@ importers:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/content-collections-cache-invalidation:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+
packages/astro/test/fixtures/content-collections-empty-dir:
dependencies:
astro: