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

[Content collections] Improve content config handling #5824

Merged
merged 5 commits into from
Jan 11, 2023
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
8 changes: 8 additions & 0 deletions .changeset/fluffy-onions-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'astro': patch
---

Better handle content type generation failures:
- Generate types when content directory is empty
- Log helpful error when running `astro sync` without a content directory
- Avoid swallowing `config.ts` syntax errors from Vite
10 changes: 9 additions & 1 deletion packages/astro/src/cli/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ export async function sync(
fs,
settings,
});
await contentTypesGenerator.init();
const typesResult = await contentTypesGenerator.init();
if (typesResult.typesGenerated === false) {
switch (typesResult.reason) {
case 'no-content-dir':
default:
info(logging, 'content', 'No content directory found. Skipping type generation.');
return 0;
}
}
} catch (e) {
throw new AstroError(AstroErrorData.GenerateContentTypesError);
}
Expand Down
37 changes: 22 additions & 15 deletions packages/astro/src/content/types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
type RawContentEvent = { name: ChokidarEvent; entry: string };
type ContentEvent = { name: ChokidarEvent; entry: URL };

export type GenerateContentTypes = {
init(): Promise<void>;
queueEvent(event: RawContentEvent): void;
};

type ContentTypesEntryMetadata = { slug: string };
type ContentTypes = Record<string, Record<string, ContentTypesEntryMetadata>>;

Expand All @@ -46,7 +41,7 @@ export async function createContentTypesGenerator({
fs,
logging,
settings,
}: CreateContentGeneratorParams): Promise<GenerateContentTypes> {
}: CreateContentGeneratorParams) {
const contentTypes: ContentTypes = {};
const contentPaths = getContentPaths(settings.config);

Expand All @@ -55,9 +50,15 @@ export async function createContentTypesGenerator({

const contentTypesBase = await fs.promises.readFile(contentPaths.typesTemplate, 'utf-8');

async function init() {
await handleEvent({ name: 'add', entry: contentPaths.config }, { logLevel: 'warn' });
const globResult = await glob('./**/*.*', {
async function init(): Promise<
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
> {
if (!fs.existsSync(contentPaths.contentDir)) {
return { typesGenerated: false, reason: 'no-content-dir' };
}

events.push(handleEvent({ name: 'add', entry: contentPaths.config }, { logLevel: 'warn' }));
const globResult = await glob('**', {
cwd: fileURLToPath(contentPaths.contentDir),
fs: {
readdir: fs.readdir.bind(fs),
Expand All @@ -74,6 +75,7 @@ export async function createContentTypesGenerator({
events.push(handleEvent({ name: 'add', entry }, { logLevel: 'warn' }));
}
await runEvents();
return { typesGenerated: true };
}

async function handleEvent(
Expand Down Expand Up @@ -109,10 +111,10 @@ export async function createContentTypesGenerator({
if (fileType === 'config') {
contentConfigObserver.set({ status: 'loading' });
const config = await loadContentConfig({ fs, settings });
if (config instanceof Error) {
contentConfigObserver.set({ status: 'error', error: config });
} else {
if (config) {
contentConfigObserver.set({ status: 'loaded', config });
} else {
contentConfigObserver.set({ status: 'error' });
}

return { shouldGenerateTypes: true };
Expand Down Expand Up @@ -258,13 +260,13 @@ export function getEntryType(
entryPath: string,
paths: ContentPaths
): 'content' | 'config' | 'unknown' | 'generated-types' {
const { dir: rawDir, ext, name, base } = path.parse(entryPath);
const { dir: rawDir, ext, base } = path.parse(entryPath);
const dir = appendForwardSlash(pathToFileURL(rawDir).href);
if ((contentFileExts as readonly string[]).includes(ext)) {
return 'content';
} else if (new URL(name, dir).pathname === paths.config.pathname) {
} else if (new URL(base, dir).href === paths.config.href) {
return 'config';
} else if (new URL(base, dir).pathname === new URL(CONTENT_TYPES_FILE, paths.cacheDir).pathname) {
} else if (new URL(base, dir).href === new URL(CONTENT_TYPES_FILE, paths.cacheDir).href) {
return 'generated-types';
} else {
return 'unknown';
Expand Down Expand Up @@ -313,6 +315,11 @@ async function writeContentFiles({
if (!isRelativePath(configPathRelativeToCacheDir))
configPathRelativeToCacheDir = './' + configPathRelativeToCacheDir;

// Remove `.ts` from import path
if (configPathRelativeToCacheDir.endsWith('.ts')) {
configPathRelativeToCacheDir = configPathRelativeToCacheDir.replace(/\.ts$/, '');
}

contentTypesBase = contentTypesBase.replace('// @@ENTRY_MAP@@', contentTypesStr);
contentTypesBase = contentTypesBase.replace(
"'@@CONTENT_CONFIG_TYPE@@'",
Expand Down
20 changes: 10 additions & 10 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,13 @@ export function parseFrontmatter(fileContents: string, filePath: string) {
}
}

export class NotFoundError extends TypeError {}
export class ZodParseError extends TypeError {}

export async function loadContentConfig({
fs,
settings,
}: {
fs: typeof fsMod;
settings: AstroSettings;
}): Promise<ContentConfig | Error> {
}): Promise<ContentConfig | undefined> {
const contentPaths = getContentPaths(settings.config);
const tempConfigServer: ViteDevServer = await createServer({
root: fileURLToPath(settings.config.root),
Expand All @@ -222,25 +219,28 @@ export async function loadContentConfig({
plugins: [astroContentVirtualModPlugin({ settings })],
});
let unparsedConfig;
if (!fs.existsSync(contentPaths.config)) {
return undefined;
}
try {
unparsedConfig = await tempConfigServer.ssrLoadModule(contentPaths.config.pathname);
} catch {
return new NotFoundError('Failed to resolve content config.');
} catch (e) {
throw e;
} finally {
await tempConfigServer.close();
}
const config = contentConfigParser.safeParse(unparsedConfig);
if (config.success) {
return config.data;
} else {
return new ZodParseError('Content config file is invalid.');
return undefined;
}
}

type ContentCtx =
| { status: 'loading' }
| { status: 'loaded'; config: ContentConfig }
| { status: 'error'; error: NotFoundError | ZodParseError };
| { status: 'error' }
| { status: 'loaded'; config: ContentConfig };

type Observable<C> = {
get: () => C;
Expand Down Expand Up @@ -292,6 +292,6 @@ export function getContentPaths({
contentDir: new URL('./content/', srcDir),
typesTemplate: new URL('types.d.ts', templateDir),
virtualModTemplate: new URL('virtual-mod.mjs', templateDir),
config: new URL('./content/config', srcDir),
config: new URL('./content/config.ts', srcDir),
};
}
61 changes: 29 additions & 32 deletions packages/astro/src/content/vite-plugin-content-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { pathToFileURL } from 'node:url';
import type { Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { info, LogOptions } from '../core/logger/core.js';
import { appendForwardSlash } from '../core/path.js';
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
import { contentFileExts, CONTENT_FLAG } from './consts.js';
import {
createContentTypesGenerator,
GenerateContentTypes,
getEntryType,
} from './types-generator.js';
import { createContentTypesGenerator, getEntryType } from './types-generator.js';
import {
ContentConfig,
contentObservable,
Expand All @@ -36,56 +33,56 @@ export function astroContentServerPlugin({
mode,
}: AstroContentServerPluginParams): Plugin[] {
const contentPaths = getContentPaths(settings.config);
let contentDirExists = false;
let contentGenerator: GenerateContentTypes;
const contentConfigObserver = contentObservable({ status: 'loading' });

async function initContentGenerator() {
const contentGenerator = await createContentTypesGenerator({
fs,
settings,
logging,
contentConfigObserver,
});
await contentGenerator.init();
return contentGenerator;
}

return [
{
name: 'astro-content-server-plugin',
async config(viteConfig) {
try {
await fs.promises.stat(contentPaths.contentDir);
contentDirExists = true;
} catch {
/* silently move on */
return;
}

if (contentDirExists && (mode === 'dev' || viteConfig.build?.ssr === true)) {
contentGenerator = await createContentTypesGenerator({
fs,
settings,
logging,
contentConfigObserver,
});
await contentGenerator.init();
info(logging, 'content', 'Types generated');
// Production build type gen
if (fs.existsSync(contentPaths.contentDir) && viteConfig.build?.ssr === true) {
await initContentGenerator();
}
},
async configureServer(viteServer) {
if (mode !== 'dev') return;

if (contentDirExists) {
// Dev server type gen
if (fs.existsSync(contentPaths.contentDir)) {
info(
logging,
'content',
`Watching ${cyan(
contentPaths.contentDir.href.replace(settings.config.root.href, '')
)} for changes`
);
attachListeners();
await attachListeners();
} else {
viteServer.watcher.on('addDir', (dir) => {
if (pathToFileURL(dir).href === contentPaths.contentDir.href) {
viteServer.watcher.on('addDir', contentDirListener);
async function contentDirListener(dir: string) {
if (appendForwardSlash(pathToFileURL(dir).href) === contentPaths.contentDir.href) {
info(logging, 'content', `Content dir found. Watching for changes`);
contentDirExists = true;
attachListeners();
await attachListeners();
viteServer.watcher.removeListener('addDir', contentDirListener);
}
});
}
}

function attachListeners() {
async function attachListeners() {
const contentGenerator = await initContentGenerator();
info(logging, 'content', 'Types generated');

viteServer.watcher.on('add', (entry) => {
contentGenerator.queueEvent({ name: 'add', entry });
});
Expand Down