0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

fix: separate image extraction from schema parsing in content layer (#11884)

* fix: separate image extraction from schema parsing in content layer

* rm unused imports
This commit is contained in:
Matt Kane 2024-09-01 10:05:43 +01:00 committed by GitHub
parent 11ebf3bd15
commit e45070459f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 45 additions and 28 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Correctly handles content layer data where the transformed value does not match the input schema

View file

@ -1,6 +1,4 @@
import { promises as fs, existsSync } from 'node:fs'; import { promises as fs, existsSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as fastq from 'fastq'; import * as fastq from 'fastq';
import type { FSWatcher } from 'vite'; import type { FSWatcher } from 'vite';
import xxhash from 'xxhash-wasm'; import xxhash from 'xxhash-wasm';
@ -19,7 +17,6 @@ import {
getEntryConfigByExtMap, getEntryConfigByExtMap,
getEntryDataAndImages, getEntryDataAndImages,
globalContentConfigObserver, globalContentConfigObserver,
posixRelative,
} from './utils.js'; } from './utils.js';
export interface ContentLayerOptions { export interface ContentLayerOptions {
@ -188,7 +185,7 @@ export class ContentLayer {
const collectionWithResolvedSchema = { ...collection, schema }; const collectionWithResolvedSchema = { ...collection, schema };
const parseData: LoaderContext['parseData'] = async ({ id, data, filePath = '' }) => { const parseData: LoaderContext['parseData'] = async ({ id, data, filePath = '' }) => {
const { imageImports, data: parsedData } = await getEntryDataAndImages( const { data: parsedData } = await getEntryDataAndImages(
{ {
id, id,
collection: name, collection: name,
@ -201,15 +198,6 @@ export class ContentLayer {
collectionWithResolvedSchema, collectionWithResolvedSchema,
false, false,
); );
if (imageImports?.length) {
this.#store.addAssetImports(
imageImports,
// This path may already be relative, if we're re-parsing an existing entry
isAbsolute(filePath)
? posixRelative(fileURLToPath(this.#settings.config.root), filePath)
: filePath,
);
}
return parsedData; return parsedData;
}; };

View file

@ -33,6 +33,7 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase when calling `renderEntry`. * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase when calling `renderEntry`.
*/ */
deferredRender?: boolean; deferredRender?: boolean;
assetImports?: Array<string>;
} }
/** /**

View file

@ -107,15 +107,11 @@ export function glob(globOptions: GlobOptions): Loader {
store.addModuleImport(existingEntry.filePath); store.addModuleImport(existingEntry.filePath);
} }
if (existingEntry.rendered?.metadata?.imagePaths?.length) { if (existingEntry.assetImports?.length) {
// Add asset imports for existing entries // Add asset imports for existing entries
store.addAssetImports( store.addAssetImports(existingEntry.assetImports, existingEntry.filePath);
existingEntry.rendered.metadata.imagePaths,
existingEntry.filePath,
);
} }
// Re-parsing to resolve images and other effects
await parseData(existingEntry);
return; return;
} }
@ -156,10 +152,9 @@ export function glob(globOptions: GlobOptions): Loader {
filePath: relativePath, filePath: relativePath,
digest, digest,
rendered, rendered,
assetImports: rendered?.metadata?.imagePaths,
}); });
if (rendered?.metadata?.imagePaths?.length) {
store.addAssetImports(rendered.metadata.imagePaths, relativePath);
}
// todo: add an explicit way to opt in to deferred rendering // todo: add an explicit way to opt in to deferred rendering
} else if ('contentModuleTypes' in entryType) { } else if ('contentModuleTypes' in entryType) {
store.set({ store.set({

View file

@ -1,7 +1,9 @@
import { promises as fs, type PathLike, existsSync } from 'node:fs'; import { promises as fs, type PathLike, existsSync } from 'node:fs';
import * as devalue from 'devalue'; import * as devalue from 'devalue';
import { Traverse } from 'neotraverse/modern';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js'; import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { IMAGE_IMPORT_PREFIX } from './consts.js';
import { type DataEntry, DataStore, type RenderedContent } from './data-store.js'; import { type DataEntry, DataStore, type RenderedContent } from './data-store.js';
import { contentModuleToId } from './utils.js'; import { contentModuleToId } from './utils.js';
@ -53,7 +55,7 @@ export class MutableDataStore extends DataStore {
this.#saveToDiskDebounced(); this.#saveToDiskDebounced();
} }
addAssetImport(assetImport: string, filePath: string) { addAssetImport(assetImport: string, filePath?: string) {
const id = imageSrcToImportId(assetImport, filePath); const id = imageSrcToImportId(assetImport, filePath);
if (id) { if (id) {
this.#assetImports.add(id); this.#assetImports.add(id);
@ -64,7 +66,7 @@ export class MutableDataStore extends DataStore {
} }
} }
addAssetImports(assets: Array<string>, filePath: string) { addAssetImports(assets: Array<string>, filePath?: string) {
assets.forEach((asset) => this.addAssetImport(asset, filePath)); assets.forEach((asset) => this.addAssetImport(asset, filePath));
} }
@ -195,7 +197,7 @@ export default new Map([\n${lines.join(',\n')}]);
entries: () => this.entries(collectionName), entries: () => this.entries(collectionName),
values: () => this.values(collectionName), values: () => this.values(collectionName),
keys: () => this.keys(collectionName), keys: () => this.keys(collectionName),
set: ({ id: key, data, body, filePath, deferredRender, digest, rendered }) => { set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
if (!key) { if (!key) {
throw new Error(`ID must be a non-empty string`); throw new Error(`ID must be a non-empty string`);
} }
@ -206,6 +208,15 @@ export default new Map([\n${lines.join(',\n')}]);
return false; return false;
} }
} }
const foundAssets = new Set<string>(assetImports);
// Check for image imports in the data. These will have been prefixed during schema parsing
new Traverse(data).forEach((_, val) => {
if (typeof val === 'string' && val.startsWith(IMAGE_IMPORT_PREFIX)) {
const src = val.replace(IMAGE_IMPORT_PREFIX, '');
foundAssets.add(src);
}
});
const entry: DataEntry = { const entry: DataEntry = {
id, id,
data, data,
@ -221,6 +232,12 @@ export default new Map([\n${lines.join(',\n')}]);
} }
entry.filePath = filePath; entry.filePath = filePath;
} }
if (foundAssets.size) {
entry.assetImports = Array.from(foundAssets);
this.addAssetImports(entry.assetImports, filePath);
}
if (digest) { if (digest) {
entry.digest = digest; entry.digest = digest;
} }
@ -334,6 +351,12 @@ export interface ScopedDataStore {
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase. * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase.
*/ */
deferredRender?: boolean; 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; }) => boolean;
values: () => Array<DataEntry>; values: () => Array<DataEntry>;
keys: () => Array<string>; keys: () => Array<string>;

View file

@ -162,17 +162,21 @@ describe('Content Layer', () => {
it('updates the store on new builds', async () => { it('updates the store on new builds', async () => {
assert.equal(json.increment.data.lastValue, 1); assert.equal(json.increment.data.lastValue, 1);
assert.equal(json.entryWithReference.data.something?.content, 'transform me');
await fixture.build(); await fixture.build();
const newJson = devalue.parse(await fixture.readFile('/collections.json')); const newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 2); assert.equal(newJson.increment.data.lastValue, 2);
assert.equal(newJson.entryWithReference.data.something?.content, 'transform me');
}); });
it('clears the store on new build with force flag', async () => { it('clears the store on new build with force flag', async () => {
let newJson = devalue.parse(await fixture.readFile('/collections.json')); let newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 2); assert.equal(newJson.increment.data.lastValue, 2);
assert.equal(newJson.entryWithReference.data.something?.content, 'transform me');
await fixture.build({ force: true }, {}); await fixture.build({ force: true }, {});
newJson = devalue.parse(await fixture.readFile('/collections.json')); newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 1); assert.equal(newJson.increment.data.lastValue, 1);
assert.equal(newJson.entryWithReference.data.something?.content, 'transform me');
}); });
it('clears the store on new build if the config has changed', async () => { it('clears the store on new build if the config has changed', async () => {

View file

@ -5,6 +5,7 @@ publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 90s] tags: [space, 90s]
cat: tabby cat: tabby
heroImage: "./shuttle.jpg" heroImage: "./shuttle.jpg"
something: "transform me"
--- ---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) **Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)

View file

@ -78,6 +78,7 @@ const spacecraft = defineCollection({
tags: z.array(z.string()), tags: z.array(z.string()),
heroImage: image().optional(), heroImage: image().optional(),
cat: reference('cats').optional(), cat: reference('cats').optional(),
something: z.string().optional().transform(str => ({ type: 'test', content: str }))
}), }),
}); });
@ -120,9 +121,9 @@ const increment = defineCollection({
schema: async () => z.object({ schema: async () => z.object({
lastValue: z.number(), lastValue: z.number(),
lastUpdated: z.date(), lastUpdated: z.date(),
}), }),
}, },
}); });
export const collections = { blog, dogs, cats, numbers, spacecraft, increment, images }; export const collections = { blog, dogs, cats, numbers, spacecraft, increment, images };

View file

@ -17,7 +17,6 @@ export async function GET() {
const increment = await getEntry('increment', 'value'); const increment = await getEntry('increment', 'value');
const images = await getCollection('images'); const images = await getCollection('images');
return new Response( return new Response(
devalue.stringify({ devalue.stringify({
customLoader, customLoader,