Skip to content

Commit

Permalink
fix: add support for js/mjs file extensions for Content Collections…
Browse files Browse the repository at this point in the history
… 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>
  • Loading branch information
MoustaphaDev and bholmesdev committed Feb 13, 2023
1 parent 27a0b63 commit c397be3
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-peaches-guess.md
@@ -0,0 +1,5 @@
---
'astro': patch
---

Add support for `.js/.mjs` file extensions for Content Collections configuration file.
63 changes: 58 additions & 5 deletions packages/astro/src/content/server-listeners.ts
@@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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 };
}
8 changes: 4 additions & 4 deletions packages/astro/src/content/types-generator.ts
Expand Up @@ -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;
Expand All @@ -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: {
Expand All @@ -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' }));
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 26 additions & 11 deletions packages/astro/src/content/utils.ts
@@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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] };
}
2 changes: 1 addition & 1 deletion packages/astro/src/content/vite-plugin-content-imports.ts
Expand Up @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions packages/astro/test/content-collections.test.js
Expand Up @@ -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;
Expand Down
@@ -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!
@@ -0,0 +1,11 @@
import { z, defineCollection } from 'astro:content';

const blog = defineCollection({
schema: z.object({
title: z.string(),
}),
});

export const collections = {
blog
}
@@ -0,0 +1,5 @@
---
import {getEntryBySlug} from "astro:content"
const blogEntry = await getEntryBySlug("blog", "introduction");
---
{blogEntry.data.title}
Expand Up @@ -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', () => {
Expand All @@ -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');
});
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c397be3

Please sign in to comment.