Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/astro/src/content/mutable-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ export class MutableDataStore extends ImmutableDataStore {
// We then export them all, mapped by the import id, so we can find them again in the build.
const imports: Array<string> = [];
const exports: Array<string> = [];
this.#assetImports.forEach((id) => {
// Sort asset imports to ensure deterministic output across builds
const sortedAssetImports = [...this.#assetImports].sort();
sortedAssetImports.forEach((id) => {
const symbol = importIdToSymbolName(id);
imports.push(`import ${symbol} from ${JSON.stringify(id)};`);
exports.push(`[${JSON.stringify(id)}, ${symbol}]`);
Expand Down Expand Up @@ -144,7 +146,11 @@ export default new Map([${exports.join(', ')}]);
// for each asset is an object with path, format and dimensions.
// We then export them all, mapped by the import id, so we can find them again in the build.
const lines: Array<string> = [];
for (const [fileName, specifier] of this.#moduleImports) {
// Sort module imports by key to ensure deterministic output across builds
const sortedModuleImports = [...this.#moduleImports.entries()].sort(([a], [b]) =>
a.localeCompare(b),
);
for (const [fileName, specifier] of sortedModuleImports) {
lines.push(`[${JSON.stringify(fileName)}, () => import(${JSON.stringify(specifier)})]`);
}
const code = `
Expand Down Expand Up @@ -389,7 +395,18 @@ export default new Map([\n${lines.join(',\n')}]);
}

toString() {
return devalue.stringify(this._collections);
// Sort collections and their entries by key to ensure deterministic serialization.
// Entry insertion order can vary between builds due to concurrent file processing (pLimit),
// so we sort here to guarantee stable output hashes regardless of processing order.
const sorted = new Map(
[...this._collections.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, collection]) => [
key,
new Map([...collection.entries()].sort(([a], [b]) => a.localeCompare(b))),
]),
);
return devalue.stringify(sorted);
}

async writeToDisk() {
Expand Down
130 changes: 71 additions & 59 deletions packages/astro/test/content-layer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,31 @@ describe('Content Layer', () => {

const ids = json.jsonLoader.map((item) => item.data.id);
assert.deepEqual(ids, [
'labrador-retriever',
'german-shepherd',
'golden-retriever',
'french-bulldog',
'bulldog',
'australian-shepherd',
'beagle',
'poodle',
'rottweiler',
'german-shorthaired-pointer',
'yorkshire-terrier',
'bernese-mountain-dog',
'boston-terrier',
'boxer',
'bulldog',
'cavalier-king-charles-spaniel',
'dachshund',
'siberian-husky',
'great-dane',
'doberman-pinscher',
'australian-shepherd',
'english-springer-spaniel',
'french-bulldog',
'german-shepherd',
'german-shorthaired-pointer',
'golden-retriever',
'great-dane',
'havanese',
'labrador-retriever',
'miniature-schnauzer',
'cavalier-king-charles-spaniel',
'shih-tzu',
'boston-terrier',
'bernese-mountain-dog',
'pomeranian',
'havanese',
'english-springer-spaniel',
'poodle',
'rottweiler',
'shetland-sheepdog',
'shih-tzu',
'siberian-husky',
'yorkshire-terrier',
]);
});

Expand Down Expand Up @@ -134,15 +134,15 @@ describe('Content Layer', () => {
assert.ok(Array.isArray(json.nestedJsonLoader));

const ids = json.nestedJsonLoader.map((item) => item.data.id);
assert.deepEqual(ids, ['bluejay', 'robin', 'sparrow', 'cardinal', 'goldfinch']);
assert.deepEqual(ids, ['bluejay', 'cardinal', 'goldfinch', 'robin', 'sparrow']);
});

it('can use an async parser in `file()` loader', async () => {
assert.ok(json.hasOwnProperty('loaderWithAsyncParse'));
assert.ok(Array.isArray(json.loaderWithAsyncParse));

const ids = json.loaderWithAsyncParse.map((item) => item.data.id);
assert.deepEqual(ids, ['bluejay', 'robin', 'sparrow', 'cardinal', 'goldfinch']);
assert.deepEqual(ids, ['bluejay', 'cardinal', 'goldfinch', 'robin', 'sparrow']);
});

it('Returns yaml `file()` loader collection', async () => {
Expand All @@ -151,16 +151,16 @@ describe('Content Layer', () => {

const ids = json.yamlLoader.map((item) => item.id);
assert.deepEqual(ids, [
'angel-fish',
'blue-tail',
'bubble-buddy',
'bubbles',
'finn',
'gold-stripe',
'nemo',
'shadow',
'spark',
'splash',
'nemo',
'angel-fish',
'gold-stripe',
'blue-tail',
'bubble-buddy',
]);
});

Expand All @@ -171,13 +171,13 @@ describe('Content Layer', () => {
const ids = json.tomlLoader.map((item) => item.id);
assert.deepEqual(ids, [
'crown',
'nikes-on-my-feet',
'stars',
'family-ties',
'honest',
'never-let-me-down',
'nikes-on-my-feet',
'no-church-in-the-wild',
'family-ties',
'somebody',
'honest',
'stars',
]);
});

Expand All @@ -187,16 +187,16 @@ describe('Content Layer', () => {

const ids = json.csvLoader.map((item) => item.data.id);
assert.deepEqual(ids, [
'basil',
'chamomile',
'daisy',
'fern',
'lavender',
'marigold',
'rose',
'sage',
'sunflower',
'basil',
'thyme',
'sage',
'daisy',
'marigold',
'chamomile',
'fern',
]);
});

Expand All @@ -221,7 +221,7 @@ describe('Content Layer', () => {
assert.ok(Array.isArray(json.nestedJsonLoader));

const ids = json.nestedJsonLoader.map((item) => item.data.id);
assert.deepEqual(ids, ['bluejay', 'robin', 'sparrow', 'cardinal', 'goldfinch']);
assert.deepEqual(ids, ['bluejay', 'cardinal', 'goldfinch', 'robin', 'sparrow']);
});

it('Returns data entry by id', async () => {
Expand All @@ -246,7 +246,7 @@ describe('Content Layer', () => {
assert.ok(json.hasOwnProperty('simpleLoader'));
assert.ok(Array.isArray(json.simpleLoader));

const item = json.simpleLoader[0];
const item = json.simpleLoader.find((i) => i.id === 'siamese');
assert.deepEqual(item, {
id: 'siamese',
collection: 'cats',
Expand Down Expand Up @@ -586,31 +586,31 @@ describe('Content Layer', () => {

const ids = json.jsonLoader.map((item) => item.data.id);
assert.deepEqual(ids, [
'labrador-retriever',
'german-shepherd',
'golden-retriever',
'french-bulldog',
'bulldog',
'australian-shepherd',
'beagle',
'poodle',
'rottweiler',
'german-shorthaired-pointer',
'yorkshire-terrier',
'bernese-mountain-dog',
'boston-terrier',
'boxer',
'bulldog',
'cavalier-king-charles-spaniel',
'dachshund',
'siberian-husky',
'great-dane',
'doberman-pinscher',
'australian-shepherd',
'english-springer-spaniel',
'french-bulldog',
'german-shepherd',
'german-shorthaired-pointer',
'golden-retriever',
'great-dane',
'havanese',
'labrador-retriever',
'miniature-schnauzer',
'cavalier-king-charles-spaniel',
'shih-tzu',
'boston-terrier',
'bernese-mountain-dog',
'pomeranian',
'havanese',
'english-springer-spaniel',
'poodle',
'rottweiler',
'shetland-sheepdog',
'shih-tzu',
'siberian-husky',
'yorkshire-terrier',
]);
});

Expand Down Expand Up @@ -653,7 +653,10 @@ describe('Content Layer', () => {
it('updates collection when data file is changed', async () => {
const rawJsonResponse = await fixture.fetch('/collections.json');
const initialJson = devalue.parse(await rawJsonResponse.text());
assert.equal(initialJson.jsonLoader[0].data.temperament.includes('Bouncy'), false);
const initialLabrador = initialJson.jsonLoader.find(
(item) => item.data.id === 'labrador-retriever',
);
assert.equal(initialLabrador.data.temperament.includes('Bouncy'), false);

await fixture.editFile('/src/data/dogs.json', (prev) => {
const data = JSON.parse(prev);
Expand All @@ -664,7 +667,10 @@ describe('Content Layer', () => {
await fixture.onNextDataStoreChange();
const updatedJsonResponse = await fixture.fetch('/collections.json');
const updated = devalue.parse(await updatedJsonResponse.text());
assert.ok(updated.jsonLoader[0].data.temperament.includes('Bouncy'));
const updatedLabrador = updated.jsonLoader.find(
(item) => item.data.id === 'labrador-retriever',
);
assert.ok(updatedLabrador.data.temperament.includes('Bouncy'));
await fixture.resetAllFiles();
});

Expand Down Expand Up @@ -772,7 +778,10 @@ describe('Content Layer', () => {

const rawJsonResponse = await fixture.fetch('/collections.json');
const initialJson = devalue.parse(await rawJsonResponse.text());
assert.equal(initialJson.jsonLoader[0].data.temperament.includes('Bouncy'), false);
const initialLabrador = initialJson.jsonLoader.find(
(item) => item.data.id === 'labrador-retriever',
);
assert.equal(initialLabrador.data.temperament.includes('Bouncy'), false);

await fixture.editFile('/src/data/dogs.json', (prev) => {
const data = JSON.parse(prev);
Expand All @@ -783,7 +792,10 @@ describe('Content Layer', () => {
await fixture.onNextDataStoreChange();
const updatedJsonResponse = await fixture.fetch('/collections.json');
const updated = devalue.parse(await updatedJsonResponse.text());
assert.ok(updated.jsonLoader[0].data.temperament.includes('Bouncy'));
const updatedLabrador = updated.jsonLoader.find(
(item) => item.data.id === 'labrador-retriever',
);
assert.ok(updatedLabrador.data.temperament.includes('Bouncy'));
logs.length = 0;

await fixture.resetAllFiles();
Expand Down
Loading