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:
parent
27a0b6339b
commit
c397be324f
12 changed files with 151 additions and 23 deletions
5
.changeset/three-peaches-guess.md
Normal file
5
.changeset/three-peaches-guess.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Add support for `.js/.mjs` file extensions for Content Collections configuration 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 };
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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] };
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
9
packages/astro/test/fixtures/content-collections-with-config-mjs/package.json
vendored
Normal file
9
packages/astro/test/fixtures/content-collections-with-config-mjs/package.json
vendored
Normal 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:*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 10000
|
||||
---
|
||||
|
||||
# Hi there!
|
11
packages/astro/test/fixtures/content-collections-with-config-mjs/src/content/config.mjs
vendored
Normal file
11
packages/astro/test/fixtures/content-collections-with-config-mjs/src/content/config.mjs
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { z, defineCollection } from 'astro:content';
|
||||
|
||||
const blog = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog
|
||||
}
|
5
packages/astro/test/fixtures/content-collections-with-config-mjs/src/pages/index.astro
vendored
Normal file
5
packages/astro/test/fixtures/content-collections-with-config-mjs/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import {getEntryBySlug} from "astro:content"
|
||||
const blogEntry = await getEntryBySlug("blog", "introduction");
|
||||
---
|
||||
{blogEntry.data.title}
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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:*
|
||||
|
|
Loading…
Reference in a new issue