mirror of
https://github.com/withastro/astro.git
synced 2025-01-06 22:10:10 -05:00
fix: wait for content layer sync before starting dev server (#12818)
* 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
This commit is contained in:
parent
7b5dc6f0f1
commit
579bd93794
9 changed files with 91 additions and 46 deletions
5
.changeset/mighty-pugs-retire.md
Normal file
5
.changeset/mighty-pugs-retire.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fixes race condition where dev server would attempt to load collections before the content had loaded
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,10 +160,10 @@ export function astroContentVirtualModPlugin({
|
|||
return fs.readFileSync(modules, 'utf-8');
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
const dataStorePath = fileURLToPath(dataStoreFile);
|
||||
|
||||
server.watcher.add(dataStorePath);
|
||||
configureServer(server) {
|
||||
devServer = server;
|
||||
const dataStorePath = fileURLToPath(dataStoreFile);
|
||||
|
||||
function invalidateDataStore() {
|
||||
const module = server.moduleGraph.getModuleById(RESOLVED_DATA_STORE_VIRTUAL_ID);
|
||||
|
|
|
@ -84,6 +84,37 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevS
|
|||
}
|
||||
}
|
||||
|
||||
let store: MutableDataStore | undefined;
|
||||
try {
|
||||
const dataStoreFile = getDataStoreFile(restart.container.settings, true);
|
||||
if (existsSync(dataStoreFile)) {
|
||||
store = await MutableDataStore.fromFile(dataStoreFile);
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('content', err.message);
|
||||
}
|
||||
if (!store) {
|
||||
store = new MutableDataStore();
|
||||
}
|
||||
await attachContentServerListeners(restart.container);
|
||||
|
||||
const config = globalContentConfigObserver.get();
|
||||
if (config.status === 'error') {
|
||||
logger.error('content', config.error.message);
|
||||
}
|
||||
if (config.status === 'loaded') {
|
||||
const contentLayer = globalContentLayer.init({
|
||||
settings: restart.container.settings,
|
||||
logger,
|
||||
watcher: restart.container.viteServer.watcher,
|
||||
store,
|
||||
});
|
||||
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(
|
||||
|
@ -103,35 +134,6 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevS
|
|||
logger.warn('SKIP_FORMAT', msg.fsStrictWarning());
|
||||
}
|
||||
|
||||
await attachContentServerListeners(restart.container);
|
||||
|
||||
let store: MutableDataStore | undefined;
|
||||
try {
|
||||
const dataStoreFile = getDataStoreFile(restart.container.settings, true);
|
||||
if (existsSync(dataStoreFile)) {
|
||||
store = await MutableDataStore.fromFile(dataStoreFile);
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('content', err.message);
|
||||
}
|
||||
if (!store) {
|
||||
store = new MutableDataStore();
|
||||
}
|
||||
|
||||
const config = globalContentConfigObserver.get();
|
||||
if (config.status === 'error') {
|
||||
logger.error('content', config.error.message);
|
||||
}
|
||||
if (config.status === 'loaded') {
|
||||
const contentLayer = globalContentLayer.init({
|
||||
settings: restart.container.settings,
|
||||
logger,
|
||||
watcher: restart.container.viteServer.watcher,
|
||||
store,
|
||||
});
|
||||
contentLayer.watchContentConfig();
|
||||
await contentLayer.sync();
|
||||
}
|
||||
|
||||
logger.info(null, green('watching for file changes...'));
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue