mirror of
https://github.com/withastro/astro.git
synced 2025-03-17 23:11:29 -05:00
feat(next): remove CCC (#12081)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
21b5e806c5
commit
8679954bf6
18 changed files with 59 additions and 1880 deletions
29
.changeset/wise-carrots-float.md
Normal file
29
.changeset/wise-carrots-float.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Removes the experimental `contentCollectionsCache` introduced in `3.5.0`.
|
||||
|
||||
Astro Content Layer API independently solves some of the caching and performance issues with legacy content collections that this strategy attempted to address. This feature has been replaced with continued work on improvements to the content layer. If you were using this experimental feature, you must now remove the flag from your Astro config as it no longer exists:
|
||||
|
||||
```diff
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
- contentCollectionsCache: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The `cacheManifest` boolean argument is no longer passed to the `astro:build:done` integration hook:
|
||||
|
||||
```diff
|
||||
const integration = {
|
||||
name: "my-integration",
|
||||
hooks: {
|
||||
"astro:build:done": ({
|
||||
- cacheManifest,
|
||||
logger
|
||||
}) => {}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -1,13 +1,12 @@
|
|||
import { dataToEsm } from '@rollup/pluginutils';
|
||||
import glob from 'fast-glob';
|
||||
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 type { Plugin } from 'vite';
|
||||
import { encodeName } from '../core/build/util.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { appendForwardSlash, removeFileExtension } from '../core/path.js';
|
||||
import { appendForwardSlash } from '../core/path.js';
|
||||
import { rootRelativePath } from '../core/viteUtils.js';
|
||||
import type { AstroSettings } from '../types/astro.js';
|
||||
import type { AstroPluginMetadata } from '../vite-plugin-astro/index.js';
|
||||
|
@ -52,7 +51,6 @@ export function astroContentVirtualModPlugin({
|
|||
fs,
|
||||
}: AstroContentVirtualModPluginParams): Plugin {
|
||||
let IS_DEV = false;
|
||||
const IS_SERVER = settings.buildOutput === 'server';
|
||||
let dataStoreFile: URL;
|
||||
return {
|
||||
name: 'astro-content-virtual-mod-plugin',
|
||||
|
@ -63,15 +61,7 @@ export function astroContentVirtualModPlugin({
|
|||
},
|
||||
async resolveId(id) {
|
||||
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 {
|
||||
// For SSG (production), we will build this file ourselves
|
||||
return { id: RESOLVED_VIRTUAL_MODULE_ID, external: true };
|
||||
}
|
||||
return RESOLVED_VIRTUAL_MODULE_ID;
|
||||
}
|
||||
if (id === DATA_STORE_VIRTUAL_ID) {
|
||||
return RESOLVED_DATA_STORE_VIRTUAL_ID;
|
||||
|
@ -117,8 +107,6 @@ export function astroContentVirtualModPlugin({
|
|||
settings,
|
||||
fs,
|
||||
lookupMap,
|
||||
IS_DEV,
|
||||
IS_SERVER,
|
||||
isClient,
|
||||
});
|
||||
|
||||
|
@ -167,17 +155,6 @@ export function astroContentVirtualModPlugin({
|
|||
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`);
|
||||
}
|
||||
},
|
||||
|
||||
configureServer(server) {
|
||||
const dataStorePath = fileURLToPath(dataStoreFile);
|
||||
|
||||
|
@ -214,15 +191,11 @@ export function astroContentVirtualModPlugin({
|
|||
export async function generateContentEntryFile({
|
||||
settings,
|
||||
lookupMap,
|
||||
IS_DEV,
|
||||
IS_SERVER,
|
||||
isClient,
|
||||
}: {
|
||||
settings: AstroSettings;
|
||||
fs: typeof nodeFs;
|
||||
lookupMap: ContentLookupMap;
|
||||
IS_DEV: boolean;
|
||||
IS_SERVER: boolean;
|
||||
isClient: boolean;
|
||||
}) {
|
||||
const contentPaths = getContentPaths(settings.config);
|
||||
|
@ -231,33 +204,23 @@ export async function generateContentEntryFile({
|
|||
let contentEntryGlobResult: string;
|
||||
let dataEntryGlobResult: string;
|
||||
let renderEntryGlobResult: string;
|
||||
if (IS_DEV || IS_SERVER || !settings.config.experimental.contentCollectionCache) {
|
||||
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
|
||||
const contentEntryExts = [...contentEntryConfigByExt.keys()];
|
||||
const dataEntryExts = getDataEntryExts(settings);
|
||||
const createGlob = (value: string[], flag: string) =>
|
||||
`import.meta.glob(${JSON.stringify(value)}, { query: { ${flag}: true } })`;
|
||||
contentEntryGlobResult = createGlob(
|
||||
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
|
||||
CONTENT_FLAG,
|
||||
);
|
||||
dataEntryGlobResult = createGlob(
|
||||
globWithUnderscoresIgnored(relContentDir, dataEntryExts),
|
||||
DATA_FLAG,
|
||||
);
|
||||
renderEntryGlobResult = createGlob(
|
||||
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
|
||||
CONTENT_RENDER_FLAG,
|
||||
);
|
||||
} else {
|
||||
contentEntryGlobResult = getStringifiedCollectionFromLookup(
|
||||
'content',
|
||||
relContentDir,
|
||||
lookupMap,
|
||||
);
|
||||
dataEntryGlobResult = getStringifiedCollectionFromLookup('data', relContentDir, lookupMap);
|
||||
renderEntryGlobResult = getStringifiedCollectionFromLookup('render', relContentDir, lookupMap);
|
||||
}
|
||||
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
|
||||
const contentEntryExts = [...contentEntryConfigByExt.keys()];
|
||||
const dataEntryExts = getDataEntryExts(settings);
|
||||
const createGlob = (value: string[], flag: string) =>
|
||||
`import.meta.glob(${JSON.stringify(value)}, { query: { ${flag}: true } })`;
|
||||
contentEntryGlobResult = createGlob(
|
||||
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
|
||||
CONTENT_FLAG,
|
||||
);
|
||||
dataEntryGlobResult = createGlob(
|
||||
globWithUnderscoresIgnored(relContentDir, dataEntryExts),
|
||||
DATA_FLAG,
|
||||
);
|
||||
renderEntryGlobResult = createGlob(
|
||||
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
|
||||
CONTENT_RENDER_FLAG,
|
||||
);
|
||||
|
||||
let virtualModContents: string;
|
||||
if (isClient) {
|
||||
|
@ -278,37 +241,6 @@ export async function generateContentEntryFile({
|
|||
return virtualModContents;
|
||||
}
|
||||
|
||||
function getStringifiedCollectionFromLookup(
|
||||
wantedType: 'content' | 'data' | 'render',
|
||||
relContentDir: string,
|
||||
lookupMap: ContentLookupMap,
|
||||
) {
|
||||
let str = '{';
|
||||
// In dev, we don't need to normalize the import specifier at all. Vite handles it.
|
||||
let normalize = (slug: string) => slug;
|
||||
// For prod builds, we need to transform from `/src/content/**/*.{md,mdx,json,yaml}` to a relative `./**/*.mjs` import
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const suffix = wantedType === 'render' ? '.entry.mjs' : '.mjs';
|
||||
normalize = (slug: string) =>
|
||||
`${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: string) => `${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 += `\n "${slug}": () => import("${normalize(slug)}"),`;
|
||||
}
|
||||
}
|
||||
}
|
||||
str += '\n}';
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a map from a collection + slug to the local file path.
|
||||
* This is used internally to resolve entry imports when using `getEntry()`.
|
||||
|
|
|
@ -61,14 +61,6 @@ export default async function build(
|
|||
const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root));
|
||||
|
||||
if (inlineConfig.force) {
|
||||
if (astroConfig.experimental.contentCollectionCache) {
|
||||
const contentCacheDir = new URL('./content/', astroConfig.cacheDir);
|
||||
if (fs.existsSync(contentCacheDir)) {
|
||||
logger.debug('content', 'clearing content cache');
|
||||
await fs.promises.rm(contentCacheDir, { force: true, recursive: true });
|
||||
logger.warn('content', 'content cache cleared (force)');
|
||||
}
|
||||
}
|
||||
await clearContentLayerCache({ settings, logger, fs });
|
||||
}
|
||||
|
||||
|
@ -239,7 +231,6 @@ class AstroBuilder {
|
|||
.map((pageData) => pageData.route)
|
||||
.concat(hasServerIslands ? getServerIslandRouteData(this.settings.config) : []),
|
||||
logging: this.logger,
|
||||
cacheManifest: internals.cacheManifestUsed,
|
||||
});
|
||||
|
||||
if (this.logger.level && levels[this.logger.level()] <= levels['info']) {
|
||||
|
|
|
@ -72,9 +72,6 @@ export interface BuildInternals {
|
|||
*/
|
||||
discoveredScripts: Set<string>;
|
||||
|
||||
cachedClientEntries: string[];
|
||||
cacheManifestUsed: boolean;
|
||||
|
||||
/**
|
||||
* Map of propagated module ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`)
|
||||
* to a set of stylesheets that it uses.
|
||||
|
@ -104,7 +101,6 @@ export interface BuildInternals {
|
|||
*/
|
||||
export function createBuildInternals(): BuildInternals {
|
||||
return {
|
||||
cachedClientEntries: [],
|
||||
cssModuleToChunkIdMap: new Map(),
|
||||
inlinedScripts: new Map(),
|
||||
entrySpecifierToBundleMap: new Map<string, string>(),
|
||||
|
@ -121,7 +117,6 @@ export function createBuildInternals(): BuildInternals {
|
|||
staticFiles: new Set(),
|
||||
componentMetadata: new Map(),
|
||||
entryPoints: new Map(),
|
||||
cacheManifestUsed: false,
|
||||
prerenderOnlyChunks: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import type { AstroBuildPluginContainer } from '../plugin.js';
|
|||
import { pluginAnalyzer } from './plugin-analyzer.js';
|
||||
import { pluginChunks } from './plugin-chunks.js';
|
||||
import { pluginComponentEntry } from './plugin-component-entry.js';
|
||||
import { pluginContent } from './plugin-content.js';
|
||||
import { pluginCSS } from './plugin-css.js';
|
||||
import { pluginInternals } from './plugin-internals.js';
|
||||
import { pluginManifest } from './plugin-manifest.js';
|
||||
|
@ -23,7 +22,6 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
|
|||
register(pluginRenderers(options));
|
||||
register(pluginMiddleware(options, internals));
|
||||
register(pluginPages(options, internals));
|
||||
register(pluginContent(options, internals));
|
||||
register(pluginCSS(options, internals));
|
||||
register(astroHeadBuildPlugin(internals));
|
||||
register(pluginPrerender(options, internals));
|
||||
|
|
|
@ -1,524 +0,0 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import fsMod from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import glob from 'fast-glob';
|
||||
import pLimit from 'p-limit';
|
||||
import { type Plugin as VitePlugin, normalizePath } from 'vite';
|
||||
import { CONTENT_RENDER_FLAG, PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
|
||||
import { type ContentLookupMap, hasContentFlag } from '../../../content/utils.js';
|
||||
import {
|
||||
generateContentEntryFile,
|
||||
generateLookupMap,
|
||||
} from '../../../content/vite-plugin-content-virtual-mod.js';
|
||||
import type { AstroConfig } from '../../../types/public/config.js';
|
||||
import { configPaths } from '../../config/index.js';
|
||||
import { emptyDir } from '../../fs/index.js';
|
||||
import {
|
||||
appendForwardSlash,
|
||||
joinPaths,
|
||||
removeFileExtension,
|
||||
removeLeadingForwardSlash,
|
||||
} from '../../path.js';
|
||||
import { isContentCollectionsCacheEnabled } from '../../util.js';
|
||||
import { addRollupInput } from '../add-rollup-input.js';
|
||||
import { CHUNKS_PATH, CONTENT_PATH } from '../consts.js';
|
||||
import type { BuildInternals } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin.js';
|
||||
import { copyFiles } from '../static-build.js';
|
||||
import type { StaticBuildOptions } from '../types.js';
|
||||
import { encodeName } from '../util.js';
|
||||
import { extendManualChunks } from './util.js';
|
||||
|
||||
const CONTENT_CACHE_DIR = './' + CONTENT_PATH;
|
||||
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 = 1;
|
||||
|
||||
interface ContentManifestKey {
|
||||
collection: string;
|
||||
type: 'content' | 'data';
|
||||
entry: string;
|
||||
}
|
||||
|
||||
interface ContentManifest {
|
||||
version: number;
|
||||
entries: [ContentManifestKey, string][];
|
||||
// Tracks components that should be included in the server build
|
||||
// When the cache is restored, these might no longer be referenced
|
||||
serverEntries: string[];
|
||||
// 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: NO_MANIFEST_VERSION,
|
||||
entries: [],
|
||||
serverEntries: [],
|
||||
clientEntries: [],
|
||||
lockfiles: '',
|
||||
configs: '',
|
||||
};
|
||||
}
|
||||
|
||||
const getContentRoot = (config: AstroConfig) => new URL('./content/', config.outDir);
|
||||
const getContentCacheDir = (config: AstroConfig) => new URL(CONTENT_CACHE_DIR, config.cacheDir);
|
||||
const getCacheTmp = (contentCacheDir: URL) => new URL('./.tmp/', contentCacheDir);
|
||||
|
||||
function vitePluginContent(
|
||||
opts: StaticBuildOptions,
|
||||
lookupMap: ContentLookupMap,
|
||||
internals: BuildInternals,
|
||||
cachedBuildOutput: Array<{ cached: URL; dist: URL }>,
|
||||
): VitePlugin {
|
||||
const { config } = opts.settings;
|
||||
const distContentRoot = getContentRoot(config);
|
||||
const contentCacheDir = getContentCacheDir(config);
|
||||
const contentManifestFile = new URL(CONTENT_MANIFEST_FILE, 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);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
name: '@astro/plugin-build-content',
|
||||
|
||||
async options(options) {
|
||||
let newOptions = Object.assign({}, options);
|
||||
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));
|
||||
const input = fileURLToPath(fileURL);
|
||||
// Adds `/src/content/blog/post-1.md?astroContentCollectionEntry` as a top-level input
|
||||
const inputs = [`${input}?${collectionTypeToFlag(type)}`];
|
||||
if (type === 'content') {
|
||||
// Content entries also need to include the version with the RENDER flag
|
||||
inputs.push(`${input}?${CONTENT_RENDER_FLAG}`);
|
||||
}
|
||||
newOptions = addRollupInput(newOptions, inputs);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// Copy over the content cache now so that new files override it
|
||||
const cacheExists = fsMod.existsSync(contentCacheDir);
|
||||
if (cacheExists) {
|
||||
await copyFiles(contentCacheDir, distContentRoot, false);
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing needs to be rebuilt, we inject a fake entrypoint to appease Rollup
|
||||
if (entries.buildFromSource.length === 0) {
|
||||
newOptions = addRollupInput(newOptions, [virtualEmptyModuleId]);
|
||||
injectedEmptyFile = true;
|
||||
}
|
||||
return newOptions;
|
||||
},
|
||||
|
||||
outputOptions(outputOptions) {
|
||||
const rootPath = normalizePath(fileURLToPath(opts.settings.config.root));
|
||||
const srcPath = normalizePath(fileURLToPath(opts.settings.config.srcDir));
|
||||
const entryCache = new Map<string, string>();
|
||||
extendManualChunks(outputOptions, {
|
||||
before(id, meta) {
|
||||
if (id.startsWith(srcPath) && id.slice(srcPath.length).startsWith('content')) {
|
||||
const info = meta.getModuleInfo(id);
|
||||
if (
|
||||
info?.dynamicImporters.length === 1 &&
|
||||
hasContentFlag(info.dynamicImporters[0], PROPAGATED_ASSET_FLAG)
|
||||
) {
|
||||
const [srcRelativePath] = id.replace(rootPath, '/').split('?');
|
||||
const resultId = encodeName(
|
||||
`${removeLeadingForwardSlash(removeFileExtension(srcRelativePath))}.render.mjs`,
|
||||
);
|
||||
return resultId;
|
||||
}
|
||||
const [srcRelativePath, flag] = id.replace(rootPath, '/').split('?');
|
||||
const collectionEntry = findEntryFromSrcRelativePath(
|
||||
lookupMap,
|
||||
srcRelativePath,
|
||||
entryCache,
|
||||
);
|
||||
if (collectionEntry) {
|
||||
let suffix = '.mjs';
|
||||
if (flag === PROPAGATED_ASSET_FLAG) {
|
||||
suffix = '.entry.mjs';
|
||||
}
|
||||
id =
|
||||
removeLeadingForwardSlash(
|
||||
removeFileExtension(encodeName(id.replace(srcPath, '/'))),
|
||||
) + suffix;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
resolveId(id) {
|
||||
if (id === virtualEmptyModuleId) {
|
||||
return resolvedVirtualEmptyModuleId;
|
||||
}
|
||||
},
|
||||
|
||||
async load(id) {
|
||||
if (id === resolvedVirtualEmptyModuleId) {
|
||||
return {
|
||||
code: `// intentionally left empty!\nexport default {}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async generateBundle(_options, bundle) {
|
||||
const code = await generateContentEntryFile({
|
||||
settings: opts.settings,
|
||||
fs: fsMod,
|
||||
lookupMap,
|
||||
IS_DEV: false,
|
||||
IS_SERVER: false,
|
||||
isClient: false,
|
||||
});
|
||||
this.emitFile({
|
||||
type: 'prebuilt-chunk',
|
||||
code,
|
||||
fileName: 'content/entry.mjs',
|
||||
});
|
||||
if (!injectedEmptyFile) return;
|
||||
Object.keys(bundle).forEach((key) => {
|
||||
const mod = bundle[key];
|
||||
if (mod.type === 'asset') return;
|
||||
if (mod.facadeModuleId === resolvedVirtualEmptyModuleId) {
|
||||
delete bundle[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async writeBundle() {
|
||||
// These are stored in the manifest to ensure that they are included in the build
|
||||
// in case they aren't referenced _outside_ of the cached content.
|
||||
// We can use this info in the manifest to run a proper client build again.
|
||||
const clientComponents = new Set([
|
||||
...oldManifest.clientEntries,
|
||||
...internals.discoveredHydratedComponents.keys(),
|
||||
...internals.discoveredClientOnlyComponents.keys(),
|
||||
...internals.discoveredScripts,
|
||||
]);
|
||||
// Likewise, these are server modules that might not be referenced
|
||||
// once the cached items are excluded from the build process
|
||||
const serverComponents = new Set([
|
||||
...oldManifest.serverEntries,
|
||||
...internals.discoveredHydratedComponents.keys(),
|
||||
]);
|
||||
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',
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function findEntryFromSrcRelativePath(
|
||||
lookupMap: ContentLookupMap,
|
||||
srcRelativePath: string,
|
||||
entryCache: Map<string, string>,
|
||||
) {
|
||||
let value = entryCache.get(srcRelativePath);
|
||||
if (value) return value;
|
||||
for (const collection of Object.values(lookupMap)) {
|
||||
for (const entry of Object.values(collection)) {
|
||||
for (const entryFile of Object.values(entry)) {
|
||||
if (entryFile === srcRelativePath) {
|
||||
value = entryFile;
|
||||
entryCache.set(srcRelativePath, entryFile);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ContentEntries {
|
||||
restoreFromCache: ContentManifestKey[];
|
||||
buildFromSource: ContentManifestKey[];
|
||||
}
|
||||
|
||||
function getEntriesFromManifests(
|
||||
oldManifest: ContentManifest,
|
||||
newManifest: ContentManifest,
|
||||
): ContentEntries {
|
||||
const { entries: oldEntries } = oldManifest;
|
||||
const { entries: newEntries } = newManifest;
|
||||
let entries: ContentEntries = { restoreFromCache: [], buildFromSource: [] };
|
||||
|
||||
const newEntryMap = new Map<ContentManifestKey, string>(newEntries);
|
||||
if (manifestState(oldManifest, newManifest) !== 'valid') {
|
||||
entries.buildFromSource = Array.from(newEntryMap.keys());
|
||||
return entries;
|
||||
}
|
||||
const oldEntryHashMap = new Map<string, ContentManifestKey>(
|
||||
oldEntries.map(([key, hash]) => [hash, key]),
|
||||
);
|
||||
|
||||
for (const [entry, hash] of newEntryMap) {
|
||||
if (oldEntryHashMap.has(hash)) {
|
||||
entries.restoreFromCache.push(entry);
|
||||
} else {
|
||||
entries.buildFromSource.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
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 = createContentManifest();
|
||||
manifest.version = CONTENT_MANIFEST_VERSION;
|
||||
const limit = pLimit(10);
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const [collection, { type, entries }] of Object.entries(lookupMap)) {
|
||||
for (const entry of Object.values(entries)) {
|
||||
const key: ContentManifestKey = { collection, type, entry };
|
||||
const fileURL = new URL(encodeURI(joinPaths(opts.settings.config.root.toString(), entry)));
|
||||
promises.push(
|
||||
limit(async () => {
|
||||
const data = await fsMod.promises.readFile(fileURL, { encoding: 'utf8' });
|
||||
manifest.entries.push([key, checksum(data, fileURL.toString())]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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') {
|
||||
const name = type[0].toUpperCase() + type.slice(1);
|
||||
return `astro${name}CollectionEntry`;
|
||||
}
|
||||
|
||||
export async function copyContentToCache(opts: StaticBuildOptions) {
|
||||
const { config } = opts.settings;
|
||||
const distContentRoot = getContentRoot(config);
|
||||
const contentCacheDir = getContentCacheDir(config);
|
||||
const cacheTmp = getCacheTmp(contentCacheDir);
|
||||
|
||||
await fsMod.promises.mkdir(cacheTmp, { recursive: true });
|
||||
await copyFiles(distContentRoot, cacheTmp, true);
|
||||
await copyFiles(cacheTmp, contentCacheDir);
|
||||
|
||||
// Read the files from `dist/content/*` and `dist/chunks/*` so that
|
||||
// we can clean them out of the dist folder
|
||||
let files: string[] = [];
|
||||
await Promise.all([
|
||||
glob(`**/*.{mjs,json}`, {
|
||||
cwd: fileURLToPath(cacheTmp),
|
||||
}).then((f) => files.push(...f.map((file) => CONTENT_PATH + file))),
|
||||
glob(`**/*.{mjs,json}`, {
|
||||
cwd: fileURLToPath(new URL('./' + CHUNKS_PATH, config.outDir)),
|
||||
}).then((f) => files.push(...f.map((file) => CHUNKS_PATH + file))),
|
||||
]);
|
||||
|
||||
// Remove the tmp folder that's no longer needed.
|
||||
await fsMod.promises.rm(cacheTmp, { recursive: true, force: true });
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function pluginContent(
|
||||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals,
|
||||
): AstroBuildPlugin {
|
||||
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'],
|
||||
hooks: {
|
||||
async 'build:before'() {
|
||||
if (!isContentCollectionsCacheEnabled(opts.settings)) {
|
||||
return { vitePlugin: undefined };
|
||||
}
|
||||
const lookupMap = await generateLookupMap({ settings: opts.settings, fs: fsMod });
|
||||
return {
|
||||
vitePlugin: vitePluginContent(opts, lookupMap, internals, cachedBuildOutput),
|
||||
};
|
||||
},
|
||||
|
||||
async 'build:post'() {
|
||||
if (!isContentCollectionsCacheEnabled(opts.settings)) {
|
||||
return;
|
||||
}
|
||||
// Cache build output of chunks and assets
|
||||
const promises: Promise<void[] | undefined>[] = [];
|
||||
for (const { cached, dist } of cachedBuildOutput) {
|
||||
if (fsMod.existsSync(dist)) {
|
||||
promises.push(copyFiles(dist, cached, true));
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length) await Promise.all(promises);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,24 +1,17 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { teardown } from '@astrojs/compiler';
|
||||
import glob from 'fast-glob';
|
||||
import { bgGreen, black, green } from 'kleur/colors';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import * as vite from 'vite';
|
||||
import { PROPAGATED_ASSET_FLAG } from '../../content/consts.js';
|
||||
import {
|
||||
getSymlinkedContentCollections,
|
||||
hasAnyContentFlag,
|
||||
reverseSymlink,
|
||||
} from '../../content/utils.js';
|
||||
import { type BuildInternals, createBuildInternals } from '../../core/build/internal.js';
|
||||
import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js';
|
||||
import { appendForwardSlash, prependForwardSlash, removeFileExtension } from '../../core/path.js';
|
||||
import { appendForwardSlash, prependForwardSlash } from '../../core/path.js';
|
||||
import { runHookBuildSetup } from '../../integrations/hooks.js';
|
||||
import { getOutputDirectory } from '../../prerender/utils.js';
|
||||
import type { RouteData } from '../../types/public/internal.js';
|
||||
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
import type { Logger } from '../logger/core.js';
|
||||
import { routeIsRedirect } from '../redirects/index.js';
|
||||
import { getOutDirWithinCwd } from './common.js';
|
||||
import { CHUNKS_PATH } from './consts.js';
|
||||
|
@ -26,7 +19,6 @@ import { generatePages } from './generate.js';
|
|||
import { trackPageData } from './internal.js';
|
||||
import { type AstroBuildPluginContainer, createPluginContainer } from './plugin.js';
|
||||
import { registerAllPlugins } from './plugins/index.js';
|
||||
import { copyContentToCache } from './plugins/plugin-content.js';
|
||||
import { RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugins/plugin-manifest.js';
|
||||
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
||||
import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js';
|
||||
|
@ -36,7 +28,7 @@ import type { StaticBuildOptions } from './types.js';
|
|||
import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js';
|
||||
|
||||
export async function viteBuild(opts: StaticBuildOptions) {
|
||||
const { allPages, settings, logger } = opts;
|
||||
const { allPages, settings } = opts;
|
||||
|
||||
settings.timer.start('SSR build');
|
||||
|
||||
|
@ -72,7 +64,7 @@ export async function viteBuild(opts: StaticBuildOptions) {
|
|||
// Build your project (SSR application code, assets, client JS, etc.)
|
||||
const ssrTime = performance.now();
|
||||
opts.logger.info('build', `Building ${settings.config.output} entrypoints...`);
|
||||
const ssrOutput = await ssrBuild(opts, internals, pageInput, container, logger);
|
||||
const ssrOutput = await ssrBuild(opts, internals, pageInput, container);
|
||||
opts.logger.info('build', green(`✓ Completed in ${getTimeStat(ssrTime, performance.now())}.`));
|
||||
|
||||
settings.timer.end('SSR build');
|
||||
|
@ -84,7 +76,6 @@ export async function viteBuild(opts: StaticBuildOptions) {
|
|||
.filter((a) => typeof a === 'string') as string[];
|
||||
|
||||
const clientInput = new Set([
|
||||
...internals.cachedClientEntries,
|
||||
...internals.discoveredHydratedComponents.keys(),
|
||||
...internals.discoveredClientOnlyComponents.keys(),
|
||||
...rendererClientEntrypoints,
|
||||
|
@ -102,9 +93,6 @@ export async function viteBuild(opts: StaticBuildOptions) {
|
|||
const clientOutputs = viteBuildReturnToRollupOutputs(clientOutput ?? []);
|
||||
await runPostBuildHooks(container, ssrOutputs, clientOutputs);
|
||||
let contentFileNames: string[] | undefined = undefined;
|
||||
if (opts.settings.config.experimental.contentCollectionCache) {
|
||||
contentFileNames = await copyContentToCache(opts);
|
||||
}
|
||||
settings.timer.end('Client build');
|
||||
|
||||
// Free up memory
|
||||
|
@ -160,17 +148,12 @@ async function ssrBuild(
|
|||
internals: BuildInternals,
|
||||
input: Set<string>,
|
||||
container: AstroBuildPluginContainer,
|
||||
logger: Logger,
|
||||
) {
|
||||
const buildID = Date.now().toString();
|
||||
const { allPages, settings, viteConfig } = opts;
|
||||
const ssr = settings.buildOutput === 'server';
|
||||
const out = getOutputDirectory(settings);
|
||||
const routes = Object.values(allPages).flatMap((pageData) => pageData.route);
|
||||
const isContentCache = !ssr && settings.config.experimental.contentCollectionCache;
|
||||
const { lastVitePlugins, vitePlugins } = await container.runBeforeHook('server', input);
|
||||
const contentDir = new URL('./src/content', settings.config.root);
|
||||
const symlinks = await getSymlinkedContentCollections({ contentDir, logger, fs });
|
||||
const viteBuildConfig: vite.InlineConfig = {
|
||||
...viteConfig,
|
||||
mode: viteConfig.mode || 'production',
|
||||
|
@ -191,9 +174,9 @@ async function ssrBuild(
|
|||
preserveEntrySignatures: 'exports-only',
|
||||
input: [],
|
||||
output: {
|
||||
hoistTransitiveImports: isContentCache,
|
||||
hoistTransitiveImports: false,
|
||||
format: 'esm',
|
||||
minifyInternalExports: !isContentCache,
|
||||
minifyInternalExports: true,
|
||||
// Server chunks can't go in the assets (_astro) folder
|
||||
// We need to keep these separate
|
||||
chunkFileNames(chunkInfo) {
|
||||
|
@ -201,18 +184,6 @@ async function ssrBuild(
|
|||
let prefix = CHUNKS_PATH;
|
||||
let suffix = '_[hash].mjs';
|
||||
|
||||
if (isContentCache) {
|
||||
prefix += `${buildID}/`;
|
||||
suffix = '.mjs';
|
||||
|
||||
if (name.includes('/content/')) {
|
||||
const parts = name.split('/');
|
||||
if (parts.at(1) === 'content') {
|
||||
return encodeName(parts.slice(1).join('/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes chunks have the `@_@astro` suffix due to SSR logic. Remove it!
|
||||
// TODO: refactor our build logic to avoid this
|
||||
if (name.includes(ASTRO_PAGE_EXTENSION_POST_PATTERN)) {
|
||||
|
@ -244,21 +215,6 @@ async function ssrBuild(
|
|||
return 'manifest_[hash].mjs';
|
||||
} else if (chunkInfo.facadeModuleId === settings.adapter?.serverEntrypoint) {
|
||||
return 'adapter_[hash].mjs';
|
||||
} else if (
|
||||
settings.config.experimental.contentCollectionCache &&
|
||||
chunkInfo.facadeModuleId &&
|
||||
hasAnyContentFlag(chunkInfo.facadeModuleId)
|
||||
) {
|
||||
const moduleId = reverseSymlink({
|
||||
symlinks,
|
||||
entry: chunkInfo.facadeModuleId,
|
||||
contentDir,
|
||||
});
|
||||
const [srcRelative, flag] = moduleId.split('/src/')[1].split('?');
|
||||
if (flag === PROPAGATED_ASSET_FLAG) {
|
||||
return encodeName(`${removeFileExtension(srcRelative)}.entry.mjs`);
|
||||
}
|
||||
return encodeName(`${removeFileExtension(srcRelative)}.mjs`);
|
||||
} else {
|
||||
return '[name].mjs';
|
||||
}
|
||||
|
|
|
@ -90,7 +90,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
|
|||
validateSecrets: false,
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: false,
|
||||
clientPrerender: false,
|
||||
contentIntellisense: false,
|
||||
},
|
||||
|
@ -510,10 +509,6 @@ export const AstroConfigSchema = z.object({
|
|||
.default(ASTRO_CONFIG_DEFAULTS.env),
|
||||
experimental: z
|
||||
.object({
|
||||
contentCollectionCache: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.contentCollectionCache),
|
||||
clientPrerender: z
|
||||
.boolean()
|
||||
.optional()
|
||||
|
|
|
@ -158,14 +158,6 @@ export function isEndpoint(file: URL, settings: AstroSettings): boolean {
|
|||
return !endsWithPageExt(file, settings) && !file.toString().includes('?astro');
|
||||
}
|
||||
|
||||
export function isContentCollectionsCacheEnabled(settings: AstroSettings): boolean {
|
||||
return (
|
||||
settings.config.experimental.contentCollectionCache &&
|
||||
// contentCollectionsCache is an SSG only feature
|
||||
settings.buildOutput !== 'server'
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveJsToTs(filePath: string) {
|
||||
if (filePath.endsWith('.jsx') && !fs.existsSync(filePath)) {
|
||||
const tryPath = filePath.slice(0, -4) + '.tsx';
|
||||
|
|
|
@ -579,16 +579,9 @@ type RunHookBuildDone = {
|
|||
pages: string[];
|
||||
routes: RouteData[];
|
||||
logging: Logger;
|
||||
cacheManifest: boolean;
|
||||
};
|
||||
|
||||
export async function runHookBuildDone({
|
||||
settings,
|
||||
pages,
|
||||
routes,
|
||||
logging,
|
||||
cacheManifest,
|
||||
}: RunHookBuildDone) {
|
||||
export async function runHookBuildDone({ settings, pages, routes, logging }: RunHookBuildDone) {
|
||||
const dir =
|
||||
settings.buildOutput === 'server' ? settings.config.build.client : settings.config.outDir;
|
||||
await fsMod.promises.mkdir(dir, { recursive: true });
|
||||
|
@ -605,7 +598,6 @@ export async function runHookBuildDone({
|
|||
dir,
|
||||
routes: integrationRoutes,
|
||||
logger,
|
||||
cacheManifest,
|
||||
}),
|
||||
logger: logging,
|
||||
});
|
||||
|
|
|
@ -1552,25 +1552,6 @@ export interface AstroUserConfig {
|
|||
* These flags are not guaranteed to be stable.
|
||||
*/
|
||||
experimental?: {
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.contentCollectionCache
|
||||
* @type {boolean}
|
||||
* @default `false`
|
||||
* @version 3.5.0
|
||||
* @description
|
||||
* Enables a persistent cache for content collections when building in static mode.
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* experimental: {
|
||||
* contentCollectionCache: true,
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
contentCollectionCache?: boolean;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.clientPrerender
|
||||
|
|
|
@ -227,7 +227,6 @@ export interface BaseIntegrationHooks {
|
|||
dir: URL;
|
||||
routes: IntegrationRouteData[];
|
||||
logger: AstroIntegrationLogger;
|
||||
cacheManifest: boolean;
|
||||
}) => void | Promise<void>;
|
||||
'astro:route:setup': (options: {
|
||||
route: RouteOptions;
|
||||
|
|
|
@ -98,13 +98,6 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
|
|||
// `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);
|
||||
}
|
||||
|
|
|
@ -1,166 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { fixLineEndings, loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Experimental Content Collections cache - references', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collection-references/',
|
||||
experimental: { contentCollectionCache: true },
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
const modes = ['dev', 'prod'];
|
||||
|
||||
for (const mode of modes) {
|
||||
describe(mode, () => {
|
||||
before(async () => {
|
||||
if (mode === 'prod') {
|
||||
await fixture.build();
|
||||
} else if (mode === 'dev') {
|
||||
devServer = await fixture.startDevServer();
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (mode === 'dev') devServer?.stop();
|
||||
});
|
||||
|
||||
describe(`JSON result`, () => {
|
||||
let json;
|
||||
before(async () => {
|
||||
if (mode === 'prod') {
|
||||
const rawJson = await fixture.readFile('/welcome-data.json');
|
||||
json = JSON.parse(rawJson);
|
||||
} else if (mode === 'dev') {
|
||||
const rawJsonResponse = await fixture.fetch('/welcome-data.json');
|
||||
const rawJson = await rawJsonResponse.text();
|
||||
json = JSON.parse(rawJson);
|
||||
}
|
||||
});
|
||||
|
||||
it('Returns expected keys', () => {
|
||||
assert.equal(json.hasOwnProperty('welcomePost'), true);
|
||||
assert.equal(json.hasOwnProperty('banner'), true);
|
||||
assert.equal(json.hasOwnProperty('author'), true);
|
||||
assert.equal(json.hasOwnProperty('relatedPosts'), true);
|
||||
});
|
||||
|
||||
it('Returns `banner` data', () => {
|
||||
const { banner } = json;
|
||||
assert.equal(banner.hasOwnProperty('data'), true);
|
||||
assert.equal(banner.id, 'welcome');
|
||||
assert.equal(banner.collection, 'banners');
|
||||
assert.equal(
|
||||
banner.data.alt,
|
||||
'Futuristic landscape with chrome buildings and blue skies',
|
||||
);
|
||||
|
||||
assert.equal(banner.data.src.width, 400);
|
||||
assert.equal(banner.data.src.height, 225);
|
||||
assert.equal(banner.data.src.format, 'jpg');
|
||||
assert.equal(banner.data.src.src.includes('the-future'), true);
|
||||
});
|
||||
|
||||
it('Returns `author` data', () => {
|
||||
const { author } = json;
|
||||
assert.equal(author.hasOwnProperty('data'), true);
|
||||
assert.deepEqual(author, {
|
||||
id: 'nate-moore',
|
||||
collection: 'authors',
|
||||
data: {
|
||||
name: 'Nate Something Moore',
|
||||
twitter: 'https://twitter.com/n_moore',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns `relatedPosts` data', () => {
|
||||
const { relatedPosts } = json;
|
||||
assert.equal(Array.isArray(relatedPosts), true);
|
||||
const topLevelInfo = relatedPosts.map(({ data, body, ...meta }) => ({
|
||||
...meta,
|
||||
body: fixLineEndings(body).trim(),
|
||||
}));
|
||||
assert.deepEqual(topLevelInfo, [
|
||||
{
|
||||
id: 'related-1.md',
|
||||
slug: 'related-1',
|
||||
body: '# Related post 1\n\nThis is related to the welcome post.',
|
||||
collection: 'blog',
|
||||
},
|
||||
{
|
||||
id: 'related-2.md',
|
||||
slug: 'related-2',
|
||||
body: '# Related post 2\n\nThis is related to the welcome post.',
|
||||
collection: 'blog',
|
||||
},
|
||||
]);
|
||||
const postData = relatedPosts.map(({ data }) => data);
|
||||
assert.deepEqual(postData, [
|
||||
{
|
||||
title: 'Related post 1',
|
||||
banner: { id: 'welcome', collection: 'banners' },
|
||||
author: { id: 'fred-schott', collection: 'authors' },
|
||||
},
|
||||
{
|
||||
title: 'Related post 2',
|
||||
banner: { id: 'welcome', collection: 'banners' },
|
||||
author: { id: 'ben-holmes', collection: 'authors' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Render result`, () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
if (mode === 'prod') {
|
||||
const html = await fixture.readFile('/welcome/index.html');
|
||||
$ = cheerio.load(html);
|
||||
} else if (mode === 'dev') {
|
||||
const htmlResponse = await fixture.fetch('/welcome');
|
||||
const html = await htmlResponse.text();
|
||||
$ = cheerio.load(html);
|
||||
}
|
||||
});
|
||||
|
||||
it('Renders `banner` data', () => {
|
||||
const banner = $('img[data-banner]');
|
||||
assert.equal(banner.length, 1);
|
||||
assert.equal(banner.attr('src').includes('the-future'), true);
|
||||
assert.equal(
|
||||
banner.attr('alt'),
|
||||
'Futuristic landscape with chrome buildings and blue skies',
|
||||
);
|
||||
assert.equal(banner.attr('width'), '400');
|
||||
assert.equal(banner.attr('height'), '225');
|
||||
});
|
||||
|
||||
it('Renders `author` data', () => {
|
||||
const author = $('a[data-author-name]');
|
||||
assert.equal(author.length, 1);
|
||||
assert.equal(author.attr('href'), 'https://twitter.com/n_moore');
|
||||
assert.equal(author.text(), 'Nate Something Moore');
|
||||
});
|
||||
|
||||
it('Renders `relatedPosts` data', () => {
|
||||
const relatedPosts = $('ul[data-related-posts]');
|
||||
assert.equal(relatedPosts.length, 1);
|
||||
const relatedPost1 = relatedPosts.find('li').eq(0);
|
||||
|
||||
assert.equal(relatedPost1.find('a').attr('href'), '/blog/related-1');
|
||||
assert.equal(relatedPost1.find('a').text(), 'Related post 1');
|
||||
const relatedPost2 = relatedPosts.find('li').eq(1);
|
||||
assert.equal(relatedPost2.find('a').attr('href'), '/blog/related-2');
|
||||
assert.equal(relatedPost2.find('a').text(), 'Related post 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,137 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import { copyFiles } from '../dist/core/build/static-build.js';
|
||||
import { loadFixture } from './test-utils.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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate content', () => {
|
||||
let fixture,
|
||||
backup,
|
||||
/** @type {ManifestTestPlugin} */
|
||||
testPlugin;
|
||||
before(async () => {
|
||||
testPlugin = new ManifestTestPlugin();
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-same-contents/',
|
||||
cacheDir: './cache/same-contents/',
|
||||
experimental: { contentCollectionCache: true },
|
||||
integrations: [testPlugin.plugin()],
|
||||
});
|
||||
backup = new CacheBackup(
|
||||
'./fixtures/content-collections-same-contents/',
|
||||
'./cache/same-contents/',
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,303 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import * as cheerio from 'cheerio';
|
||||
import testAdapter from './test-adapter.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Experimental Content Collections cache inlineStylesheets', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
// inconsequential config that differs between tests
|
||||
// to bust cache and prevent modules and their state
|
||||
// from being reused
|
||||
site: 'https://test.dev/',
|
||||
root: './fixtures/css-inline-stylesheets-2/',
|
||||
output: 'static',
|
||||
build: {
|
||||
inlineStylesheets: 'never',
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Does not render any <style> tags', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal($('style').toArray().length, 0);
|
||||
});
|
||||
|
||||
describe('Inspect linked stylesheets', () => {
|
||||
// object, so it can be passed by reference
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromStaticOutput(fixture);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Experimental Content Collections cache - inlineStylesheets to never in server output', () => {
|
||||
let app;
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
// inconsequential config that differs between tests
|
||||
// to bust cache and prevent modules and their state
|
||||
// from being reused
|
||||
site: 'https://test.dev/',
|
||||
root: './fixtures/css-inline-stylesheets/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
outDir: './dist/inline-stylesheets-never',
|
||||
build: {
|
||||
inlineStylesheets: 'never',
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Does not render any <style> tags', async () => {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal($('style').toArray().length, 0);
|
||||
});
|
||||
|
||||
describe('Inspect linked stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromServer(app);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Experimental Content Collections cache - inlineStylesheets to auto in static output', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
// inconsequential config that differs between tests
|
||||
// to bust cache and prevent modules and their state
|
||||
// from being reused
|
||||
site: 'https://test.info/',
|
||||
root: './fixtures/css-inline-stylesheets/',
|
||||
output: 'static',
|
||||
outDir: './dist/inline-stylesheets-auto',
|
||||
build: {
|
||||
inlineStylesheets: 'auto',
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
assetsInlineLimit: 512,
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it(
|
||||
'Renders some <style> and some <link> tags',
|
||||
{ todo: 'Styles have the wrong length' },
|
||||
async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// the count of style/link tags depends on our css chunking logic
|
||||
// this test should be updated if it changes
|
||||
assert.equal($('style').length, 3);
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
},
|
||||
);
|
||||
|
||||
describe('Inspect linked and inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromStaticOutput(fixture);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to always in static output', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
// inconsequential config that differs between tests
|
||||
// to bust cache and prevent modules and their state
|
||||
// from being reused
|
||||
site: 'https://test.net/',
|
||||
// TODO: Uses -3 variant to bust ESM module cache when rendering the pages. Particularly in
|
||||
// `node_modules/.astro/content/entry.mjs` and `import('./en/endeavour.mjs')`. Ideally this
|
||||
// should be solved in core, but using this workaround for now.
|
||||
root: './fixtures/css-inline-stylesheets-3/',
|
||||
output: 'static',
|
||||
build: {
|
||||
inlineStylesheets: 'always',
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Does not render any <link> tags', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal($('link[rel=stylesheet]').toArray().length, 0);
|
||||
});
|
||||
|
||||
describe('Inspect inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromStaticOutput(fixture);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting inlineStylesheets to always in server output', () => {
|
||||
let app;
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
// inconsequential config that differs between tests
|
||||
// to bust cache and prevent modules and their state
|
||||
// from being reused
|
||||
site: 'https://test.net/',
|
||||
root: './fixtures/css-inline-stylesheets/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
outDir: './dist/inline-stylesheets-always',
|
||||
build: {
|
||||
inlineStylesheets: 'always',
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Does not render any <link> tags', async () => {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal($('link[rel=stylesheet]').toArray().length, 0);
|
||||
});
|
||||
|
||||
describe('Inspect inlined stylesheets', () => {
|
||||
const allStyles = {};
|
||||
|
||||
before(async () => {
|
||||
allStyles.value = await stylesFromServer(app);
|
||||
});
|
||||
|
||||
commonExpectations(allStyles);
|
||||
});
|
||||
});
|
||||
|
||||
async function stylesFromStaticOutput(fixture) {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const links = $('link[rel=stylesheet]');
|
||||
const hrefs = links.map((_, linkEl) => linkEl.attribs.href).toArray();
|
||||
const allLinkedStylesheets = await Promise.all(hrefs.map((href) => fixture.readFile(href)));
|
||||
const allLinkedStyles = allLinkedStylesheets.join('');
|
||||
|
||||
const styles = $('style');
|
||||
const allInlinedStylesheets = styles.map((_, styleEl) => styleEl.children[0].data).toArray();
|
||||
const allInlinedStyles = allInlinedStylesheets.join('');
|
||||
|
||||
return allLinkedStyles + allInlinedStyles;
|
||||
}
|
||||
|
||||
async function stylesFromServer(app) {
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const links = $('link[rel=stylesheet]');
|
||||
const hrefs = links.map((_, linkEl) => linkEl.attribs.href).toArray();
|
||||
const allLinkedStylesheets = await Promise.all(
|
||||
hrefs.map(async (href) => {
|
||||
const cssRequest = new Request(`http://example.com${href}`);
|
||||
const cssResponse = await app.render(cssRequest);
|
||||
return await cssResponse.text();
|
||||
}),
|
||||
);
|
||||
const allLinkedStyles = allLinkedStylesheets.join('');
|
||||
|
||||
const styles = $('style');
|
||||
const allInlinedStylesheets = styles.map((_, styleEl) => styleEl.children[0].data).toArray();
|
||||
const allInlinedStyles = allInlinedStylesheets.join('');
|
||||
return allLinkedStyles + allInlinedStyles;
|
||||
}
|
||||
|
||||
function commonExpectations(allStyles) {
|
||||
it.skip(
|
||||
'Includes all authored css',
|
||||
{ todo: 'Styles seem to return something different' },
|
||||
() => {
|
||||
// authored in imported.css
|
||||
assert.equal(allStyles.value.includes('.bg-lightcoral'), true);
|
||||
|
||||
// authored in index.astro
|
||||
assert.equal(allStyles.value.includes('#welcome'), true);
|
||||
|
||||
// authored in components/Button.astro
|
||||
assert.equal(allStyles.value.includes('.variant-outline'), true);
|
||||
|
||||
// authored in layouts/Layout.astro
|
||||
assert.equal(allStyles.value.includes('Menlo'), true);
|
||||
},
|
||||
);
|
||||
|
||||
it('Styles used both in content layout and directly in page are included only once', () => {
|
||||
// authored in components/Button.astro
|
||||
assert.equal(allStyles.value.match(/cubic-bezier/g).length, 1);
|
||||
});
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { isWindows, loadFixture } from './test-utils.js';
|
||||
|
||||
if (!isWindows) {
|
||||
describe('Experimental Content Collections cache - render()', () => {
|
||||
describe('Build - SSG', () => {
|
||||
/** @type {import('./test-utils.js').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content/',
|
||||
// test suite was authored when inlineStylesheets defaulted to never
|
||||
build: { inlineStylesheets: 'never' },
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fixture.clean();
|
||||
});
|
||||
|
||||
it('Includes CSS for rendered entry', async () => {
|
||||
const html = await fixture.readFile('/launch-week/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Renders content
|
||||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Includes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
});
|
||||
|
||||
it('Excludes CSS for non-rendered entries', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Excludes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 0);
|
||||
});
|
||||
|
||||
it('De-duplicates CSS used both in layout and directly in target page', async () => {
|
||||
const html = await fixture.readFile('/with-layout-prop/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const set = new Set();
|
||||
|
||||
$('link[rel=stylesheet]').each((_, linkEl) => {
|
||||
const href = linkEl.attribs.href;
|
||||
assert.equal(set.has(href), false);
|
||||
set.add(href);
|
||||
});
|
||||
|
||||
$('style').each((_, styleEl) => {
|
||||
const textContent = styleEl.children[0].data;
|
||||
assert.equal(set.has(textContent), false);
|
||||
set.add(textContent);
|
||||
});
|
||||
});
|
||||
|
||||
it('Includes component scripts for rendered entry', async () => {
|
||||
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Includes script
|
||||
assert.equal($('script[type="module"]').length, 1);
|
||||
|
||||
// Includes inline script
|
||||
assert.equal($('script[data-is-inline]').length, 1);
|
||||
});
|
||||
|
||||
it('Excludes component scripts for non-rendered entries', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const allScripts = $('head > script[type="module"]');
|
||||
|
||||
// Excludes script
|
||||
assert.notEqual(
|
||||
[...allScripts].find((script) =>
|
||||
$(script).text().includes('document.querySelector("#update-me")'),
|
||||
),
|
||||
'`WithScripts.astro` script included unexpectedly.',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('Applies MDX components export', async () => {
|
||||
const html = await fixture.readFile('/launch-week-components-export/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const h2 = $('h2');
|
||||
assert.equal(h2.length, 1);
|
||||
assert.equal(h2.attr('data-components-export-applied'), 'true');
|
||||
});
|
||||
|
||||
describe('Rebuild from cache', () => {
|
||||
before(async () => {
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Includes CSS for rendered entry', async () => {
|
||||
const html = await fixture.readFile('/launch-week/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Renders content
|
||||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Includes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
});
|
||||
|
||||
it('content folder is cleaned', async () => {
|
||||
let found = true;
|
||||
try {
|
||||
await fixture.readFile('content/manifest.json');
|
||||
} catch {
|
||||
found = false;
|
||||
}
|
||||
assert.equal(found, false, 'manifest not in dist folder');
|
||||
});
|
||||
|
||||
it('chunks folder is cleaned', async () => {
|
||||
const files = await fixture.readdir('');
|
||||
assert.equal(files.includes('chunks'), false, 'chunks folder removed');
|
||||
});
|
||||
|
||||
it('Script is built', async () => {
|
||||
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const allScripts = $('script[type="module"]');
|
||||
assert.ok(allScripts.length > 0);
|
||||
|
||||
// Includes script
|
||||
assert.notEqual(
|
||||
[...allScripts].find((script) =>
|
||||
$(script).attr('src')?.includes('/_astro/WithScripts'),
|
||||
),
|
||||
undefined,
|
||||
'Script missing.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,392 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as devalue from 'devalue';
|
||||
import testAdapter from './test-adapter.js';
|
||||
import { preventNodeBuiltinDependencyPlugin } from './test-plugins.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Experimental Content Collections cache', () => {
|
||||
describe('Query', () => {
|
||||
let fixture;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collections/',
|
||||
experimental: { contentCollectionCache: true },
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
describe('Collection', () => {
|
||||
let json;
|
||||
before(async () => {
|
||||
const rawJson = await fixture.readFile('/collections.json');
|
||||
json = devalue.parse(rawJson);
|
||||
});
|
||||
|
||||
it('Returns `without config` collection', async () => {
|
||||
assert.equal(json.hasOwnProperty('withoutConfig'), true);
|
||||
assert.equal(Array.isArray(json.withoutConfig), true);
|
||||
|
||||
const ids = json.withoutConfig.map((item) => item.id).sort();
|
||||
assert.deepEqual(
|
||||
ids,
|
||||
[
|
||||
'columbia.md',
|
||||
'endeavour.md',
|
||||
'enterprise.md',
|
||||
// Spaces allowed in IDs
|
||||
'promo/launch week.mdx',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Handles spaces in `without config` slugs', async () => {
|
||||
assert.equal(json.hasOwnProperty('withoutConfig'), true);
|
||||
assert.equal(Array.isArray(json.withoutConfig), true);
|
||||
|
||||
const slugs = json.withoutConfig.map((item) => item.slug).sort();
|
||||
assert.deepEqual(
|
||||
slugs,
|
||||
[
|
||||
'columbia',
|
||||
'endeavour',
|
||||
'enterprise',
|
||||
// "launch week.mdx" is converted to "launch-week.mdx"
|
||||
'promo/launch-week',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Returns `with schema` collection', async () => {
|
||||
assert.equal(json.hasOwnProperty('withSchemaConfig'), true);
|
||||
assert.equal(Array.isArray(json.withSchemaConfig), true);
|
||||
|
||||
const ids = json.withSchemaConfig.map((item) => item.id).sort();
|
||||
const publishedDates = json.withSchemaConfig.map((item) => item.data.publishedAt);
|
||||
|
||||
assert.deepEqual(ids, ['four%.md', 'one.md', 'three.md', 'two.md']);
|
||||
assert.equal(
|
||||
publishedDates.every((date) => date instanceof Date),
|
||||
true,
|
||||
'Not all publishedAt dates are Date objects',
|
||||
);
|
||||
assert.deepEqual(publishedDates.map((date) => date.toISOString()).sort(), [
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-01-02T00:00:00.000Z',
|
||||
'2021-01-03T00:00:00.000Z',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Returns `with custom slugs` collection', async () => {
|
||||
assert.equal(json.hasOwnProperty('withSlugConfig'), true);
|
||||
assert.equal(Array.isArray(json.withSlugConfig), true);
|
||||
|
||||
const slugs = json.withSlugConfig.map((item) => item.slug).sort();
|
||||
assert.deepEqual(slugs, ['excellent-three', 'fancy-one', 'interesting-two']);
|
||||
});
|
||||
|
||||
it('Returns `with union schema` collection', async () => {
|
||||
assert.equal(json.hasOwnProperty('withUnionSchema'), true);
|
||||
assert.equal(Array.isArray(json.withUnionSchema), true);
|
||||
|
||||
const post = json.withUnionSchema.find((item) => item.id === 'post.md');
|
||||
assert.equal(post !== undefined, true);
|
||||
assert.deepEqual(post.data, {
|
||||
type: 'post',
|
||||
title: 'My Post',
|
||||
description: 'This is my post',
|
||||
});
|
||||
const newsletter = json.withUnionSchema.find((item) => item.id === 'newsletter.md');
|
||||
assert.equal(newsletter !== undefined, true);
|
||||
assert.deepEqual(newsletter.data, {
|
||||
type: 'newsletter',
|
||||
subject: 'My Newsletter',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Entry', () => {
|
||||
let json;
|
||||
before(async () => {
|
||||
const rawJson = await fixture.readFile('/entries.json');
|
||||
json = devalue.parse(rawJson);
|
||||
});
|
||||
|
||||
it('Returns `without config` collection entry', async () => {
|
||||
assert.equal(json.hasOwnProperty('columbiaWithoutConfig'), true);
|
||||
assert.equal(json.columbiaWithoutConfig.id, 'columbia.md');
|
||||
});
|
||||
|
||||
it('Returns `with schema` collection entry', async () => {
|
||||
assert.equal(json.hasOwnProperty('oneWithSchemaConfig'), true);
|
||||
assert.equal(json.oneWithSchemaConfig.id, 'one.md');
|
||||
assert.equal(json.oneWithSchemaConfig.data.publishedAt instanceof Date, true);
|
||||
assert.equal(
|
||||
json.oneWithSchemaConfig.data.publishedAt.toISOString(),
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
it('Returns `with custom slugs` collection entry', async () => {
|
||||
assert.equal(json.hasOwnProperty('twoWithSlugConfig'), true);
|
||||
assert.equal(json.twoWithSlugConfig.slug, 'interesting-two');
|
||||
});
|
||||
|
||||
it('Returns `with union schema` collection entry', async () => {
|
||||
assert.equal(json.hasOwnProperty('postWithUnionSchema'), true);
|
||||
assert.equal(json.postWithUnionSchema.id, 'post.md');
|
||||
assert.deepEqual(json.postWithUnionSchema.data, {
|
||||
type: 'post',
|
||||
title: 'My Post',
|
||||
description: 'This is my post',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const blogSlugToContents = {
|
||||
'first-post': {
|
||||
title: 'First post',
|
||||
element: 'blockquote',
|
||||
content: 'First post loaded: yes!',
|
||||
},
|
||||
'second-post': {
|
||||
title: 'Second post',
|
||||
element: 'blockquote',
|
||||
content: 'Second post loaded: yes!',
|
||||
},
|
||||
'third-post': {
|
||||
title: 'Third post',
|
||||
element: 'blockquote',
|
||||
content: 'Third post loaded: yes!',
|
||||
},
|
||||
'using-mdx': {
|
||||
title: 'Using MDX',
|
||||
element: 'a[href="#"]',
|
||||
content: 'Embedded component in MDX',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Static paths integration', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-static-paths-integration/',
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Generates expected pages', async () => {
|
||||
for (const slug in blogSlugToContents) {
|
||||
assert.equal(fixture.pathExists(`/posts/${slug}`), true);
|
||||
}
|
||||
});
|
||||
|
||||
it('Renders titles', async () => {
|
||||
for (const slug in blogSlugToContents) {
|
||||
const post = await fixture.readFile(`/posts/${slug}/index.html`);
|
||||
const $ = cheerio.load(post);
|
||||
assert.equal($('h1').text(), blogSlugToContents[slug].title);
|
||||
}
|
||||
});
|
||||
|
||||
it('Renders content', async () => {
|
||||
for (const slug in blogSlugToContents) {
|
||||
const post = await fixture.readFile(`/posts/${slug}/index.html`);
|
||||
const $ = cheerio.load(post);
|
||||
assert.equal(
|
||||
$(blogSlugToContents[slug].element).text().trim(),
|
||||
blogSlugToContents[slug].content,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('With spaces in path', () => {
|
||||
it('Does not throw', async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/content with spaces in folder name/',
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
let error = null;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
await fixture.clean();
|
||||
}
|
||||
assert.equal(error, null);
|
||||
});
|
||||
});
|
||||
describe('With config.mjs', () => {
|
||||
it("Errors when frontmatter doesn't match schema", async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-with-config-mjs/',
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
await fixture.clean();
|
||||
}
|
||||
assert.equal(error.includes('**title**: Expected type `"string"`, received "number"'), true);
|
||||
});
|
||||
});
|
||||
describe('With config.mts', () => {
|
||||
it("Errors when frontmatter doesn't match schema", async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-with-config-mts/',
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
await fixture.clean();
|
||||
}
|
||||
assert.equal(error.includes('**title**: Expected type `"string"`, received "number"'), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With empty markdown file', () => {
|
||||
it('Throws the right error', async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-empty-md-file/',
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
await fixture.clean();
|
||||
}
|
||||
assert.equal(error.includes('**title**: Required'), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With empty collections directory', () => {
|
||||
it('Handles the empty directory correctly', async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-empty-dir/',
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
await fixture.clean();
|
||||
}
|
||||
assert.equal(error, undefined);
|
||||
// TODO: try to render a page
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR integration', () => {
|
||||
let app;
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-ssr-integration/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
vite: {
|
||||
plugins: [preventNodeBuiltinDependencyPlugin()],
|
||||
},
|
||||
experimental: {
|
||||
contentCollectionCache: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Responds 200 for expected pages', async () => {
|
||||
for (const slug in blogSlugToContents) {
|
||||
const request = new Request('http://example.com/posts/' + slug);
|
||||
const response = await app.render(request);
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
});
|
||||
|
||||
it('Renders titles', async () => {
|
||||
for (const slug in blogSlugToContents) {
|
||||
const request = new Request('http://example.com/posts/' + slug);
|
||||
const response = await app.render(request);
|
||||
const body = await response.text();
|
||||
const $ = cheerio.load(body);
|
||||
assert.equal($('h1').text(), blogSlugToContents[slug].title);
|
||||
}
|
||||
});
|
||||
|
||||
it('Renders content', async () => {
|
||||
for (const slug in blogSlugToContents) {
|
||||
const request = new Request('http://example.com/posts/' + slug);
|
||||
const response = await app.render(request);
|
||||
const body = await response.text();
|
||||
const $ = cheerio.load(body);
|
||||
assert.equal(
|
||||
$(blogSlugToContents[slug].element).text().trim(),
|
||||
blogSlugToContents[slug].content,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Base configuration', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-base/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(async () => await fixture.clean());
|
||||
|
||||
it('Includes base in links', async () => {
|
||||
const html = await fixture.readFile('/docs/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
assert.equal($('link').attr('href').startsWith('/docs'), true);
|
||||
});
|
||||
|
||||
it('Includes base in scripts', async () => {
|
||||
const html = await fixture.readFile('/docs/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
assert.equal($('script').attr('src').startsWith('/docs'), true);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue