Skip to content

Commit

Permalink
fix: wait for content layer sync before starting dev server (#12818)
Browse files Browse the repository at this point in the history
* fix: wait for content layer sync before starting dev server

* Load config earlier

* Wait for fs flush

* Add delay

* Test utils fixes

* Ignore files in dotastro

* Wait for loading config

* Fix test

* Defer watching data store
ascorbic authored Jan 2, 2025

Verified

This commit was signed with the committer’s verified signature.
vkubiv Volodymyr Kubiv
1 parent 7b5dc6f commit 579bd93
Showing 9 changed files with 82 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-pugs-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes race condition where dev server would attempt to load collections before the content had loaded
5 changes: 2 additions & 3 deletions packages/astro/e2e/actions-blog.test.js
Original file line number Diff line number Diff line change
@@ -15,15 +15,15 @@ test.afterAll(async () => {

test.afterEach(async ({ astro }) => {
// Force database reset between tests
await astro.editFile('./db/seed.ts', (original) => original);
await astro.editFile('./db/seed.ts', (original) => original, false);
});

test.describe('Astro Actions - Blog', () => {
test('Like action', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('Like');
await waitForHydrate(page, likeButton);
await new Promise(resolve => setTimeout(resolve, 500))
await expect(likeButton, 'like button starts with 10 likes').toContainText('10');
await likeButton.click();
await expect(likeButton, 'like button should increment likes').toContainText('11');
@@ -34,7 +34,6 @@ test.describe('Astro Actions - Blog', () => {

const likeButton = page.getByLabel('get-request');
const likeCount = page.getByLabel('Like');

await expect(likeCount, 'like button starts with 10 likes').toContainText('10');
await likeButton.click();
await expect(likeCount, 'like button should increment likes').toContainText('11');
4 changes: 2 additions & 2 deletions packages/astro/e2e/actions-react-19.test.js
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ test.beforeAll(async ({ astro }) => {

test.afterEach(async ({ astro }) => {
// Force database reset between tests
await astro.editFile('./db/seed.ts', (original) => original);
await astro.editFile('./db/seed.ts', (original) => original, false);
});

test.afterAll(async () => {
@@ -21,9 +21,9 @@ test.afterAll(async () => {
test.describe('Astro Actions - React 19', () => {
test('Like action - client pending state', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('likes-client');
await waitForHydrate(page, likeButton);
await new Promise(resolve => setTimeout(resolve, 500))

await expect(likeButton).toBeVisible();
await likeButton.click();
28 changes: 24 additions & 4 deletions packages/astro/src/content/content-layer.ts
Original file line number Diff line number Diff line change
@@ -16,9 +16,11 @@ import {
import type { LoaderContext } from './loaders/types.js';
import type { MutableDataStore } from './mutable-data-store.js';
import {
type ContentObservable,
getEntryConfigByExtMap,
getEntryDataAndImages,
globalContentConfigObserver,
reloadContentConfigObserver,
safeStringify,
} from './utils.js';

@@ -136,17 +138,35 @@ export class ContentLayer {
}

async #doSync(options: RefreshContentOptions) {
const contentConfig = globalContentConfigObserver.get();
let contentConfig = globalContentConfigObserver.get();
const logger = this.#logger.forkIntegrationLogger('content');

if (contentConfig?.status === 'loading') {
contentConfig = await Promise.race<ReturnType<ContentObservable['get']>>([
new Promise((resolve) => {
const unsub = globalContentConfigObserver.subscribe((ctx) => {
unsub();
resolve(ctx);
});
}),
new Promise((resolve) =>
setTimeout(
() =>
resolve({ status: 'error', error: new Error('Content config loading timed out') }),
5000,
),
),
]);
}

if (contentConfig?.status === 'error') {
logger.error(`Error loading content config. Skipping sync.\n${contentConfig.error.message}`);
return;
}

// It shows as loaded with no collections even if there's no config
if (contentConfig?.status !== 'loaded') {
logger.error('Content config not loaded, skipping sync');
logger.error(`Content config not loaded, skipping sync. Status was ${contentConfig?.status}`);
return;
}

@@ -173,11 +193,11 @@ export class ContentLayer {
shouldClear = true;
}

if (currentConfigDigest && previousConfigDigest !== currentConfigDigest) {
if (previousConfigDigest && previousConfigDigest !== currentConfigDigest) {
logger.info('Content config changed');
shouldClear = true;
}
if (process.env.ASTRO_VERSION && previousAstroVersion !== process.env.ASTRO_VERSION) {
if (previousAstroVersion && previousAstroVersion !== process.env.ASTRO_VERSION) {
logger.info('Astro version changed');
shouldClear = true;
}
15 changes: 13 additions & 2 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
@@ -420,19 +420,28 @@ function getRelativeEntryPath(entry: URL, collection: string, contentDir: URL) {
return relativeToCollection;
}

function isParentDirectory(parent: URL, child: URL) {
const relative = path.relative(fileURLToPath(parent), fileURLToPath(child));
return !relative.startsWith('..') && !path.isAbsolute(relative);
}

export function getEntryType(
entryPath: string,
paths: Pick<ContentPaths, 'config' | 'contentDir'>,
paths: Pick<ContentPaths, 'config' | 'contentDir' | 'root'>,
contentFileExts: string[],
dataFileExts: string[],
): 'content' | 'data' | 'config' | 'ignored' {
const { ext } = path.parse(entryPath);
const fileUrl = pathToFileURL(entryPath);

const dotAstroDir = new URL('./.astro/', paths.root);

if (fileUrl.href === paths.config.url.href) {
return 'config';
} else if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir)) {
return 'ignored';
} else if (isParentDirectory(dotAstroDir, fileUrl)) {
return 'ignored';
} else if (contentFileExts.includes(ext)) {
return 'content';
} else if (dataFileExts.includes(ext)) {
@@ -712,6 +721,7 @@ export function contentObservable(initialCtx: ContentCtx): ContentObservable {
}

export type ContentPaths = {
root: URL;
contentDir: URL;
assetsDir: URL;
typesTemplate: URL;
@@ -723,12 +733,13 @@ export type ContentPaths = {
};

export function getContentPaths(
{ srcDir, legacy }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy'>,
{ srcDir, legacy, root }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy'>,
fs: typeof fsMod = fsMod,
): ContentPaths {
const configStats = search(fs, srcDir, legacy?.collections);
const pkgBase = new URL('../../', import.meta.url);
return {
root: new URL('./', root),
contentDir: new URL('./content/', srcDir),
assetsDir: new URL('./assets/', srcDir),
typesTemplate: new URL('templates/content/types.d.ts', pkgBase),
11 changes: 8 additions & 3 deletions packages/astro/src/content/vite-plugin-content-virtual-mod.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import { dataToEsm } from '@rollup/pluginutils';
import glob from 'fast-glob';
import pLimit from 'p-limit';
import type { Plugin } from 'vite';
import type { Plugin, ViteDevServer } from 'vite';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { rootRelativePath } from '../core/viteUtils.js';
import type { AstroSettings } from '../types/astro.js';
@@ -51,12 +51,17 @@ export function astroContentVirtualModPlugin({
fs,
}: AstroContentVirtualModPluginParams): Plugin {
let dataStoreFile: URL;
let devServer: ViteDevServer;
return {
name: 'astro-content-virtual-mod-plugin',
enforce: 'pre',
config(_, env) {
dataStoreFile = getDataStoreFile(settings, env.command === 'serve');
},
buildStart() {
// We defer adding the data store file to the watcher until the server is ready
devServer?.watcher.add(fileURLToPath(dataStoreFile));
},
async resolveId(id) {
if (id === VIRTUAL_MODULE_ID) {
return RESOLVED_VIRTUAL_MODULE_ID;
@@ -155,11 +160,11 @@ export function astroContentVirtualModPlugin({
return fs.readFileSync(modules, 'utf-8');
}
},

configureServer(server) {
devServer = server;
const dataStorePath = fileURLToPath(dataStoreFile);

server.watcher.add(dataStorePath);

function invalidateDataStore() {
const module = server.moduleGraph.getModuleById(RESOLVED_DATA_STORE_VIRTUAL_ID);
if (module) {
44 changes: 23 additions & 21 deletions packages/astro/src/core/dev/dev.ts
Original file line number Diff line number Diff line change
@@ -84,27 +84,6 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevS
}
}

// Start listening to the port
const devServerAddressInfo = await startContainer(restart.container);
logger.info(
'SKIP_FORMAT',
msg.serverStart({
startupTime: performance.now() - devStart,
resolvedUrls: restart.container.viteServer.resolvedUrls || { local: [], network: [] },
host: restart.container.settings.config.server.host,
base: restart.container.settings.config.base,
}),
);

if (isPrerelease) {
logger.warn('SKIP_FORMAT', msg.prerelease({ currentVersion }));
}
if (restart.container.viteServer.config.server?.fs?.strict === false) {
logger.warn('SKIP_FORMAT', msg.fsStrictWarning());
}

await attachContentServerListeners(restart.container);

let store: MutableDataStore | undefined;
try {
const dataStoreFile = getDataStoreFile(restart.container.settings, true);
@@ -117,6 +96,7 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevS
if (!store) {
store = new MutableDataStore();
}
await attachContentServerListeners(restart.container);

const config = globalContentConfigObserver.get();
if (config.status === 'error') {
@@ -131,8 +111,30 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevS
});
contentLayer.watchContentConfig();
await contentLayer.sync();
} else {
logger.warn('content', 'Content config not loaded');
}

// Start listening to the port
const devServerAddressInfo = await startContainer(restart.container);
logger.info(
'SKIP_FORMAT',
msg.serverStart({
startupTime: performance.now() - devStart,
resolvedUrls: restart.container.viteServer.resolvedUrls || { local: [], network: [] },
host: restart.container.settings.config.server.host,
base: restart.container.settings.config.base,
}),
);

if (isPrerelease) {
logger.warn('SKIP_FORMAT', msg.prerelease({ currentVersion }));
}
if (restart.container.viteServer.config.server?.fs?.strict === false) {
logger.warn('SKIP_FORMAT', msg.fsStrictWarning());
}


logger.info(null, green('watching for file changes...'));

return {
5 changes: 3 additions & 2 deletions packages/astro/test/test-utils.js
Original file line number Diff line number Diff line change
@@ -181,6 +181,7 @@ export async function loadFixture(inlineConfig) {
devServer = await dev(mergeConfig(inlineConfig, extraInlineConfig));
config.server.host = parseAddressToHost(devServer.address.address); // update host
config.server.port = devServer.address.port; // update port
await new Promise(resolve => setTimeout(resolve, 100))
return devServer;
},
onNextDataStoreChange: (timeout = 5000) => {
@@ -284,7 +285,7 @@ export async function loadFixture(inlineConfig) {
app.manifest = manifest;
return app;
},
editFile: async (filePath, newContentsOrCallback) => {
editFile: async (filePath, newContentsOrCallback, waitForNextWrite = true) => {
const fileUrl = new URL(filePath.replace(/^\//, ''), config.root);
const contents = await fs.promises.readFile(fileUrl, 'utf-8');
const reset = () => {
@@ -299,7 +300,7 @@ export async function loadFixture(inlineConfig) {
typeof newContentsOrCallback === 'function'
? newContentsOrCallback(contents)
: newContentsOrCallback;
const nextChange = devServer ? onNextChange() : Promise.resolve();
const nextChange = devServer && waitForNextWrite ? onNextChange() : Promise.resolve();
await fs.promises.writeFile(fileUrl, newContents);
await nextChange;
return reset;
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ const fixtures = [
exists: true,
},
contentDir: new URL('src/content/', import.meta.url),
root: new URL('.', import.meta.url),
},
},
{
@@ -22,6 +23,7 @@ const fixtures = [
exists: true,
},
contentDir: new URL('_src/content/', import.meta.url),
root: new URL('.', import.meta.url),
},
},
];

0 comments on commit 579bd93

Please sign in to comment.