Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: wait for content layer sync before starting dev server #12818

Merged
merged 11 commits into from
Jan 2, 2025
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
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/e2e/actions-react-19.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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();
Expand Down
28 changes: 24 additions & 4 deletions packages/astro/src/content/content-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import type { LoaderContext } from './loaders/types.js';
import type { MutableDataStore } from './mutable-data-store.js';
import {
type ContentObservable,
getEntryConfigByExtMap,
getEntryDataAndImages,
globalContentConfigObserver,
reloadContentConfigObserver,

Check warning on line 23 in packages/astro/src/content/content-layer.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/correctness/noUnusedImports

This import is unused.
safeStringify,
} from './utils.js';

Expand Down Expand Up @@ -136,17 +138,35 @@
}

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;
}

Expand All @@ -173,11 +193,11 @@
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;
}
Expand Down
15 changes: 13 additions & 2 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -712,6 +721,7 @@ export function contentObservable(initialCtx: ContentCtx): ContentObservable {
}

export type ContentPaths = {
root: URL;
contentDir: URL;
assetsDir: URL;
typesTemplate: URL;
Expand All @@ -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),
Expand Down
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
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
44 changes: 23 additions & 21 deletions packages/astro/src/core/dev/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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') {
Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions packages/astro/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 = () => {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const fixtures = [
exists: true,
},
contentDir: new URL('src/content/', import.meta.url),
root: new URL('.', import.meta.url),
},
},
{
Expand All @@ -22,6 +23,7 @@ const fixtures = [
exists: true,
},
contentDir: new URL('_src/content/', import.meta.url),
root: new URL('.', import.meta.url),
},
},
];
Expand Down
Loading