mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
Implement legacy collections using glob (#11976)
* feat: support pattern arrays with glob * wip * feat: emulate legacy content collections * Fixes * Lint * Correctly handle legacy data * Fix tests * Switch flag handling * Fix warnings * Add layout warning * Update fixtures * More tests! * Handle empty md files * Lockfile * Dedupe name * Handle data ID unslug * Fix e2e * Clean build * Clean builds in tests * Test fixes * Fix test * Fix typegen * Fix tests * Fixture updates * Test updates * Update changeset * Test * Remove wait in test * Handle race condition * Lock * chore: changes from review * Handle folders without config * lint * Fix test * Update wording for auto-collections * Delete legacyId * Sort another fixture * Rename flag to `legacy.collections` * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Changes from review * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * lockfile * lock --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
953e6e0f23
commit
abf9a89ac1
117 changed files with 2172 additions and 511 deletions
47
.changeset/quick-onions-leave.md
Normal file
47
.changeset/quick-onions-leave.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
'astro': major
|
||||
---
|
||||
|
||||
Refactors legacy `content` and `data` collections to use the Content Layer API `glob()` loader for better performance and to support backwards compatibility. Also introduces the `legacy.collections` flag for projects that are unable to update to the new behavior immediately.
|
||||
|
||||
:warning: **BREAKING CHANGE FOR LEGACY CONTENT COLLECTIONS** :warning:
|
||||
|
||||
By default, collections that use the old types (`content` or `data`) and do not define a `loader` are now implemented under the hood using the Content Layer API's built-in `glob()` loader, with extra backward-compatibility handling.
|
||||
|
||||
In order to achieve backwards compatibility with existing `content` collections, the following have been implemented:
|
||||
|
||||
- a `glob` loader collection is defined, with patterns that match the previous handling (matches `src/content/<collection name>/**/*.md` and other content extensions depending on installed integrations, with underscore-prefixed files and folders ignored)
|
||||
- When used in the runtime, the entries have an ID based on the filename in the same format as legacy collections
|
||||
- A `slug` field is added with the same format as before
|
||||
- A `render()` method is added to the entry, so they can be called using `entry.render()`
|
||||
- `getEntryBySlug` is supported
|
||||
|
||||
In order to achieve backwards compatibility with existing `data` collections, the following have been implemented:
|
||||
|
||||
- a `glob` loader collection is defined, with patterns that match the previous handling (matches `src/content/<collection name>/**/*{.json,.yaml}` and other data extensions, with underscore-prefixed files and folders ignored)
|
||||
- Entries have an ID that is not slugified
|
||||
- `getDataEntryById` is supported
|
||||
|
||||
While this backwards compatibility implementation is able to emulate most of the features of legacy collections, **there are some differences and limitations that may cause breaking changes to existing collections**:
|
||||
|
||||
- In previous versions of Astro, collections would be generated for all folders in `src/content/`, even if they were not defined in `src/content/config.ts`. This behavior is now deprecated, and collections should always be defined in `src/content/config.ts`. For existing collections, these can just be empty declarations (e.g. `const blog = defineCollection({})`) and Astro will implicitly define your legacy collection for you in a way that is compatible with the new loading behavior.
|
||||
- The special `layout` field is not supported in Markdown collection entries. This property is intended only for standalone page files located in `src/pages/` and not likely to be in your collection entries. However, if you were using this property, you must now create dynamic routes that include your page styling.
|
||||
- Sort order of generated collections is non-deterministic and platform-dependent. This means that if you are calling `getCollection()`, the order in which entries are returned may be different than before. If you need a specific order, you should sort the collection entries yourself.
|
||||
- `image().refine()` is not supported. If you need to validate the properties of an image you will need to do this at runtime in your page or component.
|
||||
- the `key` argument of `getEntry(collection, key)` is typed as `string`, rather than having types for every entry.
|
||||
|
||||
A new legacy configuration flag `legacy.collections` is added for users that want to keep their current legacy (content and data) collections behavior (available in Astro v2 - v4), or who are not yet ready to update their projects:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
collections: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
When set, no changes to your existing collections are necessary, and the restrictions on storing both new and old collections continue to exist: legacy collections (only) must continue to remain in `src/content/`, while new collections using a loader from the Content Layer API are forbidden in that folder.
|
||||
|
5
examples/with-markdoc/src/content/config.ts
Normal file
5
examples/with-markdoc/src/content/config.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { defineCollection } from 'astro:content';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({})
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { defineCollection } from "astro:content";
|
||||
|
||||
|
||||
const posts = defineCollection({});
|
||||
|
||||
export const collections = { posts };
|
|
@ -34,6 +34,8 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
|
|||
*/
|
||||
deferredRender?: boolean;
|
||||
assetImports?: Array<string>;
|
||||
/** @deprecated */
|
||||
legacyId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,7 +35,7 @@ function generateIdDefault({ entry, base, data }: GenerateIdOptions): string {
|
|||
if (data.slug) {
|
||||
return data.slug as string;
|
||||
}
|
||||
const entryURL = new URL(entry, base);
|
||||
const entryURL = new URL(encodeURI(entry), base);
|
||||
const { slug } = getContentEntryIdAndSlug({
|
||||
entry: entryURL,
|
||||
contentDir: base,
|
||||
|
@ -55,6 +55,15 @@ function checkPrefix(pattern: string | Array<string>, prefix: string) {
|
|||
* Loads multiple entries, using a glob pattern to match files.
|
||||
* @param pattern A glob pattern to match files, relative to the content directory.
|
||||
*/
|
||||
export function glob(globOptions: GlobOptions): Loader;
|
||||
/** @private */
|
||||
export function glob(
|
||||
globOptions: GlobOptions & {
|
||||
/** @deprecated */
|
||||
_legacy?: true;
|
||||
},
|
||||
): Loader;
|
||||
|
||||
export function glob(globOptions: GlobOptions): Loader {
|
||||
if (checkPrefix(globOptions.pattern, '../')) {
|
||||
throw new Error(
|
||||
|
@ -80,19 +89,21 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
>();
|
||||
|
||||
const untouchedEntries = new Set(store.keys());
|
||||
|
||||
const isLegacy = (globOptions as any)._legacy;
|
||||
// If global legacy collection handling flag is *not* enabled then this loader is used to emulate them instead
|
||||
const emulateLegacyCollections = !config.legacy.collections;
|
||||
async function syncData(entry: string, base: URL, entryType?: ContentEntryType) {
|
||||
if (!entryType) {
|
||||
logger.warn(`No entry type found for ${entry}`);
|
||||
return;
|
||||
}
|
||||
const fileUrl = new URL(entry, base);
|
||||
const fileUrl = new URL(encodeURI(entry), base);
|
||||
const contents = await fs.readFile(fileUrl, 'utf-8').catch((err) => {
|
||||
logger.error(`Error reading ${entry}: ${err.message}`);
|
||||
return;
|
||||
});
|
||||
|
||||
if (!contents) {
|
||||
if (!contents && contents !== '') {
|
||||
logger.warn(`No contents found for ${entry}`);
|
||||
return;
|
||||
}
|
||||
|
@ -103,6 +114,17 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
});
|
||||
|
||||
const id = generateId({ entry, base, data });
|
||||
let legacyId: string | undefined;
|
||||
|
||||
if (isLegacy) {
|
||||
const entryURL = new URL(encodeURI(entry), base);
|
||||
const legacyOptions = getContentEntryIdAndSlug({
|
||||
entry: entryURL,
|
||||
contentDir: base,
|
||||
collection: '',
|
||||
});
|
||||
legacyId = legacyOptions.id;
|
||||
}
|
||||
untouchedEntries.delete(id);
|
||||
|
||||
const existingEntry = store.get(id);
|
||||
|
@ -132,6 +154,12 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
filePath,
|
||||
});
|
||||
if (entryType.getRenderFunction) {
|
||||
if (isLegacy && data.layout) {
|
||||
logger.error(
|
||||
`The Markdown "layout" field is not supported in content collections in Astro 5. Ignoring layout for ${JSON.stringify(entry)}. Enable "legacy.collections" if you need to use the layout field.`,
|
||||
);
|
||||
}
|
||||
|
||||
let render = renderFunctionByContentType.get(entryType);
|
||||
if (!render) {
|
||||
render = await entryType.getRenderFunction(config);
|
||||
|
@ -160,6 +188,7 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
digest,
|
||||
rendered,
|
||||
assetImports: rendered?.metadata?.imagePaths,
|
||||
legacyId,
|
||||
});
|
||||
|
||||
// todo: add an explicit way to opt in to deferred rendering
|
||||
|
@ -171,9 +200,10 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
filePath: relativePath,
|
||||
digest,
|
||||
deferredRender: true,
|
||||
legacyId,
|
||||
});
|
||||
} else {
|
||||
store.set({ id, data: parsedData, body, filePath: relativePath, digest });
|
||||
store.set({ id, data: parsedData, body, filePath: relativePath, digest, legacyId });
|
||||
}
|
||||
|
||||
fileToIdMap.set(filePath, id);
|
||||
|
@ -222,7 +252,7 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
if (isConfigFile(entry)) {
|
||||
return;
|
||||
}
|
||||
if (isInContentDir(entry)) {
|
||||
if (!emulateLegacyCollections && isInContentDir(entry)) {
|
||||
skippedFiles.push(entry);
|
||||
return;
|
||||
}
|
||||
|
@ -240,7 +270,9 @@ export function glob(globOptions: GlobOptions): Loader {
|
|||
? globOptions.pattern.join(', ')
|
||||
: globOptions.pattern;
|
||||
|
||||
logger.warn(`The glob() loader cannot be used for files in ${bold('src/content')}.`);
|
||||
logger.warn(
|
||||
`The glob() loader cannot be used for files in ${bold('src/content')} when legacy mode is enabled.`,
|
||||
);
|
||||
if (skipCount > 10) {
|
||||
logger.warn(
|
||||
`Skipped ${green(skippedFiles.length)} files that matched ${green(patternList)}.`,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Traverse } from 'neotraverse/modern';
|
|||
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { IMAGE_IMPORT_PREFIX } from './consts.js';
|
||||
import { type DataEntry, ImmutableDataStore, type RenderedContent } from './data-store.js';
|
||||
import { type DataEntry, ImmutableDataStore } from './data-store.js';
|
||||
import { contentModuleToId } from './utils.js';
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 500;
|
||||
|
@ -197,7 +197,17 @@ export default new Map([\n${lines.join(',\n')}]);
|
|||
entries: () => this.entries(collectionName),
|
||||
values: () => this.values(collectionName),
|
||||
keys: () => this.keys(collectionName),
|
||||
set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
|
||||
set: ({
|
||||
id: key,
|
||||
data,
|
||||
body,
|
||||
filePath,
|
||||
deferredRender,
|
||||
digest,
|
||||
rendered,
|
||||
assetImports,
|
||||
legacyId,
|
||||
}) => {
|
||||
if (!key) {
|
||||
throw new Error(`ID must be a non-empty string`);
|
||||
}
|
||||
|
@ -244,6 +254,9 @@ export default new Map([\n${lines.join(',\n')}]);
|
|||
if (rendered) {
|
||||
entry.rendered = rendered;
|
||||
}
|
||||
if (legacyId) {
|
||||
entry.legacyId = legacyId;
|
||||
}
|
||||
if (deferredRender) {
|
||||
entry.deferredRender = deferredRender;
|
||||
if (filePath) {
|
||||
|
@ -335,30 +348,7 @@ export interface DataStore {
|
|||
key: string,
|
||||
) => DataEntry<TData> | undefined;
|
||||
entries: () => Array<[id: string, DataEntry]>;
|
||||
set: <TData extends Record<string, unknown>>(opts: {
|
||||
/** The ID of the entry. Must be unique per collection. */
|
||||
id: string;
|
||||
/** The data to store. */
|
||||
data: TData;
|
||||
/** The raw body of the content, if applicable. */
|
||||
body?: string;
|
||||
/** The file path of the content, if applicable. Relative to the site root. */
|
||||
filePath?: string;
|
||||
/** A content digest, to check if the content has changed. */
|
||||
digest?: number | string;
|
||||
/** The rendered content, if applicable. */
|
||||
rendered?: RenderedContent;
|
||||
/**
|
||||
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase.
|
||||
*/
|
||||
deferredRender?: boolean;
|
||||
/**
|
||||
* Assets such as images to process during the build. These should be files on disk, with a path relative to filePath.
|
||||
* Any values that use image() in the schema will already be added automatically.
|
||||
* @internal
|
||||
*/
|
||||
assetImports?: Array<string>;
|
||||
}) => boolean;
|
||||
set: <TData extends Record<string, unknown>>(opts: DataEntry<TData>) => boolean;
|
||||
values: () => Array<DataEntry>;
|
||||
keys: () => Array<string>;
|
||||
delete: (key: string) => void;
|
||||
|
|
|
@ -94,7 +94,7 @@ export function createGetCollection({
|
|||
if (hasFilter && !filter(entry)) {
|
||||
continue;
|
||||
}
|
||||
result.push(entry);
|
||||
result.push(entry.legacyId ? emulateLegacyEntry(entry) : entry);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
|
@ -162,23 +162,31 @@ export function createGetEntryBySlug({
|
|||
getEntryImport,
|
||||
getRenderEntryImport,
|
||||
collectionNames,
|
||||
getEntry,
|
||||
}: {
|
||||
getEntryImport: GetEntryImport;
|
||||
getRenderEntryImport: GetEntryImport;
|
||||
collectionNames: Set<string>;
|
||||
getEntry: ReturnType<typeof createGetEntry>;
|
||||
}) {
|
||||
return async function getEntryBySlug(collection: string, slug: string) {
|
||||
const store = await globalDataStore.get();
|
||||
|
||||
if (!collectionNames.has(collection)) {
|
||||
if (store.hasCollection(collection)) {
|
||||
const entry = await getEntry(collection, slug);
|
||||
if (entry && 'slug' in entry) {
|
||||
return entry;
|
||||
}
|
||||
throw new AstroError({
|
||||
...AstroErrorData.GetEntryDeprecationError,
|
||||
message: AstroErrorData.GetEntryDeprecationError.message(collection, 'getEntryBySlug'),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
|
||||
console.warn(
|
||||
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -207,22 +215,23 @@ export function createGetEntryBySlug({
|
|||
export function createGetDataEntryById({
|
||||
getEntryImport,
|
||||
collectionNames,
|
||||
getEntry,
|
||||
}: {
|
||||
getEntryImport: GetEntryImport;
|
||||
collectionNames: Set<string>;
|
||||
getEntry: ReturnType<typeof createGetEntry>;
|
||||
}) {
|
||||
return async function getDataEntryById(collection: string, id: string) {
|
||||
const store = await globalDataStore.get();
|
||||
|
||||
if (!collectionNames.has(collection)) {
|
||||
if (store.hasCollection(collection)) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.GetEntryDeprecationError,
|
||||
message: AstroErrorData.GetEntryDeprecationError.message(collection, 'getDataEntryById'),
|
||||
});
|
||||
return getEntry(collection, id);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
|
||||
console.warn(
|
||||
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -256,6 +265,21 @@ type DataEntryResult = {
|
|||
|
||||
type EntryLookupObject = { collection: string; id: string } | { collection: string; slug: string };
|
||||
|
||||
function emulateLegacyEntry(entry: DataEntry) {
|
||||
// Define this first so it's in scope for the render function
|
||||
const legacyEntry = {
|
||||
...entry,
|
||||
id: entry.legacyId!,
|
||||
slug: entry.id,
|
||||
};
|
||||
delete legacyEntry.legacyId;
|
||||
return {
|
||||
...legacyEntry,
|
||||
// Define separately so the render function isn't included in the object passed to `renderEntry()`
|
||||
render: () => renderEntry(legacyEntry),
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetEntry({
|
||||
getEntryImport,
|
||||
getRenderEntryImport,
|
||||
|
@ -303,6 +327,9 @@ export function createGetEntry({
|
|||
// @ts-expect-error virtual module
|
||||
const { default: imageAssetMap } = await import('astro:asset-imports');
|
||||
entry.data = updateImageReferencesInData(entry.data, entry.filePath, imageAssetMap);
|
||||
if (entry.legacyId) {
|
||||
return { ...emulateLegacyEntry(entry), collection } as ContentEntryResult;
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
collection,
|
||||
|
@ -311,7 +338,9 @@ export function createGetEntry({
|
|||
|
||||
if (!collectionNames.has(collection)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
|
||||
console.warn(
|
||||
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -433,13 +462,16 @@ function updateImageReferencesInData<T extends Record<string, unknown>>(
|
|||
}
|
||||
|
||||
export async function renderEntry(
|
||||
entry: DataEntry | { render: () => Promise<{ Content: AstroComponentFactory }> },
|
||||
entry:
|
||||
| DataEntry
|
||||
| { render: () => Promise<{ Content: AstroComponentFactory }> }
|
||||
| (DataEntry & { render: () => Promise<{ Content: AstroComponentFactory }> }),
|
||||
) {
|
||||
if (!entry) {
|
||||
throw new AstroError(AstroErrorData.RenderUndefinedEntryError);
|
||||
}
|
||||
|
||||
if ('render' in entry) {
|
||||
if ('render' in entry && !('legacyId' in entry)) {
|
||||
// This is an old content collection entry, so we use its render method
|
||||
return entry.render();
|
||||
}
|
||||
|
@ -619,6 +651,7 @@ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap })
|
|||
}
|
||||
return { id: lookup, collection };
|
||||
}
|
||||
|
||||
// If the collection is not in the lookup map or store, it may be a content layer collection and the store may not yet be populated.
|
||||
// If the store has 0 or 1 entries it probably means that the entries have not yet been loaded.
|
||||
// The store may have a single entry even if the collections have not loaded, because the top-level metadata collection is generated early.
|
||||
|
@ -627,7 +660,6 @@ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap })
|
|||
// later in the pipeline when we do have access to the store.
|
||||
return { id: lookup, collection };
|
||||
}
|
||||
|
||||
const { type, entries } = lookupMap[collection];
|
||||
const entry = entries[lookup];
|
||||
|
||||
|
|
|
@ -501,7 +501,10 @@ async function writeContentFiles({
|
|||
contentTypesStr += `};\n`;
|
||||
break;
|
||||
case CONTENT_LAYER_TYPE:
|
||||
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n collection: ${collectionKey};\n data: ${dataType};\n rendered?: RenderedContent;\n filePath?: string;\n body?: string \n}>;\n`;
|
||||
const legacyTypes = (collectionConfig as any)?._legacy
|
||||
? 'render(): Render[".md"];\n slug: string;\n body: string;\n'
|
||||
: 'body?: string;\n';
|
||||
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n ${legacyTypes} collection: ${collectionKey};\n data: ${dataType};\n rendered?: RenderedContent;\n filePath?: string;\n}>;\n`;
|
||||
break;
|
||||
case 'data':
|
||||
if (collectionEntryKeys.length === 0) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { z } from 'zod';
|
|||
import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js';
|
||||
import { isYAMLException } from '../core/errors/utils.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
import { appendForwardSlash } from '../core/path.js';
|
||||
import { normalizePath } from '../core/viteUtils.js';
|
||||
import type { AstroSettings } from '../types/astro.js';
|
||||
import type { AstroConfig } from '../types/public/config.js';
|
||||
|
@ -22,7 +23,9 @@ import {
|
|||
IMAGE_IMPORT_PREFIX,
|
||||
PROPAGATED_ASSET_FLAG,
|
||||
} from './consts.js';
|
||||
import { glob } from './loaders/glob.js';
|
||||
import { createImage } from './runtime-assets.js';
|
||||
import { green } from 'kleur/colors';
|
||||
/**
|
||||
* Amap from a collection + slug to the local file path.
|
||||
* This is used internally to resolve entry imports when using `getEntry()`.
|
||||
|
@ -114,6 +117,8 @@ const collectionConfigParser = z.union([
|
|||
render: z.function(z.tuple([z.any()], z.unknown())).optional(),
|
||||
}),
|
||||
]),
|
||||
/** deprecated */
|
||||
_legacy: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -162,7 +167,7 @@ export async function getEntryDataAndImages<
|
|||
pluginContext?: PluginContext,
|
||||
): Promise<{ data: TOutputData; imageImports: Array<string> }> {
|
||||
let data: TOutputData;
|
||||
if (collectionConfig.type === 'data' || collectionConfig.type === CONTENT_LAYER_TYPE) {
|
||||
if (collectionConfig.type === 'data') {
|
||||
data = entry.unvalidatedData as TOutputData;
|
||||
} else {
|
||||
const { slug, ...unvalidatedData } = entry.unvalidatedData;
|
||||
|
@ -536,6 +541,97 @@ async function loadContentConfig({
|
|||
}
|
||||
}
|
||||
|
||||
export async function autogenerateCollections({
|
||||
config,
|
||||
settings,
|
||||
fs,
|
||||
}: {
|
||||
config?: ContentConfig;
|
||||
settings: AstroSettings;
|
||||
fs: typeof fsMod;
|
||||
}): Promise<ContentConfig | undefined> {
|
||||
if (settings.config.legacy.collections) {
|
||||
return config;
|
||||
}
|
||||
const contentDir = new URL('./content/', settings.config.srcDir);
|
||||
|
||||
const collections: Record<string, CollectionConfig> = config?.collections ?? {};
|
||||
|
||||
const contentExts = getContentEntryExts(settings);
|
||||
const dataExts = getDataEntryExts(settings);
|
||||
|
||||
const contentPattern = globWithUnderscoresIgnored('', contentExts);
|
||||
const dataPattern = globWithUnderscoresIgnored('', dataExts);
|
||||
let usesContentLayer = false;
|
||||
for (const collectionName of Object.keys(collections)) {
|
||||
if (collections[collectionName]?.type === 'content_layer') {
|
||||
usesContentLayer = true;
|
||||
// This is already a content layer, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
const isDataCollection = collections[collectionName]?.type === 'data';
|
||||
const base = new URL(`${collectionName}/`, contentDir);
|
||||
// Only "content" collections need special legacy handling
|
||||
const _legacy = !isDataCollection || undefined;
|
||||
collections[collectionName] = {
|
||||
...collections[collectionName],
|
||||
type: 'content_layer',
|
||||
_legacy,
|
||||
loader: glob({
|
||||
base,
|
||||
pattern: isDataCollection ? dataPattern : contentPattern,
|
||||
_legacy,
|
||||
// Legacy data collections IDs aren't slugified
|
||||
generateId: isDataCollection
|
||||
? ({ entry }) =>
|
||||
getDataEntryId({
|
||||
entry: new URL(entry, base),
|
||||
collection: collectionName,
|
||||
contentDir,
|
||||
})
|
||||
: undefined,
|
||||
|
||||
// Zod weirdness has trouble with typing the args to the load function
|
||||
}) as any,
|
||||
};
|
||||
}
|
||||
if (!usesContentLayer) {
|
||||
// If the user hasn't defined any collections using the content layer, we'll try and help out by checking for
|
||||
// any orphaned folders in the content directory and creating collections for them.
|
||||
const orphanedCollections = [];
|
||||
for (const entry of await fs.promises.readdir(contentDir, { withFileTypes: true })) {
|
||||
const collectionName = entry.name;
|
||||
if (['_', '.'].includes(collectionName.at(0) ?? '')) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory() && !(collectionName in collections)) {
|
||||
orphanedCollections.push(collectionName);
|
||||
const base = new URL(`${collectionName}/`, contentDir);
|
||||
collections[collectionName] = {
|
||||
type: 'content_layer',
|
||||
loader: glob({
|
||||
base,
|
||||
pattern: contentPattern,
|
||||
_legacy: true,
|
||||
}) as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (orphanedCollections.length > 0) {
|
||||
console.warn(
|
||||
`
|
||||
Auto-generating collections for folders in "src/content/" that are not defined as collections.
|
||||
This is deprecated, so you should define these collections yourself in "src/content/config.ts".
|
||||
The following collections have been auto-generated: ${orphanedCollections
|
||||
.map((name) => green(name))
|
||||
.join(', ')}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { ...config, collections };
|
||||
}
|
||||
|
||||
export async function reloadContentConfigObserver({
|
||||
observer = globalContentConfigObserver,
|
||||
...loadContentConfigOpts
|
||||
|
@ -547,7 +643,13 @@ export async function reloadContentConfigObserver({
|
|||
}) {
|
||||
observer.set({ status: 'loading' });
|
||||
try {
|
||||
const config = await loadContentConfig(loadContentConfigOpts);
|
||||
let config = await loadContentConfig(loadContentConfigOpts);
|
||||
|
||||
config = await autogenerateCollections({
|
||||
config,
|
||||
...loadContentConfigOpts,
|
||||
});
|
||||
|
||||
if (config) {
|
||||
observer.set({ status: 'loaded', config });
|
||||
} else {
|
||||
|
@ -685,6 +787,16 @@ export function hasAssetPropagationFlag(id: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): string[] {
|
||||
const extGlob = getExtGlob(exts);
|
||||
const contentDir = relContentDir.length > 0 ? appendForwardSlash(relContentDir) : relContentDir;
|
||||
return [
|
||||
`${contentDir}**/*${extGlob}`,
|
||||
`!${contentDir}**/_*/**/*${extGlob}`,
|
||||
`!${contentDir}**/_*${extGlob}`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a platform path to a posix path.
|
||||
*/
|
||||
|
|
|
@ -6,7 +6,6 @@ import glob from 'fast-glob';
|
|||
import pLimit from 'p-limit';
|
||||
import type { Plugin } from 'vite';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.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';
|
||||
|
@ -38,6 +37,7 @@ import {
|
|||
getEntrySlug,
|
||||
getEntryType,
|
||||
getExtGlob,
|
||||
globWithUnderscoresIgnored,
|
||||
isDeferredModule,
|
||||
} from './utils.js';
|
||||
|
||||
|
@ -98,10 +98,12 @@ export function astroContentVirtualModPlugin({
|
|||
},
|
||||
async load(id, args) {
|
||||
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
||||
const lookupMap = await generateLookupMap({
|
||||
settings,
|
||||
fs,
|
||||
});
|
||||
const lookupMap = settings.config.legacy.collections
|
||||
? await generateLookupMap({
|
||||
settings,
|
||||
fs,
|
||||
})
|
||||
: {};
|
||||
const isClient = !args?.ssr;
|
||||
const code = await generateContentEntryFile({
|
||||
settings,
|
||||
|
@ -201,26 +203,28 @@ export async function generateContentEntryFile({
|
|||
const contentPaths = getContentPaths(settings.config);
|
||||
const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);
|
||||
|
||||
let contentEntryGlobResult: string;
|
||||
let dataEntryGlobResult: string;
|
||||
let renderEntryGlobResult: string;
|
||||
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 contentEntryGlobResult = '""';
|
||||
let dataEntryGlobResult = '""';
|
||||
let renderEntryGlobResult = '""';
|
||||
if (settings.config.legacy.collections) {
|
||||
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) {
|
||||
|
@ -354,16 +358,6 @@ export async function generateLookupMap({
|
|||
return lookupMap;
|
||||
}
|
||||
|
||||
function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): string[] {
|
||||
const extGlob = getExtGlob(exts);
|
||||
const contentDir = appendForwardSlash(relContentDir);
|
||||
return [
|
||||
`${contentDir}**/*${extGlob}`,
|
||||
`!${contentDir}**/_*/**/*${extGlob}`,
|
||||
`!${contentDir}**/_*${extGlob}`,
|
||||
];
|
||||
}
|
||||
|
||||
const UnexpectedLookupMapError = new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `Unexpected error while parsing content entry IDs and slugs.`,
|
||||
|
|
|
@ -80,7 +80,9 @@ export const ASTRO_CONFIG_DEFAULTS = {
|
|||
integrations: [],
|
||||
markdown: markdownConfigDefaults,
|
||||
vite: {},
|
||||
legacy: {},
|
||||
legacy: {
|
||||
collections: false,
|
||||
},
|
||||
redirects: {},
|
||||
security: {
|
||||
checkOrigin: true,
|
||||
|
@ -522,7 +524,14 @@ export const AstroConfigSchema = z.object({
|
|||
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`,
|
||||
)
|
||||
.default({}),
|
||||
legacy: z.object({}).default({}),
|
||||
legacy: z
|
||||
.object({
|
||||
collections: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.legacy.collections),
|
||||
})
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export type AstroConfigType = z.infer<typeof AstroConfigSchema>;
|
||||
|
|
|
@ -1541,7 +1541,49 @@ export interface AstroUserConfig {
|
|||
* These flags allow you to opt in to some deprecated or otherwise outdated behavior of Astro
|
||||
* in the latest version, so that you can continue to upgrade and take advantage of new Astro releases.
|
||||
*/
|
||||
legacy?: object;
|
||||
legacy?: {
|
||||
/**
|
||||
* @docs
|
||||
* @name legacy.collections
|
||||
* @type {boolean}
|
||||
* @default `false`
|
||||
* @version 5.0.0
|
||||
* @description
|
||||
* Enable legacy behavior for content collections.
|
||||
*
|
||||
* ```js
|
||||
* // astro.config.mjs
|
||||
* import { defineConfig } from 'astro/config';
|
||||
* export default defineConfig({
|
||||
* legacy: {
|
||||
* collections: true
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If enabled, `data` and `content` collections (only) are handled using the legacy content collections implementation. Collections with a `loader` (only) will continue to use the Content Layer API instead. Both kinds of collections may exist in the same project, each using their respective implementations.
|
||||
*
|
||||
* The following limitations continue to exist:
|
||||
*
|
||||
* - Any legacy (`type: 'content'` or `type: 'data'`) collections must continue to be located in the `src/content/` directory.
|
||||
* - These legacy collections will not be transformed to implicitly use the `glob()` loader, and will instead be handled by legacy code.
|
||||
* - Collections using the Content Layer API (with a `loader` defined) are forbidden in `src/content/`, but may exist anywhere else in your project.
|
||||
*
|
||||
* When you are ready to remove this flag and migrate to the new Content Layer API for your legacy collections, you must define a collection for any directories in `src/content/` that you want to continue to use as a collection. It is sufficient to declare an empty collection, and Astro will implicitly generate an appropriate definition for your legacy collections:
|
||||
*
|
||||
* ```js
|
||||
* // src/content/config.ts
|
||||
* import { defineCollection, z } from 'astro:content';
|
||||
*
|
||||
* const blog = defineCollection({ })
|
||||
*
|
||||
* export const collections = { blog };
|
||||
* ```
|
||||
*
|
||||
|
||||
*/
|
||||
collections?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @docs
|
||||
|
|
|
@ -58,23 +58,25 @@ export const getCollection = createGetCollection({
|
|||
cacheEntriesByCollection,
|
||||
});
|
||||
|
||||
export const getEntryBySlug = createGetEntryBySlug({
|
||||
getEntryImport: createGlobLookup(contentCollectionToEntryMap),
|
||||
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
|
||||
collectionNames,
|
||||
});
|
||||
|
||||
export const getDataEntryById = createGetDataEntryById({
|
||||
getEntryImport: createGlobLookup(dataCollectionToEntryMap),
|
||||
collectionNames,
|
||||
});
|
||||
|
||||
export const getEntry = createGetEntry({
|
||||
getEntryImport: createGlobLookup(collectionToEntryMap),
|
||||
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
|
||||
collectionNames,
|
||||
});
|
||||
|
||||
export const getEntryBySlug = createGetEntryBySlug({
|
||||
getEntryImport: createGlobLookup(contentCollectionToEntryMap),
|
||||
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
|
||||
collectionNames,
|
||||
getEntry,
|
||||
});
|
||||
|
||||
export const getDataEntryById = createGetDataEntryById({
|
||||
getEntryImport: createGlobLookup(dataCollectionToEntryMap),
|
||||
collectionNames,
|
||||
getEntry,
|
||||
});
|
||||
|
||||
export const getEntries = createGetEntries(getEntry);
|
||||
|
||||
export const reference = createReference({ lookupMap });
|
||||
|
|
|
@ -142,20 +142,24 @@ describe('astro sync', () => {
|
|||
'.astro/content.d.ts',
|
||||
`"blog": Record<string, {
|
||||
id: string;
|
||||
render(): Render[".md"];
|
||||
slug: string;
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">;
|
||||
render(): Render[".md"];
|
||||
}>;`,
|
||||
rendered?: RenderedContent;
|
||||
filePath?: string;`,
|
||||
'Types file does not include empty collection type',
|
||||
);
|
||||
fixture.thenFileContentShouldInclude(
|
||||
'.astro/content.d.ts',
|
||||
`"blogMeta": Record<string, {
|
||||
id: string;
|
||||
body?: string;
|
||||
collection: "blogMeta";
|
||||
data: InferEntrySchema<"blogMeta">;
|
||||
rendered?: RenderedContent;
|
||||
filePath?: string;
|
||||
}>;`,
|
||||
'Types file does not include empty collection type',
|
||||
);
|
||||
|
|
|
@ -16,9 +16,12 @@ describe('Content Collections - references', () => {
|
|||
describe(mode, () => {
|
||||
before(async () => {
|
||||
if (mode === 'prod') {
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
} else if (mode === 'dev') {
|
||||
devServer = await fixture.startDevServer();
|
||||
devServer = await fixture.startDevServer({ force: true });
|
||||
await fixture.onNextDataStoreChange(1000).catch(() => {
|
||||
// Ignore timeout, because it may have saved before we get here.
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -65,13 +68,9 @@ describe('Content Collections - references', () => {
|
|||
it('Returns `author` data', () => {
|
||||
const { author } = json;
|
||||
assert.ok(author.hasOwnProperty('data'));
|
||||
assert.deepEqual(author, {
|
||||
id: 'nate-moore',
|
||||
collection: 'authors',
|
||||
data: {
|
||||
name: 'Nate Something Moore',
|
||||
twitter: 'https://twitter.com/n_moore',
|
||||
},
|
||||
assert.deepEqual(author.data, {
|
||||
name: 'Nate Something Moore',
|
||||
twitter: 'https://twitter.com/n_moore',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -82,20 +81,23 @@ describe('Content Collections - references', () => {
|
|||
...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',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
topLevelInfo.map(({ id, slug, body, collection }) => ({ id, slug, body, collection })),
|
||||
[
|
||||
{
|
||||
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, [
|
||||
{
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('Content Collections', () => {
|
|||
let fixture;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/content-collections/' });
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
});
|
||||
|
||||
describe('Collection', () => {
|
||||
|
@ -26,13 +26,16 @@ describe('Content Collections', () => {
|
|||
assert.equal(Array.isArray(json.withoutConfig), true);
|
||||
|
||||
const ids = json.withoutConfig.map((item) => item.id);
|
||||
assert.deepEqual(ids, [
|
||||
'columbia.md',
|
||||
'endeavour.md',
|
||||
'enterprise.md',
|
||||
// Spaces allowed in IDs
|
||||
'promo/launch week.mdx',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
ids.sort(),
|
||||
[
|
||||
'columbia.md',
|
||||
'endeavour.md',
|
||||
'enterprise.md',
|
||||
// Spaces allowed in IDs
|
||||
'promo/launch week.mdx',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Handles spaces in `without config` slugs', async () => {
|
||||
|
@ -40,13 +43,16 @@ describe('Content Collections', () => {
|
|||
assert.equal(Array.isArray(json.withoutConfig), true);
|
||||
|
||||
const slugs = json.withoutConfig.map((item) => item.slug);
|
||||
assert.deepEqual(slugs, [
|
||||
'columbia',
|
||||
'endeavour',
|
||||
'enterprise',
|
||||
// "launch week.mdx" is converted to "launch-week.mdx"
|
||||
'promo/launch-week',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
slugs.sort(),
|
||||
[
|
||||
'columbia',
|
||||
'endeavour',
|
||||
'enterprise',
|
||||
// "launch week.mdx" is converted to "launch-week.mdx"
|
||||
'promo/launch-week',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Returns `with schema` collection', async () => {
|
||||
|
@ -55,20 +61,20 @@ describe('Content Collections', () => {
|
|||
|
||||
const ids = json.withSchemaConfig.map((item) => item.id);
|
||||
const publishedDates = json.withSchemaConfig.map((item) => item.data.publishedAt);
|
||||
assert.deepEqual(ids, ['four%.md', 'one.md', 'three.md', 'two.md']);
|
||||
assert.deepEqual(ids.sort(), ['four%.md', 'one.md', 'three.md', 'two.md'].sort());
|
||||
assert.equal(
|
||||
publishedDates.every((date) => date instanceof Date),
|
||||
true,
|
||||
'Not all publishedAt dates are Date objects',
|
||||
);
|
||||
assert.deepEqual(
|
||||
publishedDates.map((date) => date.toISOString()),
|
||||
publishedDates.map((date) => date.toISOString()).sort(),
|
||||
[
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-01-03T00:00:00.000Z',
|
||||
'2021-01-02T00:00:00.000Z',
|
||||
],
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -77,7 +83,7 @@ describe('Content Collections', () => {
|
|||
assert.equal(Array.isArray(json.withSlugConfig), true);
|
||||
|
||||
const slugs = json.withSlugConfig.map((item) => item.slug);
|
||||
assert.deepEqual(slugs, ['fancy-one', 'excellent-three', 'interesting-two']);
|
||||
assert.deepEqual(slugs.sort(), ['fancy-one', 'excellent-three', 'interesting-two'].sort());
|
||||
});
|
||||
|
||||
it('Returns `with union schema` collection', async () => {
|
||||
|
@ -102,10 +108,12 @@ describe('Content Collections', () => {
|
|||
it('Handles symlinked content', async () => {
|
||||
assert.ok(json.hasOwnProperty('withSymlinkedContent'));
|
||||
assert.equal(Array.isArray(json.withSymlinkedContent), true);
|
||||
|
||||
const ids = json.withSymlinkedContent.map((item) => item.id);
|
||||
assert.deepEqual(ids, ['first.md', 'second.md', 'third.md']);
|
||||
assert.equal(json.withSymlinkedContent[0].data.title, 'First Blog');
|
||||
assert.deepEqual(ids.sort(), ['first.md', 'second.md', 'third.md'].sort());
|
||||
assert.equal(
|
||||
json.withSymlinkedContent.find(({ id }) => id === 'first.md').data.title,
|
||||
'First Blog',
|
||||
);
|
||||
});
|
||||
|
||||
it('Handles symlinked data', async () => {
|
||||
|
@ -137,11 +145,6 @@ describe('Content Collections', () => {
|
|||
json = devalue.parse(rawJson);
|
||||
});
|
||||
|
||||
it('Returns `without config` collection entry', async () => {
|
||||
assert.ok(json.hasOwnProperty('columbiaWithoutConfig'));
|
||||
assert.equal(json.columbiaWithoutConfig.id, 'columbia.md');
|
||||
});
|
||||
|
||||
it('Returns `with schema` collection entry', async () => {
|
||||
assert.ok(json.hasOwnProperty('oneWithSchemaConfig'));
|
||||
assert.equal(json.oneWithSchemaConfig.id, 'one.md');
|
||||
|
@ -212,7 +215,7 @@ describe('Content Collections', () => {
|
|||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/content-static-paths-integration/' });
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
});
|
||||
|
||||
it('Generates expected pages', async () => {
|
||||
|
@ -246,7 +249,7 @@ describe('Content Collections', () => {
|
|||
const fixture = await loadFixture({ root: './fixtures/content with spaces in folder name/' });
|
||||
let error = null;
|
||||
try {
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
@ -260,7 +263,7 @@ describe('Content Collections', () => {
|
|||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
@ -274,7 +277,7 @@ describe('Content Collections', () => {
|
|||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
@ -289,7 +292,7 @@ describe('Content Collections', () => {
|
|||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
@ -304,7 +307,7 @@ describe('Content Collections', () => {
|
|||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
@ -330,7 +333,7 @@ describe('Content Collections', () => {
|
|||
plugins: [preventNodeBuiltinDependencyPlugin()],
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
|
@ -373,7 +376,7 @@ describe('Content Collections', () => {
|
|||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-base/',
|
||||
});
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
});
|
||||
|
||||
it('Includes base in links', async () => {
|
||||
|
@ -396,7 +399,7 @@ describe('Content Collections', () => {
|
|||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-mutation/',
|
||||
});
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
});
|
||||
|
||||
it('Does not mutate cached collection', async () => {
|
||||
|
|
|
@ -9,7 +9,7 @@ describe('Content Collections - data collections', () => {
|
|||
let fixture;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/data-collections/' });
|
||||
await fixture.build();
|
||||
await fixture.build({ force: true });
|
||||
});
|
||||
|
||||
describe('Authors Collection', () => {
|
||||
|
@ -26,19 +26,25 @@ describe('Content Collections - data collections', () => {
|
|||
|
||||
it('Generates correct ids', async () => {
|
||||
const ids = json.map((item) => item.id).sort();
|
||||
assert.deepEqual(ids, ['Ben Holmes', 'Fred K Schott', 'Nate Moore']);
|
||||
assert.deepEqual(ids.sort(), ['Ben Holmes', 'Fred K Schott', 'Nate Moore'].sort());
|
||||
});
|
||||
|
||||
it('Generates correct data', async () => {
|
||||
const names = json.map((item) => item.data.name);
|
||||
assert.deepEqual(names, ['Ben J Holmes', 'Fred K Schott', 'Nate Something Moore']);
|
||||
assert.deepEqual(
|
||||
names.sort(),
|
||||
['Ben J Holmes', 'Fred K Schott', 'Nate Something Moore'].sort(),
|
||||
);
|
||||
|
||||
const twitterUrls = json.map((item) => item.data.twitter);
|
||||
assert.deepEqual(twitterUrls, [
|
||||
'https://twitter.com/bholmesdev',
|
||||
'https://twitter.com/FredKSchott',
|
||||
'https://twitter.com/n_moore',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
twitterUrls.sort(),
|
||||
[
|
||||
'https://twitter.com/bholmesdev',
|
||||
'https://twitter.com/FredKSchott',
|
||||
'https://twitter.com/n_moore',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ export default defineConfig({
|
|||
build: {
|
||||
assetsInlineLimit: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { defineConfig } from 'astro/config';
|
|||
export default defineConfig({
|
||||
integrations: [mdx()],
|
||||
experimental: {
|
||||
contentIntellisense: true
|
||||
contentIntellisense: true,
|
||||
|
||||
}
|
||||
});
|
||||
|
|
14
packages/astro/test/fixtures/content-collections/src/content/.should-also-ignore/enterprise.md
vendored
Normal file
14
packages/astro/test/fixtures/content-collections/src/content/.should-also-ignore/enterprise.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: 'Enterprise'
|
||||
description: 'Learn about the Enterprise NASA space shuttle.'
|
||||
publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
tags: [space, 70s]
|
||||
---
|
||||
|
||||
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
|
||||
|
||||
Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
|
||||
|
||||
Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
|
||||
|
||||
Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.
|
14
packages/astro/test/fixtures/content-collections/src/content/_should-ignore/enterprise.md
vendored
Normal file
14
packages/astro/test/fixtures/content-collections/src/content/_should-ignore/enterprise.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: 'Enterprise'
|
||||
description: 'Learn about the Enterprise NASA space shuttle.'
|
||||
publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
tags: [space, 70s]
|
||||
---
|
||||
|
||||
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
|
||||
|
||||
Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
|
||||
|
||||
Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
|
||||
|
||||
Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.
|
|
@ -52,6 +52,8 @@ const withSymlinkedContent = defineCollection({
|
|||
}),
|
||||
});
|
||||
|
||||
const withScripts = defineCollection({});
|
||||
|
||||
export const collections = {
|
||||
'with-data': withData,
|
||||
'with-custom-slugs': withCustomSlugs,
|
||||
|
@ -59,4 +61,5 @@ export const collections = {
|
|||
'with-union-schema': withUnionSchema,
|
||||
'with-symlinked-data': withSymlinkedData,
|
||||
'with-symlinked-content': withSymlinkedContent,
|
||||
'with-scripts': withScripts,
|
||||
};
|
||||
|
|
|
@ -3,14 +3,14 @@ import * as devalue from 'devalue';
|
|||
import { stripAllRenderFn } from '../utils.js';
|
||||
|
||||
export async function GET() {
|
||||
const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
|
||||
const withSchemaConfig = stripAllRenderFn(await getCollection('with-schema-config'));
|
||||
const withSlugConfig = stripAllRenderFn(await getCollection('with-custom-slugs'));
|
||||
const withUnionSchema = stripAllRenderFn(await getCollection('with-union-schema'));
|
||||
const withSymlinkedContent = stripAllRenderFn(await getCollection('with-symlinked-content'));
|
||||
const withSymlinkedData = stripAllRenderFn(await getCollection('with-symlinked-data'));
|
||||
const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
|
||||
|
||||
return new Response(
|
||||
devalue.stringify({ withoutConfig, withSchemaConfig, withSlugConfig, withUnionSchema, withSymlinkedContent, withSymlinkedData }),
|
||||
devalue.stringify({ withSchemaConfig, withSlugConfig, withUnionSchema, withSymlinkedContent, withSymlinkedData, withoutConfig }),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import * as devalue from 'devalue';
|
|||
import { stripRenderFn } from '../utils.js';
|
||||
|
||||
export async function GET() {
|
||||
const columbiaWithoutConfig = stripRenderFn(await getEntryBySlug('without-config', 'columbia'));
|
||||
const oneWithSchemaConfig = stripRenderFn(await getEntryBySlug('with-schema-config', 'one'));
|
||||
const twoWithSlugConfig = stripRenderFn(
|
||||
await getEntryBySlug('with-custom-slugs', 'interesting-two')
|
||||
|
@ -12,7 +11,6 @@ export async function GET() {
|
|||
|
||||
return new Response(
|
||||
devalue.stringify({
|
||||
columbiaWithoutConfig,
|
||||
oneWithSchemaConfig,
|
||||
twoWithSlugConfig,
|
||||
postWithUnionSchema,
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
collections: true,
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,4 +3,8 @@ import { defineConfig } from 'astro/config';
|
|||
|
||||
export default defineConfig({
|
||||
integrations: [mdx()],
|
||||
legacy: {
|
||||
// Enable legacy content collections as we test layout fields
|
||||
collections: true
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,7 +11,6 @@ export async function getStaticPaths() {
|
|||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
const myImage = await getImage(entry.data.image);
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
|
9
packages/astro/test/fixtures/core-image-errors/astro.config.mjs
vendored
Normal file
9
packages/astro/test/fixtures/core-image-errors/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
// Needed because we're using image().refine()
|
||||
collections: true,
|
||||
},
|
||||
});
|
9
packages/astro/test/fixtures/core-image/astro.config.mjs
vendored
Normal file
9
packages/astro/test/fixtures/core-image/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
// Needed because we're using image().refine()
|
||||
collections: true,
|
||||
},
|
||||
});
|
8
packages/astro/test/fixtures/css-inline-stylesheets-2/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/css-inline-stylesheets-2/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
// Enable legacy content collections as we test layout fields
|
||||
collections: true
|
||||
}
|
||||
});
|
8
packages/astro/test/fixtures/css-inline-stylesheets-3/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/css-inline-stylesheets-3/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
// Enable legacy content collections as we test layout fields
|
||||
collections: true
|
||||
}
|
||||
});
|
8
packages/astro/test/fixtures/css-inline-stylesheets/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/css-inline-stylesheets/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
// Enable legacy content collections as we test layout fields
|
||||
collections: true
|
||||
}
|
||||
});
|
|
@ -38,4 +38,6 @@ const image = defineCollection({
|
|||
}),
|
||||
});
|
||||
|
||||
export const collections = { docs, func, image, i18n };
|
||||
const authors = defineCollection({});
|
||||
|
||||
export const collections = { docs, func, image, i18n, authors };
|
||||
|
|
|
@ -9,7 +9,7 @@ export function getStaticPaths() {
|
|||
/** @param {import('astro').APIContext} params */
|
||||
export async function GET({ params }) {
|
||||
const { id } = params;
|
||||
const author = await getEntry('authors-without-config', id);
|
||||
const author = await getEntry('authors', id);
|
||||
if (!author) {
|
||||
return Response.json({ error: `Author ${id} Not found` });
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const authors = await getCollection('authors-without-config');
|
||||
const authors = await getCollection('authors');
|
||||
return Response.json(authors);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
export default defineConfig({
|
||||
|
||||
});
|
||||
|
|
|
@ -17,4 +17,8 @@ const i18n = defineCollection({
|
|||
}),
|
||||
});
|
||||
|
||||
export const collections = { docs, i18n };
|
||||
const authors = defineCollection({
|
||||
type: 'data'
|
||||
});
|
||||
|
||||
export const collections = { docs, i18n, authors };
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { getEntry } from 'astro:content';
|
||||
import { getEntry, getCollection } from 'astro:content';
|
||||
|
||||
const ids = ['Ben Holmes', 'Fred K Schott', 'Nate Moore'];
|
||||
export async function getStaticPaths() {
|
||||
const collection = await getCollection('authors');
|
||||
|
||||
export function getStaticPaths() {
|
||||
return ids.map((id) => ({ params: { id } }));
|
||||
return collection.map(({ id }) => ({ params: { id } }));
|
||||
}
|
||||
|
||||
/** @param {import('astro').APIContext} params */
|
||||
export async function GET({ params }) {
|
||||
const { id } = params;
|
||||
const author = await getEntry('authors-without-config', id);
|
||||
const author = await getEntry('authors', id);
|
||||
if (!author) {
|
||||
return Response.json({ error: `Author ${id} Not found` });
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const authors = await getCollection('authors-without-config');
|
||||
const authors = await getCollection('authors');
|
||||
return Response.json(authors);
|
||||
}
|
||||
|
|
13
packages/astro/test/fixtures/legacy-content-collections/astro.config.mjs
vendored
Normal file
13
packages/astro/test/fixtures/legacy-content-collections/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
import mdx from '@astrojs/mdx';
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [mdx()],
|
||||
experimental: {
|
||||
contentIntellisense: true,
|
||||
},
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
9
packages/astro/test/fixtures/legacy-content-collections/package.json
vendored
Normal file
9
packages/astro/test/fixtures/legacy-content-collections/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/legacy-content-collections",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/mdx": "workspace:*"
|
||||
}
|
||||
}
|
BIN
packages/astro/test/fixtures/legacy-content-collections/src/assets/the-future.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/legacy-content-collections/src/assets/the-future.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
1
packages/astro/test/fixtures/legacy-content-collections/src/components/ScriptCompA.astro
vendored
Normal file
1
packages/astro/test/fixtures/legacy-content-collections/src/components/ScriptCompA.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<script>console.log('ScriptCompA')</script>
|
1
packages/astro/test/fixtures/legacy-content-collections/src/components/ScriptCompB.astro
vendored
Normal file
1
packages/astro/test/fixtures/legacy-content-collections/src/components/ScriptCompB.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<script>console.log('ScriptCompB')</script>
|
62
packages/astro/test/fixtures/legacy-content-collections/src/content/config.ts
vendored
Normal file
62
packages/astro/test/fixtures/legacy-content-collections/src/content/config.ts
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const withData = defineCollection({
|
||||
type: 'data',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const withCustomSlugs = defineCollection({
|
||||
// Ensure schema passes even when `slug` is present
|
||||
schema: z.object({}).strict(),
|
||||
});
|
||||
|
||||
const withSchemaConfig = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
isDraft: z.boolean().default(false),
|
||||
lang: z.enum(['en', 'fr', 'es']).default('en'),
|
||||
publishedAt: z.date().transform((val) => new Date(val)),
|
||||
}),
|
||||
});
|
||||
|
||||
const withUnionSchema = defineCollection({
|
||||
schema: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('post'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('newsletter'),
|
||||
subject: z.string(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
const withSymlinkedData = defineCollection({
|
||||
type: 'data',
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
alt: z.string(),
|
||||
src: image(),
|
||||
}),
|
||||
});
|
||||
|
||||
const withSymlinkedContent = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
'with-data': withData,
|
||||
'with-custom-slugs': withCustomSlugs,
|
||||
'with-schema-config': withSchemaConfig,
|
||||
'with-union-schema': withUnionSchema,
|
||||
'with-symlinked-data': withSymlinkedData,
|
||||
'with-symlinked-content': withSymlinkedContent,
|
||||
};
|
5
packages/astro/test/fixtures/legacy-content-collections/src/content/with-custom-slugs/one.md
vendored
Normal file
5
packages/astro/test/fixtures/legacy-content-collections/src/content/with-custom-slugs/one.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
slug: fancy-one
|
||||
---
|
||||
|
||||
# It's the first page, fancy!
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
slug: excellent-three
|
||||
---
|
||||
|
||||
# It's the third page, excellent!
|
5
packages/astro/test/fixtures/legacy-content-collections/src/content/with-custom-slugs/two.md
vendored
Normal file
5
packages/astro/test/fixtures/legacy-content-collections/src/content/with-custom-slugs/two.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
slug: interesting-two
|
||||
---
|
||||
|
||||
# It's the second page, interesting!
|
3
packages/astro/test/fixtures/legacy-content-collections/src/content/with-data/one.json
vendored
Normal file
3
packages/astro/test/fixtures/legacy-content-collections/src/content/with-data/one.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "One"
|
||||
}
|
3
packages/astro/test/fixtures/legacy-content-collections/src/content/with-data/three.json
vendored
Normal file
3
packages/astro/test/fixtures/legacy-content-collections/src/content/with-data/three.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Three"
|
||||
}
|
3
packages/astro/test/fixtures/legacy-content-collections/src/content/with-data/two.json
vendored
Normal file
3
packages/astro/test/fixtures/legacy-content-collections/src/content/with-data/two.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Two"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: Four
|
||||
description: The forth page
|
||||
lang: en
|
||||
publishedAt: 2021-01-01
|
||||
---
|
||||
|
||||
# It's the forth page, fancy!
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: One
|
||||
description: The first page
|
||||
lang: en
|
||||
publishedAt: 2021-01-01
|
||||
---
|
||||
|
||||
# It's the first page, fancy!
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: Three
|
||||
description: The third page
|
||||
lang: es
|
||||
publishedAt: 2021-01-03
|
||||
---
|
||||
|
||||
# It's the third page, excellent!
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: Two
|
||||
description: The second page
|
||||
lang: en
|
||||
publishedAt: 2021-01-02
|
||||
---
|
||||
|
||||
# It's the second page, interesting!
|
7
packages/astro/test/fixtures/legacy-content-collections/src/content/with-scripts/one.mdx
vendored
Normal file
7
packages/astro/test/fixtures/legacy-content-collections/src/content/with-scripts/one.mdx
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import ScriptCompA from '../../components/ScriptCompA.astro'
|
||||
import ScriptCompB from '../../components/ScriptCompB.astro'
|
||||
|
||||
Both scripts should exist.
|
||||
|
||||
<ScriptCompA />
|
||||
<ScriptCompB />
|
1
packages/astro/test/fixtures/legacy-content-collections/src/content/with-symlinked-content
vendored
Symbolic link
1
packages/astro/test/fixtures/legacy-content-collections/src/content/with-symlinked-content
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../symlinked-collections/content-collection
|
1
packages/astro/test/fixtures/legacy-content-collections/src/content/with-symlinked-data
vendored
Symbolic link
1
packages/astro/test/fixtures/legacy-content-collections/src/content/with-symlinked-data
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../symlinked-collections/data-collection
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: newsletter
|
||||
subject: My Newsletter
|
||||
---
|
||||
|
||||
# It's a newsletter!
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: post
|
||||
title: My Post
|
||||
description: This is my post
|
||||
---
|
||||
|
||||
# It's a post!
|
15
packages/astro/test/fixtures/legacy-content-collections/src/content/without-config/columbia.md
vendored
Normal file
15
packages/astro/test/fixtures/legacy-content-collections/src/content/without-config/columbia.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Columbia
|
||||
description: 'Learn about the Columbia NASA space shuttle.'
|
||||
publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
tags: [space, 90s]
|
||||
---
|
||||
|
||||
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
|
||||
|
||||
Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
|
||||
|
||||
The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
|
||||
|
||||
NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
|
||||
Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
|
14
packages/astro/test/fixtures/legacy-content-collections/src/content/without-config/endeavour.md
vendored
Normal file
14
packages/astro/test/fixtures/legacy-content-collections/src/content/without-config/endeavour.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Endeavour
|
||||
description: 'Learn about the Endeavour NASA space shuttle.'
|
||||
publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
tags: [space, 90s]
|
||||
---
|
||||
|
||||
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
|
||||
|
||||
Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
|
||||
|
||||
The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
|
||||
|
||||
NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
|
14
packages/astro/test/fixtures/legacy-content-collections/src/content/without-config/enterprise.md
vendored
Normal file
14
packages/astro/test/fixtures/legacy-content-collections/src/content/without-config/enterprise.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: 'Enterprise'
|
||||
description: 'Learn about the Enterprise NASA space shuttle.'
|
||||
publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
tags: [space, 70s]
|
||||
---
|
||||
|
||||
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
|
||||
|
||||
Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
|
||||
|
||||
Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
|
||||
|
||||
Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.
|
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
font-family: 'Comic Sans MS', sans-serif;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: 'Launch week!'
|
||||
description: 'Join us for the exciting launch of SPACE BLOG'
|
||||
publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
tags: ['announcement']
|
||||
---
|
||||
|
||||
import './_launch-week-styles.css';
|
||||
|
||||
Join us for the space blog launch!
|
||||
|
||||
- THIS THURSDAY
|
||||
- Houston, TX
|
||||
- Dress code: **interstellar casual** ✨
|
16
packages/astro/test/fixtures/legacy-content-collections/src/pages/collections.json.js
vendored
Normal file
16
packages/astro/test/fixtures/legacy-content-collections/src/pages/collections.json.js
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
import * as devalue from 'devalue';
|
||||
import { stripAllRenderFn } from '../utils.js';
|
||||
|
||||
export async function GET() {
|
||||
const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
|
||||
const withSchemaConfig = stripAllRenderFn(await getCollection('with-schema-config'));
|
||||
const withSlugConfig = stripAllRenderFn(await getCollection('with-custom-slugs'));
|
||||
const withUnionSchema = stripAllRenderFn(await getCollection('with-union-schema'));
|
||||
const withSymlinkedContent = stripAllRenderFn(await getCollection('with-symlinked-content'));
|
||||
const withSymlinkedData = stripAllRenderFn(await getCollection('with-symlinked-data'));
|
||||
|
||||
return new Response(
|
||||
devalue.stringify({ withoutConfig, withSchemaConfig, withSlugConfig, withUnionSchema, withSymlinkedContent, withSymlinkedData }),
|
||||
);
|
||||
}
|
21
packages/astro/test/fixtures/legacy-content-collections/src/pages/entries.json.js
vendored
Normal file
21
packages/astro/test/fixtures/legacy-content-collections/src/pages/entries.json.js
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { getEntryBySlug } from 'astro:content';
|
||||
import * as devalue from 'devalue';
|
||||
import { stripRenderFn } from '../utils.js';
|
||||
|
||||
export async function GET() {
|
||||
const columbiaWithoutConfig = stripRenderFn(await getEntryBySlug('without-config', 'columbia'));
|
||||
const oneWithSchemaConfig = stripRenderFn(await getEntryBySlug('with-schema-config', 'one'));
|
||||
const twoWithSlugConfig = stripRenderFn(
|
||||
await getEntryBySlug('with-custom-slugs', 'interesting-two')
|
||||
);
|
||||
const postWithUnionSchema = stripRenderFn(await getEntryBySlug('with-union-schema', 'post'));
|
||||
|
||||
return new Response(
|
||||
devalue.stringify({
|
||||
columbiaWithoutConfig,
|
||||
oneWithSchemaConfig,
|
||||
twoWithSlugConfig,
|
||||
postWithUnionSchema,
|
||||
})
|
||||
);
|
||||
}
|
24
packages/astro/test/fixtures/legacy-content-collections/src/pages/index.astro
vendored
Normal file
24
packages/astro/test/fixtures/legacy-content-collections/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
|
||||
---
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>It's content time!</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
font-family: system-ui;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
22
packages/astro/test/fixtures/legacy-content-collections/src/pages/propagation.astro
vendored
Normal file
22
packages/astro/test/fixtures/legacy-content-collections/src/pages/propagation.astro
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
const posts = await getCollection("with-schema-config");
|
||||
---
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<div class="foo">
|
||||
<div>Hello World</div>
|
||||
<span>Styles?</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.foo {
|
||||
background-color: blue;
|
||||
}
|
||||
span::after {
|
||||
content: "works!";
|
||||
}
|
||||
</style>
|
21
packages/astro/test/fixtures/legacy-content-collections/src/pages/with-scripts/[...slug].astro
vendored
Normal file
21
packages/astro/test/fixtures/legacy-content-collections/src/pages/with-scripts/[...slug].astro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('with-scripts');
|
||||
return blogEntries.map(entry => ({
|
||||
params: { slug: entry.slug }, props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
|
||||
const { Content } = await entry.render();
|
||||
const { title } = entry.data;
|
||||
---
|
||||
|
||||
<article>
|
||||
<h1>This is a content collection post</h1>
|
||||
<h2>{title}</h2>
|
||||
<Content />
|
||||
</article>
|
8
packages/astro/test/fixtures/legacy-content-collections/src/utils.js
vendored
Normal file
8
packages/astro/test/fixtures/legacy-content-collections/src/utils.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
export function stripRenderFn(entryWithRender) {
|
||||
const { render, ...entry } = entryWithRender;
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function stripAllRenderFn(collection = []) {
|
||||
return collection.map(stripRenderFn);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "First Blog"
|
||||
date: 2024-04-05
|
||||
---
|
||||
|
||||
First blog content.
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Second Blog"
|
||||
date: 2024-04-06
|
||||
---
|
||||
|
||||
Second blog content.
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Third Blog"
|
||||
date: 2024-04-07
|
||||
---
|
||||
|
||||
Third blog content.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"alt": "Futuristic landscape with chrome buildings and blue skies",
|
||||
"src": "../../assets/the-future.jpg"
|
||||
}
|
8
packages/astro/test/fixtures/legacy-data-collections/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/legacy-data-collections/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
16
packages/astro/test/fixtures/legacy-data-collections/package.json
vendored
Normal file
16
packages/astro/test/fixtures/legacy-data-collections/package.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@test/legacy-data-collections",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
name: Ben J Holmes
|
||||
twitter: https://twitter.com/bholmesdev
|
|
@ -0,0 +1,2 @@
|
|||
name: Fred K Schott
|
||||
twitter: https://twitter.com/FredKSchott
|
|
@ -0,0 +1,2 @@
|
|||
name: Nate Something Moore
|
||||
twitter: https://twitter.com/n_moore
|
20
packages/astro/test/fixtures/legacy-data-collections/src/content/config.ts
vendored
Normal file
20
packages/astro/test/fixtures/legacy-data-collections/src/content/config.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const docs = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
const i18n = defineCollection({
|
||||
type: 'data',
|
||||
schema: z.object({
|
||||
homepage: z.object({
|
||||
greeting: z.string(),
|
||||
preamble: z.string(),
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { docs, i18n };
|
3
packages/astro/test/fixtures/legacy-data-collections/src/content/docs/example.md
vendored
Normal file
3
packages/astro/test/fixtures/legacy-data-collections/src/content/docs/example.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: The future of content
|
||||
---
|
6
packages/astro/test/fixtures/legacy-data-collections/src/content/i18n/en.json
vendored
Normal file
6
packages/astro/test/fixtures/legacy-data-collections/src/content/i18n/en.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"homepage": {
|
||||
"greeting": "Hello World!",
|
||||
"preamble": "Welcome to the future of content."
|
||||
}
|
||||
}
|
6
packages/astro/test/fixtures/legacy-data-collections/src/content/i18n/es.json
vendored
Normal file
6
packages/astro/test/fixtures/legacy-data-collections/src/content/i18n/es.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"homepage": {
|
||||
"greeting": "¡Hola Mundo!",
|
||||
"preamble": "Bienvenido al futuro del contenido."
|
||||
}
|
||||
}
|
3
packages/astro/test/fixtures/legacy-data-collections/src/content/i18n/fr.yaml
vendored
Normal file
3
packages/astro/test/fixtures/legacy-data-collections/src/content/i18n/fr.yaml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
homepage:
|
||||
greeting: "Bonjour le monde!"
|
||||
preamble: "Bienvenue dans le futur du contenu."
|
18
packages/astro/test/fixtures/legacy-data-collections/src/pages/authors/[id].json.js
vendored
Normal file
18
packages/astro/test/fixtures/legacy-data-collections/src/pages/authors/[id].json.js
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { getEntry } from 'astro:content';
|
||||
|
||||
const ids = ['Ben Holmes', 'Fred K Schott', 'Nate Moore'];
|
||||
|
||||
export function getStaticPaths() {
|
||||
return ids.map((id) => ({ params: { id } }));
|
||||
}
|
||||
|
||||
/** @param {import('astro').APIContext} params */
|
||||
export async function GET({ params }) {
|
||||
const { id } = params;
|
||||
const author = await getEntry('authors-without-config', id);
|
||||
if (!author) {
|
||||
return Response.json({ error: `Author ${id} Not found` });
|
||||
} else {
|
||||
return Response.json(author);
|
||||
}
|
||||
}
|
6
packages/astro/test/fixtures/legacy-data-collections/src/pages/authors/all.json.js
vendored
Normal file
6
packages/astro/test/fixtures/legacy-data-collections/src/pages/authors/all.json.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const authors = await getCollection('authors-without-config');
|
||||
return Response.json(authors);
|
||||
}
|
18
packages/astro/test/fixtures/legacy-data-collections/src/pages/translations/[lang].json.js
vendored
Normal file
18
packages/astro/test/fixtures/legacy-data-collections/src/pages/translations/[lang].json.js
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { getEntry } from 'astro:content';
|
||||
|
||||
const langs = ['en', 'es', 'fr'];
|
||||
|
||||
export function getStaticPaths() {
|
||||
return langs.map((lang) => ({ params: { lang } }));
|
||||
}
|
||||
|
||||
/** @param {import('astro').APIContext} params */
|
||||
export async function GET({ params }) {
|
||||
const { lang } = params;
|
||||
const translations = await getEntry('i18n', lang);
|
||||
if (!translations) {
|
||||
return Response.json({ error: `Translation ${lang} Not found` });
|
||||
} else {
|
||||
return Response.json(translations);
|
||||
}
|
||||
}
|
6
packages/astro/test/fixtures/legacy-data-collections/src/pages/translations/all.json.js
vendored
Normal file
6
packages/astro/test/fixtures/legacy-data-collections/src/pages/translations/all.json.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const translations = await getCollection('i18n');
|
||||
return Response.json(translations);
|
||||
}
|
6
packages/astro/test/fixtures/legacy-data-collections/src/pages/translations/by-id.json.js
vendored
Normal file
6
packages/astro/test/fixtures/legacy-data-collections/src/pages/translations/by-id.json.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { getDataEntryById } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const item = await getDataEntryById('i18n', 'en');
|
||||
return Response.json(item);
|
||||
}
|
450
packages/astro/test/legacy-content-collections.test.js
Normal file
450
packages/astro/test/legacy-content-collections.test.js
Normal file
|
@ -0,0 +1,450 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { 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('Legacy Content Collections', () => {
|
||||
describe('Query', () => {
|
||||
let fixture;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/legacy-content-collections/' });
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
describe('Collection', () => {
|
||||
let json;
|
||||
before(async () => {
|
||||
const rawJson = await fixture.readFile('/collections.json');
|
||||
json = devalue.parse(rawJson);
|
||||
});
|
||||
|
||||
it('Returns `without config` collection', async () => {
|
||||
assert.ok(json.hasOwnProperty('withoutConfig'));
|
||||
assert.equal(Array.isArray(json.withoutConfig), true);
|
||||
|
||||
const ids = json.withoutConfig.map((item) => item.id);
|
||||
assert.deepEqual(
|
||||
ids.sort(),
|
||||
[
|
||||
'columbia.md',
|
||||
'endeavour.md',
|
||||
'enterprise.md',
|
||||
// Spaces allowed in IDs
|
||||
'promo/launch week.mdx',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Handles spaces in `without config` slugs', async () => {
|
||||
assert.ok(json.hasOwnProperty('withoutConfig'));
|
||||
assert.equal(Array.isArray(json.withoutConfig), true);
|
||||
|
||||
const slugs = json.withoutConfig.map((item) => item.slug);
|
||||
assert.deepEqual(
|
||||
slugs.sort(),
|
||||
[
|
||||
'columbia',
|
||||
'endeavour',
|
||||
'enterprise',
|
||||
// "launch week.mdx" is converted to "launch-week.mdx"
|
||||
'promo/launch-week',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Returns `with schema` collection', async () => {
|
||||
assert.ok(json.hasOwnProperty('withSchemaConfig'));
|
||||
assert.equal(Array.isArray(json.withSchemaConfig), true);
|
||||
|
||||
const ids = json.withSchemaConfig.map((item) => item.id);
|
||||
const publishedDates = json.withSchemaConfig.map((item) => item.data.publishedAt);
|
||||
assert.deepEqual(ids.sort(), ['four%.md', 'one.md', 'three.md', 'two.md'].sort());
|
||||
assert.equal(
|
||||
publishedDates.every((date) => date instanceof Date),
|
||||
true,
|
||||
'Not all publishedAt dates are Date objects',
|
||||
);
|
||||
assert.deepEqual(
|
||||
publishedDates.map((date) => date.toISOString()),
|
||||
[
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-01-01T00:00:00.000Z',
|
||||
'2021-01-03T00:00:00.000Z',
|
||||
'2021-01-02T00:00:00.000Z',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('Returns `with custom slugs` collection', async () => {
|
||||
assert.ok(json.hasOwnProperty('withSlugConfig'));
|
||||
assert.equal(Array.isArray(json.withSlugConfig), true);
|
||||
|
||||
const slugs = json.withSlugConfig.map((item) => item.slug);
|
||||
assert.deepEqual(slugs, ['fancy-one', 'excellent-three', 'interesting-two']);
|
||||
});
|
||||
|
||||
it('Returns `with union schema` collection', async () => {
|
||||
assert.ok(json.hasOwnProperty('withUnionSchema'));
|
||||
assert.equal(Array.isArray(json.withUnionSchema), true);
|
||||
|
||||
const post = json.withUnionSchema.find((item) => item.id === 'post.md');
|
||||
assert.notEqual(post, undefined);
|
||||
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.notEqual(newsletter, undefined);
|
||||
assert.deepEqual(newsletter.data, {
|
||||
type: 'newsletter',
|
||||
subject: 'My Newsletter',
|
||||
});
|
||||
});
|
||||
|
||||
it('Handles symlinked content', async () => {
|
||||
assert.ok(json.hasOwnProperty('withSymlinkedContent'));
|
||||
assert.equal(Array.isArray(json.withSymlinkedContent), true);
|
||||
const ids = json.withSymlinkedContent.map((item) => item.id);
|
||||
assert.deepEqual(ids.sort(), ['first.md', 'second.md', 'third.md'].sort());
|
||||
assert.equal(
|
||||
json.withSymlinkedContent.find(({ id }) => id === 'first.md').data.title,
|
||||
'First Blog',
|
||||
);
|
||||
});
|
||||
|
||||
it('Handles symlinked data', async () => {
|
||||
assert.ok(json.hasOwnProperty('withSymlinkedData'));
|
||||
assert.equal(Array.isArray(json.withSymlinkedData), true);
|
||||
|
||||
const ids = json.withSymlinkedData.map((item) => item.id);
|
||||
assert.deepEqual(ids, ['welcome']);
|
||||
assert.equal(
|
||||
json.withSymlinkedData[0].data.alt,
|
||||
'Futuristic landscape with chrome buildings and blue skies',
|
||||
);
|
||||
assert.notEqual(json.withSymlinkedData[0].data.src.src, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Propagation', () => {
|
||||
it('Applies styles', async () => {
|
||||
const html = await fixture.readFile('/propagation/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
assert.equal($('style').text().includes('content:"works!"'), true);
|
||||
});
|
||||
});
|
||||
|
||||
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.ok(json.hasOwnProperty('columbiaWithoutConfig'));
|
||||
assert.equal(json.columbiaWithoutConfig.id, 'columbia.md');
|
||||
});
|
||||
|
||||
it('Returns `with schema` collection entry', async () => {
|
||||
assert.ok(json.hasOwnProperty('oneWithSchemaConfig'));
|
||||
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.ok(json.hasOwnProperty('twoWithSlugConfig'));
|
||||
assert.equal(json.twoWithSlugConfig.slug, 'interesting-two');
|
||||
});
|
||||
|
||||
it('Returns `with union schema` collection entry', async () => {
|
||||
assert.ok(json.hasOwnProperty('postWithUnionSchema'));
|
||||
assert.equal(json.postWithUnionSchema.id, 'post.md');
|
||||
assert.deepEqual(json.postWithUnionSchema.data, {
|
||||
type: 'post',
|
||||
title: 'My Post',
|
||||
description: 'This is my post',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scripts', () => {
|
||||
it('Contains all the scripts imported by components', async () => {
|
||||
const html = await fixture.readFile('/with-scripts/one/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
assert.equal($('script').length, 2);
|
||||
// Read the scripts' content
|
||||
const scriptsCode = $('script')
|
||||
.map((_, el) => $(el).text())
|
||||
.toArray()
|
||||
.join('\n');
|
||||
assert.match(scriptsCode, /ScriptCompA/);
|
||||
assert.match(scriptsCode, /ScriptCompB/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
let error = null;
|
||||
try {
|
||||
await fixture.build({ force: true });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
let error;
|
||||
try {
|
||||
await fixture.build();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
assert.equal(error, undefined);
|
||||
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const h1 = $('h1');
|
||||
assert.equal(h1.text(), 'Entries length: 0');
|
||||
assert.equal(h1.attr('data-entries'), '[]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR integration', () => {
|
||||
let app;
|
||||
|
||||
before(async () => {
|
||||
const fixture = await loadFixture({
|
||||
root: './fixtures/content-ssr-integration/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
vite: {
|
||||
plugins: [preventNodeBuiltinDependencyPlugin()],
|
||||
},
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
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/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mutation', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/content-collections-mutation/',
|
||||
legacy: {
|
||||
collections: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Does not mutate cached collection', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const index = cheerio.load(html)('h2:first').text();
|
||||
const html2 = await fixture.readFile('/another_page/index.html');
|
||||
const anotherPage = cheerio.load(html2)('h2:first').text();
|
||||
|
||||
assert.equal(index, anotherPage);
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue