fix: add support for js/mjs file extensions for Content Collections config file (#6229)

* test: add fixture

* test: add test case

* test: fix tests

* feat: support mjs/ js file extensions for cc config

* chore: sync lockfile

* test: make assertion more specific

* test: make template minimal

* chore: add changeset

* feat: add warning when `allowJs` is `false`

* improve warning

* extract tsconfig loader to another function

* rename to more descriptive variable

* apply review suggestion

Co-authored-by: Ben Holmes <hey@bholmes.dev>

---------

Co-authored-by: Ben Holmes <hey@bholmes.dev>
This commit is contained in:
Happydev 2023-02-13 22:19:16 +00:00 committed by GitHub
parent 27a0b6339b
commit c397be324f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 151 additions and 23 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Add support for `.js/.mjs` file extensions for Content Collections configuration file.

View file

@ -1,12 +1,14 @@
import { cyan } from 'kleur/colors';
import { bold, cyan } from 'kleur/colors';
import type fsMod from 'node:fs';
import { pathToFileURL } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { ViteDevServer } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { info, LogOptions } from '../core/logger/core.js';
import { info, LogOptions, warn } from '../core/logger/core.js';
import { appendForwardSlash } from '../core/path.js';
import { createContentTypesGenerator } from './types-generator.js';
import { getContentPaths, globalContentConfigObserver } from './utils.js';
import { ContentPaths, getContentPaths, globalContentConfigObserver } from './utils.js';
import { loadTSConfig } from '../core/config/tsconfig.js';
import path from 'node:path';
interface ContentServerListenerParams {
fs: typeof fsMod;
@ -21,7 +23,10 @@ export async function attachContentServerListeners({
logging,
settings,
}: ContentServerListenerParams) {
const contentPaths = getContentPaths(settings.config);
const contentPaths = getContentPaths(settings.config, fs);
const maybeTsConfigStats = getTSConfigStatsWhenAllowJsFalse({ contentPaths, settings });
if (maybeTsConfigStats) warnAllowJsIsFalse({ ...maybeTsConfigStats, logging });
if (fs.existsSync(contentPaths.contentDir)) {
info(
@ -71,3 +76,51 @@ export async function attachContentServerListeners({
);
}
}
function warnAllowJsIsFalse({
logging,
tsConfigFileName,
contentConfigFileName,
}: {
logging: LogOptions;
tsConfigFileName: string;
contentConfigFileName: string;
}) {
if (!['info', 'warn'].includes(logging.level))
warn(
logging,
'content',
`Make sure you have the ${bold('allowJs')} compiler option set to ${bold(
'true'
)} in your ${bold(tsConfigFileName)} file to have autocompletion in your ${bold(
contentConfigFileName
)} file.
See ${bold('https://www.typescriptlang.org/tsconfig#allowJs')} for more information.
`
);
}
function getTSConfigStatsWhenAllowJsFalse({
contentPaths,
settings,
}: {
contentPaths: ContentPaths;
settings: AstroSettings;
}) {
const isContentConfigJsFile = ['.js', '.mjs'].some((ext) =>
contentPaths.config.url.pathname.endsWith(ext)
);
if (!isContentConfigJsFile) return;
const inputConfig = loadTSConfig(fileURLToPath(settings.config.root), false);
const tsConfigFileName = inputConfig.exists && inputConfig.path.split(path.sep).pop();
if (!tsConfigFileName) return;
const contentConfigFileName = contentPaths.config.url.pathname.split(path.sep).pop()!;
const allowJSOption = inputConfig?.config?.compilerOptions?.allowJs;
const hasAllowJs =
allowJSOption === true || (tsConfigFileName === 'jsconfig.json' && allowJSOption !== false);
if (hasAllowJs) return;
return { tsConfigFileName, contentConfigFileName };
}

View file

@ -51,7 +51,7 @@ export async function createContentTypesGenerator({
viteServer,
}: CreateContentGeneratorParams) {
const contentTypes: ContentTypes = {};
const contentPaths = getContentPaths(settings.config);
const contentPaths = getContentPaths(settings.config, fs);
let events: Promise<{ shouldGenerateTypes: boolean; error?: Error }>[] = [];
let debounceTimeout: NodeJS.Timeout | undefined;
@ -65,7 +65,7 @@ export async function createContentTypesGenerator({
return { typesGenerated: false, reason: 'no-content-dir' };
}
events.push(handleEvent({ name: 'add', entry: contentPaths.config }, { logLevel: 'warn' }));
events.push(handleEvent({ name: 'add', entry: contentPaths.config.url }, { logLevel: 'warn' }));
const globResult = await glob('**', {
cwd: fileURLToPath(contentPaths.contentDir),
fs: {
@ -77,7 +77,7 @@ export async function createContentTypesGenerator({
.map((e) => new URL(e, contentPaths.contentDir))
.filter(
// Config loading handled first. Avoid running twice.
(e) => !e.href.startsWith(contentPaths.config.href)
(e) => !e.href.startsWith(contentPaths.config.url.href)
);
for (const entry of entries) {
events.push(handleEvent({ name: 'add', entry }, { logLevel: 'warn' }));
@ -331,7 +331,7 @@ async function writeContentFiles({
}
let configPathRelativeToCacheDir = normalizePath(
path.relative(contentPaths.cacheDir.pathname, contentPaths.config.pathname)
path.relative(contentPaths.cacheDir.pathname, contentPaths.config.url.pathname)
);
if (!isRelativePath(configPathRelativeToCacheDir))
configPathRelativeToCacheDir = './' + configPathRelativeToCacheDir;

View file

@ -1,6 +1,6 @@
import { slug as githubSlug } from 'github-slugger';
import matter from 'gray-matter';
import type fsMod from 'node:fs';
import fsMod from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from 'vite';
@ -169,7 +169,7 @@ export function getEntryType(
return 'ignored';
} else if ((contentFileExts as readonly string[]).includes(ext)) {
return 'content';
} else if (fileUrl.href === paths.config.href) {
} else if (fileUrl.href === paths.config.url.href) {
return 'config';
} else {
return 'unsupported';
@ -250,13 +250,13 @@ export async function loadContentConfig({
settings: AstroSettings;
viteServer: ViteDevServer;
}): Promise<ContentConfig | undefined> {
const contentPaths = getContentPaths(settings.config);
const contentPaths = getContentPaths(settings.config, fs);
let unparsedConfig;
if (!fs.existsSync(contentPaths.config)) {
if (!contentPaths.config.exists) {
return undefined;
}
try {
const configPathname = fileURLToPath(contentPaths.config);
const configPathname = fileURLToPath(contentPaths.config.url);
unparsedConfig = await viteServer.ssrLoadModule(configPathname);
} catch (e) {
throw e;
@ -313,19 +313,34 @@ export type ContentPaths = {
cacheDir: URL;
typesTemplate: URL;
virtualModTemplate: URL;
config: URL;
config: {
exists: boolean;
url: URL;
};
};
export function getContentPaths({
srcDir,
root,
}: Pick<AstroConfig, 'root' | 'srcDir'>): ContentPaths {
export function getContentPaths(
{ srcDir, root }: Pick<AstroConfig, 'root' | 'srcDir'>,
fs: typeof fsMod = fsMod
): ContentPaths {
const configStats = search(fs, srcDir);
const templateDir = new URL('../../src/content/template/', import.meta.url);
return {
cacheDir: new URL('.astro/', root),
contentDir: new URL('./content/', srcDir),
typesTemplate: new URL('types.d.ts', templateDir),
virtualModTemplate: new URL('virtual-mod.mjs', templateDir),
config: new URL('./content/config.ts', srcDir),
config: configStats,
};
}
function search(fs: typeof fsMod, srcDir: URL) {
const paths = ['config.mjs', 'config.js', 'config.ts'].map(
(p) => new URL(`./content/${p}`, srcDir)
);
for (const file of paths) {
if (fs.existsSync(file)) {
return { exists: true, url: file };
}
}
return { exists: false, url: paths[0] };
}

View file

@ -30,7 +30,7 @@ export function astroContentImportPlugin({
fs: typeof fsMod;
settings: AstroSettings;
}): Plugin {
const contentPaths = getContentPaths(settings.config);
const contentPaths = getContentPaths(settings.config, fs);
return {
name: 'astro:content-imports',

View file

@ -199,6 +199,20 @@ describe('Content Collections', () => {
expect(error).to.be.null;
});
});
describe('With config.mjs', () => {
it("Errors when frontmatter doesn't match schema", async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections-with-config-mjs/',
});
let error;
try {
await fixture.build();
} catch (e) {
error = e.message;
}
expect(error).to.include('"title" should be string, not number.');
});
});
describe('SSR integration', () => {
let app;

View file

@ -0,0 +1,9 @@
{
"name": "@test/content-with-spaces-in-folder-name",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/mdx": "workspace:*"
}
}

View file

@ -0,0 +1,5 @@
---
title: 10000
---
# Hi there!

View file

@ -0,0 +1,11 @@
import { z, defineCollection } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
}),
});
export const collections = {
blog
}

View file

@ -0,0 +1,5 @@
---
import {getEntryBySlug} from "astro:content"
const blogEntry = await getEntryBySlug("blog", "introduction");
---
{blogEntry.data.title}

View file

@ -5,7 +5,10 @@ import { fileURLToPath } from 'node:url';
describe('Content Collections - getEntryType', () => {
const contentDir = new URL('src/content/', import.meta.url);
const contentPaths = {
config: new URL('src/content/config.ts', import.meta.url),
config: {
url: new URL('src/content/config.ts', import.meta.url),
exists: true,
},
};
it('Returns "content" for Markdown files', () => {
@ -25,7 +28,7 @@ describe('Content Collections - getEntryType', () => {
});
it('Returns "config" for config files', () => {
const entry = fileURLToPath(contentPaths.config);
const entry = fileURLToPath(contentPaths.config.url);
const type = getEntryType(entry, contentPaths);
expect(type).to.equal('config');
});

View file

@ -1677,6 +1677,14 @@ importers:
'@astrojs/mdx': link:../../../../integrations/mdx
astro: link:../../..
packages/astro/test/fixtures/content-collections-with-config-mjs:
specifiers:
'@astrojs/mdx': workspace:*
astro: workspace:*
dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
astro: link:../../..
packages/astro/test/fixtures/content-ssr-integration:
specifiers:
'@astrojs/mdx': workspace:*