From b55a1499da99acf44629ef9aeadcd6405be5ad66 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Mon, 23 Dec 2024 13:25:23 +0000 Subject: [PATCH 1/9] fix: wait for content layer sync before starting dev server --- .changeset/mighty-pugs-retire.md | 5 +++ packages/astro/src/content/content-layer.ts | 4 +- packages/astro/src/core/dev/dev.ts | 42 ++++++++++----------- 3 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 .changeset/mighty-pugs-retire.md diff --git a/.changeset/mighty-pugs-retire.md b/.changeset/mighty-pugs-retire.md new file mode 100644 index 000000000000..8520213a4d32 --- /dev/null +++ b/.changeset/mighty-pugs-retire.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes race condition where dev server would attempt to load collections before the content had loaded diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts index 4e7d364841e7..44c81f48586b 100644 --- a/packages/astro/src/content/content-layer.ts +++ b/packages/astro/src/content/content-layer.ts @@ -173,11 +173,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; } diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts index 0af637164735..1ac6691e381c 100644 --- a/packages/astro/src/core/dev/dev.ts +++ b/packages/astro/src/core/dev/dev.ts @@ -84,27 +84,6 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise Date: Mon, 23 Dec 2024 13:52:06 +0000 Subject: [PATCH 2/9] Load config earlier --- packages/astro/src/core/dev/dev.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts index 1ac6691e381c..03cd8ae391ec 100644 --- a/packages/astro/src/core/dev/dev.ts +++ b/packages/astro/src/core/dev/dev.ts @@ -96,6 +96,7 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise Date: Mon, 23 Dec 2024 15:43:32 +0000 Subject: [PATCH 3/9] Wait for fs flush --- packages/astro/e2e/actions-blog.test.js | 3 ++- packages/astro/e2e/actions-react-19.test.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/astro/e2e/actions-blog.test.js b/packages/astro/e2e/actions-blog.test.js index 84981f078ee4..c778f8b13ce0 100644 --- a/packages/astro/e2e/actions-blog.test.js +++ b/packages/astro/e2e/actions-blog.test.js @@ -14,6 +14,8 @@ test.afterAll(async () => { }); test.afterEach(async ({ astro }) => { + // Allow time for fs events to flush + await new Promise(resolve => setTimeout(resolve, 500)) // Force database reset between tests await astro.editFile('./db/seed.ts', (original) => original); }); @@ -34,7 +36,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'); diff --git a/packages/astro/e2e/actions-react-19.test.js b/packages/astro/e2e/actions-react-19.test.js index 3298db1e334c..e86d50db1d8f 100644 --- a/packages/astro/e2e/actions-react-19.test.js +++ b/packages/astro/e2e/actions-react-19.test.js @@ -7,9 +7,12 @@ let devServer; test.beforeAll(async ({ astro }) => { devServer = await astro.startDevServer(); + }); test.afterEach(async ({ astro }) => { + // Allow time for fs events to flush + await new Promise(resolve => setTimeout(resolve, 500)) // Force database reset between tests await astro.editFile('./db/seed.ts', (original) => original); }); From d9876187ab317e9e8e2b70cbcd81efbaa9e3bc54 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Mon, 23 Dec 2024 15:52:15 +0000 Subject: [PATCH 4/9] Add delay --- packages/astro/e2e/actions-blog.test.js | 1 + packages/astro/e2e/actions-react-19.test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/astro/e2e/actions-blog.test.js b/packages/astro/e2e/actions-blog.test.js index c778f8b13ce0..ebbffc3b0727 100644 --- a/packages/astro/e2e/actions-blog.test.js +++ b/packages/astro/e2e/actions-blog.test.js @@ -26,6 +26,7 @@ test.describe('Astro Actions - Blog', () => { 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'); diff --git a/packages/astro/e2e/actions-react-19.test.js b/packages/astro/e2e/actions-react-19.test.js index e86d50db1d8f..e6063dea712e 100644 --- a/packages/astro/e2e/actions-react-19.test.js +++ b/packages/astro/e2e/actions-react-19.test.js @@ -24,9 +24,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(); From d4e71867af43677767825b61bd38b414c73f1760 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Mon, 23 Dec 2024 16:18:34 +0000 Subject: [PATCH 5/9] Test utils fixes --- packages/astro/e2e/actions-blog.test.js | 5 +---- packages/astro/e2e/actions-react-19.test.js | 5 +---- packages/astro/test/test-utils.js | 5 +++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/astro/e2e/actions-blog.test.js b/packages/astro/e2e/actions-blog.test.js index ebbffc3b0727..03e0c205756a 100644 --- a/packages/astro/e2e/actions-blog.test.js +++ b/packages/astro/e2e/actions-blog.test.js @@ -14,16 +14,13 @@ test.afterAll(async () => { }); test.afterEach(async ({ astro }) => { - // Allow time for fs events to flush - await new Promise(resolve => setTimeout(resolve, 500)) // 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)) diff --git a/packages/astro/e2e/actions-react-19.test.js b/packages/astro/e2e/actions-react-19.test.js index e6063dea712e..d0274e43f20d 100644 --- a/packages/astro/e2e/actions-react-19.test.js +++ b/packages/astro/e2e/actions-react-19.test.js @@ -7,14 +7,11 @@ let devServer; test.beforeAll(async ({ astro }) => { devServer = await astro.startDevServer(); - }); test.afterEach(async ({ astro }) => { - // Allow time for fs events to flush - await new Promise(resolve => setTimeout(resolve, 500)) // 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 () => { diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index f0bdf8542fb6..246d152327a7 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -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; From dd2d34c10777d703966f5934c38f87a22be3cecd Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Jan 2025 15:53:35 +0000 Subject: [PATCH 6/9] Ignore files in dotastro --- packages/astro/src/content/utils.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 32d53f5db819..8da675c6025c 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -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, + paths: Pick, 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, + { srcDir, legacy, root }: Pick, 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), From fb9067f1896309ce6d11514fdab496b47e28a99a Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Jan 2025 16:11:22 +0000 Subject: [PATCH 7/9] Wait for loading config --- packages/astro/src/content/content-layer.ts | 24 +++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts index 44c81f48586b..f1cad4a3481e 100644 --- a/packages/astro/src/content/content-layer.ts +++ b/packages/astro/src/content/content-layer.ts @@ -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,9 +138,27 @@ 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>([ + 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; @@ -146,7 +166,7 @@ export class ContentLayer { // 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; } From 616dfaaa28897b9168f91059eefb862966cd457d Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Jan 2025 16:13:34 +0000 Subject: [PATCH 8/9] Fix test --- .../astro/test/units/content-collections/get-entry-type.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/astro/test/units/content-collections/get-entry-type.test.js b/packages/astro/test/units/content-collections/get-entry-type.test.js index 9d60c4c5c03d..d8dcb459b85c 100644 --- a/packages/astro/test/units/content-collections/get-entry-type.test.js +++ b/packages/astro/test/units/content-collections/get-entry-type.test.js @@ -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), }, }, ]; From ee6d2f6727f5ed08ba38bfae5424f61817af9a46 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Jan 2025 17:46:21 +0000 Subject: [PATCH 9/9] Defer watching data store --- .../src/content/vite-plugin-content-virtual-mod.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index 30c703b5c22f..e0468c654f85 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -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) {