feat: add support for other markdown file extensions (#5164)

* fix: add `.markdown ` file extension support

adds `.markdown` file extension support for markdown files

* test: add test case

* chore: adds changeset

* test: move test and fixture to relevant locations

* test: update test

* feat: add multiple markdown file extension support

* feat: add module declaration for different markdown file extensions

* refactor: markdown module declarations

for ease of TS refactoring

* fix: add .js extension to module imports

* test: update test

* chore: update changeset

* chore: update changeset

* test: add new test cases

* test: update tests

* fix: correct typo
This commit is contained in:
Happydev 2022-10-26 14:18:49 +00:00 committed by GitHub
parent d151d9f3f2
commit 4a8a346ca9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 229 additions and 33 deletions

View file

@ -0,0 +1,11 @@
---
'astro': minor
'@astrojs/rss': patch
---
Add support for markdown files with the following extensions:
- `.markdown`
- `.mdown`
- `.mkdn`
- `.mkd`
- `.mdwn`

View file

@ -17,7 +17,7 @@ type RSSOptions = {
/**
* List of RSS feed items to render. Accepts either:
* a) list of RSSFeedItems
* b) import.meta.glob result. You can only glob ".md" files within src/pages/ when using this method!
* b) import.meta.glob result. You can only glob ".md" (or alternative extensions for markdown files like ".markdown") files within src/pages/ when using this method!
*/
items: RSSFeedItem[] | GlobResult;
/** Specify arbitrary metadata on opening <xml> tag */
@ -58,7 +58,7 @@ function mapGlobResult(items: GlobResult): Promise<RSSFeedItem[]> {
const { url, frontmatter } = await getInfo();
if (url === undefined || url === null) {
throw new Error(
`[RSS] When passing an import.meta.glob result directly, you can only glob ".md" files within /pages! Consider mapping the result to an array of RSSFeedItems. See the RSS docs for usage examples: https://docs.astro.build/en/guides/rss/#2-list-of-rss-feed-objects`
`[RSS] When passing an import.meta.glob result directly, you can only glob ".md" (or alternative extensions for markdown files like ".markdown") files within /pages! Consider mapping the result to an array of RSSFeedItems. See the RSS docs for usage examples: https://docs.astro.build/en/guides/rss/#2-list-of-rss-feed-objects`
);
}
if (!Boolean(frontmatter.title) || !Boolean(frontmatter.pubDate)) {

View file

@ -198,7 +198,11 @@ describe('rss', () => {
});
chai.expect(false).to.equal(true, 'Should have errored');
} catch (err) {
chai.expect(err.message).to.contain('you can only glob ".md" files within /pages');
chai
.expect(err.message)
.to.contain(
'you can only glob ".md" (or alternative extensions for markdown files like ".markdown") files within /pages'
);
}
});
});

View file

@ -1,19 +1,106 @@
/// <reference path="./import-meta.d.ts" />
declare module '*.md' {
type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
export const frontmatter: MD['frontmatter'];
export const file: MD['file'];
export const url: MD['url'];
export const getHeadings: MD['getHeadings'];
type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
interface ExportedMarkdownModuleEntities {
frontmatter: MD['frontmatter'];
file: MD['file'];
url: MD['url'];
getHeadings: MD['getHeadings'];
/** @deprecated Renamed to `getHeadings()` */
export const getHeaders: () => void;
export const Content: MD['Content'];
export const rawContent: MD['rawContent'];
export const compiledContent: MD['compiledContent'];
getHeaders: () => void;
Content: MD['Content'];
rawContent: MD['rawContent'];
compiledContent: MD['compiledContent'];
load: MD['default'];
}
const load: MD['default'];
declare module '*.md' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}
declare module '*.markdown' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}
declare module '*.mkdn' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}
declare module '*.mkd' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}
declare module '*.mdwn' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}
declare module '*.mdown' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}

View file

@ -1,3 +1,4 @@
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
import type {
MarkdownHeading,
MarkdownMetadata,
@ -246,6 +247,9 @@ export interface AstroGlobal<Props extends Record<string, any> = Record<string,
};
}
/** Union type of supported markdown file extensions */
type MarkdowFileExtension = typeof SUPPORTED_MARKDOWN_FILE_EXTENSIONS[number];
export interface AstroGlobalPartial {
/**
* @deprecated since version 0.24. See the {@link https://astro.build/deprecated/resolve upgrade guide} for more details.
@ -264,7 +268,9 @@ export interface AstroGlobalPartial {
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astroglob)
*/
glob(globStr: `${any}.astro`): Promise<AstroInstance[]>;
glob<T extends Record<string, any>>(globStr: `${any}.md`): Promise<MarkdownInstance<T>[]>;
glob<T extends Record<string, any>>(
globStr: `${any}${MarkdowFileExtension}`
): Promise<MarkdownInstance<T>[]>;
glob<T extends Record<string, any>>(globStr: `${any}.mdx`): Promise<MDXInstance<T>[]>;
glob<T extends Record<string, any>>(globStr: string): Promise<T[]>;
/**
@ -868,7 +874,7 @@ export interface AstroUserConfig {
* @default `false`
* @version 1.0.0-rc.1
* @description
* Enable Astro's pre-v1.0 support for components and JSX expressions in `.md` Markdown files.
* Enable Astro's pre-v1.0 support for components and JSX expressions in `.md` (and alternative extensions for markdown files like ".markdown") Markdown files.
* In Astro `1.0.0-rc`, this original behavior was removed as the default, in favor of our new [MDX integration](/en/guides/integrations-guide/mdx/).
*
* To enable this behavior, set `legacy.astroFlavoredMarkdown` to `true` in your [`astro.config.mjs` configuration file](/en/guides/configuring-astro/#the-astro-config-file).

View file

@ -32,7 +32,7 @@ export function moduleIsTopLevelPage(info: ModuleInfo): boolean {
}
// This function walks the dependency graph, going up until it finds a page component.
// This could be a .astro page or a .md page.
// This could be a .astro page, a .markdown or a .md (or really any file extension for markdown files) page.
export function* getTopLevelPages(
id: string,
ctx: { getModuleInfo: GetModuleInfo }

View file

@ -1,3 +1,4 @@
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js';
import type { AstroConfig, AstroSettings } from '../../@types/astro';
import jsxRenderer from '../../jsx/renderer.js';
@ -13,7 +14,7 @@ export function createSettings(config: AstroConfig, cwd?: string): AstroSettings
adapter: undefined,
injectedRoutes: [],
pageExtensions: ['.astro', '.md', '.html'],
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
renderers: [jsxRenderer],
scripts: [],
watchFiles: tsconfig?.exists ? [tsconfig.path, ...tsconfig.extendedPaths] : [],

View file

@ -1,2 +1,12 @@
// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
// possible extensions for markdown files
export const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
'.markdown',
'.mdown',
'.mkdn',
'.mkd',
'.mdwn',
'.md',
] as const;

View file

@ -1,5 +1,6 @@
import npath from 'path';
import vite from 'vite';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js';
import { unwrapId } from '../../util.js';
import { STYLE_EXTENSIONS } from '../util.js';
@ -7,7 +8,7 @@ import { STYLE_EXTENSIONS } from '../util.js';
* List of file extensions signalling we can (and should) SSR ahead-of-time
* See usage below
*/
const fileExtensionsToSSR = new Set(['.astro', '.md']);
const fileExtensionsToSSR = new Set(['.astro', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS]);
const STRIP_QUERY_PARAMS_REGEX = /\?.*$/;

View file

@ -17,6 +17,7 @@ import { warn } from '../../logger/core.js';
import { removeLeadingForwardSlash } from '../../path.js';
import { resolvePages } from '../../util.js';
import { getRouteGenerator } from './generator.js';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js';
const require = createRequire(import.meta.url);
interface Item {
@ -206,7 +207,11 @@ export function createRouteManifest(
): ManifestData {
const components: string[] = [];
const routes: RouteData[] = [];
const validPageExtensions: Set<string> = new Set(['.astro', '.md', ...settings.pageExtensions]);
const validPageExtensions: Set<string> = new Set([
'.astro',
...SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
...settings.pageExtensions,
]);
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
function walk(dir: string, parentSegments: RoutePart[][], parentParams: string[]) {

View file

@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
import { ErrorPayload, normalizePath, ViteDevServer } from 'vite';
import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro';
import { prependForwardSlash, removeTrailingForwardSlash } from './path.js';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
/** Returns true if argument is an object of any prototype/class (but not null). */
export function isObject(value: unknown): value is Record<string, any> {
@ -17,6 +18,23 @@ export function isObject(value: unknown): value is Record<string, any> {
export function isURL(value: unknown): value is URL {
return Object.prototype.toString.call(value) === '[object URL]';
}
/** Check if a file is a markdown file based on its extension */
export function isMarkdownFile(
fileId: string,
option: { criteria: 'endsWith' | 'includes'; suffix?: string }
): boolean {
const _suffix = option.suffix ?? '';
if (option.criteria === 'endsWith') {
for (let markdownFileExtension of SUPPORTED_MARKDOWN_FILE_EXTENSIONS) {
if (fileId.endsWith(`${markdownFileExtension}${_suffix}`)) return true;
}
return false;
}
for (let markdownFileExtension of SUPPORTED_MARKDOWN_FILE_EXTENSIONS) {
if (fileId.includes(`${markdownFileExtension}${_suffix}`)) return true;
}
return false;
}
/** Wraps an object in an array. If an array is passed, ignore it. */
export function arraify<T>(target: T | T[]): T[] {

View file

@ -4,6 +4,7 @@ import type { NodePath } from 'ast-types/lib/node-path';
import { parse, print, types, visit } from 'recast';
import type { Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro';
import { isMarkdownFile } from '../core/util.js';
// Check for `Astro.glob()`. Be very forgiving of whitespace. False positives are okay.
const ASTRO_GLOB_REGEX = /Astro2?\s*\.\s*glob\s*\(/;
@ -16,8 +17,8 @@ export default function astro(_opts: AstroPluginOptions): Plugin {
return {
name: 'astro:postprocess',
async transform(code, id) {
// Currently only supported in ".astro" & ".md" files
if (!id.endsWith('.astro') && !id.endsWith('.md')) {
// Currently only supported in ".astro" and ".md" (or any alternative markdown file extension like `.markdown`) files
if (!id.endsWith('.astro') && !isMarkdownFile(id, { criteria: 'endsWith' })) {
return null;
}

View file

@ -11,7 +11,7 @@ import esbuild from 'esbuild';
import * as colors from 'kleur/colors';
import path from 'path';
import { error } from '../core/logger/core.js';
import { parseNpmName } from '../core/util.js';
import { isMarkdownFile, parseNpmName } from '../core/util.js';
import tagExportsPlugin from './tag.js';
type FixedCompilerOptions = TsConfigJson.CompilerOptions & {
@ -193,7 +193,7 @@ export default function jsx({ settings, logging }: AstroPluginJSXOptions): Plugi
const { mode } = viteConfig;
// Shortcut: only use Astro renderer for MD and MDX files
if (id.includes('.mdx') || id.includes('.md')) {
if (id.includes('.mdx') || isMarkdownFile(id, { criteria: 'includes' })) {
const { code: jsxCode } = await esbuild.transform(code, {
loader: getEsbuildLoader(path.extname(id)) as esbuild.Loader,
jsx: 'preserve',

View file

@ -10,6 +10,7 @@ import { pagesVirtualModuleId } from '../core/app/index.js';
import { cachedCompilation, CompileProps } from '../core/compile/index.js';
import { collectErrorMetadata } from '../core/errors.js';
import type { LogOptions } from '../core/logger/core.js';
import { isMarkdownFile } from '../core/util.js';
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
import { getFileInfo } from '../vite-plugin-utils/index.js';
import {
@ -79,10 +80,10 @@ export default function markdown({ settings }: AstroPluginOptions): Plugin {
styleTransformer.viteDevServer = server;
},
async resolveId(id, importer, options) {
// Resolve any .md files with the `?content` cache buster. This should only come from
// Resolve any .md (or alternative extensions of markdown files like .markdown) files with the `?content` cache buster. This should only come from
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
// Unclear if this is expected or if cache busting is just working around a Vite bug.
if (id.endsWith(`.md${MARKDOWN_CONTENT_FLAG}`)) {
if (isMarkdownFile(id, { criteria: 'endsWith', suffix: MARKDOWN_CONTENT_FLAG })) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
return resolvedId?.id.replace(MARKDOWN_CONTENT_FLAG, '');
}
@ -90,7 +91,7 @@ export default function markdown({ settings }: AstroPluginOptions): Plugin {
// that defers the markdown -> HTML rendering until it is needed. This is especially useful
// when fetching and then filtering many markdown files, like with import.meta.glob() or Astro.glob().
// Otherwise, resolve directly to the actual component.
if (id.endsWith('.md') && !isRootImport(importer)) {
if (isMarkdownFile(id, { criteria: 'endsWith' }) && !isRootImport(importer)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
if (resolvedId) {
return resolvedId.id + MARKDOWN_IMPORT_FLAG;
@ -103,7 +104,7 @@ export default function markdown({ settings }: AstroPluginOptions): Plugin {
// A markdown file has been imported via ESM!
// Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content.
if (id.endsWith(`.md${MARKDOWN_IMPORT_FLAG}`)) {
if (isMarkdownFile(id, { criteria: 'endsWith', suffix: MARKDOWN_IMPORT_FLAG })) {
const { fileId, fileUrl } = getFileInfo(id, config);
const source = await fs.promises.readFile(fileId, 'utf8');
@ -143,7 +144,7 @@ export default function markdown({ settings }: AstroPluginOptions): Plugin {
// A markdown file is being rendered! This markdown file was either imported
// directly as a page in Vite, or it was a deferred render from a JS module.
// This returns the compiled markdown -> astro component that renders to HTML.
if (id.endsWith('.md')) {
if (isMarkdownFile(id, { criteria: 'endsWith' })) {
const filename = normalizeFilename(id);
const source = await fs.promises.readFile(filename, 'utf8');
const renderOpts = config.markdown;

View file

@ -8,6 +8,7 @@ import type { AstroSettings } from '../@types/astro';
import { collectErrorMetadata } from '../core/errors.js';
import type { LogOptions } from '../core/logger/core.js';
import { warn } from '../core/logger/core.js';
import { isMarkdownFile } from '../core/util.js';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
import { getFileInfo, safelyGetAstroData } from '../vite-plugin-utils/index.js';
@ -39,7 +40,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
// passing to the transform hook. This lets us get the truly raw value
// to escape "import.meta.env" ourselves.
async load(id) {
if (id.endsWith('.md')) {
if (isMarkdownFile(id, { criteria: 'endsWith' })) {
const { fileId, fileUrl } = getFileInfo(id, settings.config);
const rawFile = await fs.promises.readFile(fileId, 'utf-8');
const raw = safeMatter(rawFile, id);
@ -63,7 +64,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
warn(
logging,
'markdown',
`[${id}] Astro now supports MDX! Support for components in ".md" files using the "setup" frontmatter is no longer enabled by default. Migrate this file to MDX or add the "legacy.astroFlavoredMarkdown" config flag to re-enable support.`
`[${id}] Astro now supports MDX! Support for components in ".md" (or alternative extensions like ".markdown") files using the "setup" frontmatter is no longer enabled by default. Migrate this file to MDX or add the "legacy.astroFlavoredMarkdown" config flag to re-enable support.`
);
}

View file

@ -0,0 +1,3 @@
# Page with alternative .markdown extension
Hope this loads fine 🤞

View file

@ -0,0 +1,3 @@
# Page with alternative .mdown extension
Hope this loads fine 🤞

View file

@ -0,0 +1,3 @@
# Page with alternative .mdwn extension
Hope this loads fine 🤞

View file

@ -0,0 +1,3 @@
# Page with alternative .mkd extension
Hope this loads fine 🤞

View file

@ -0,0 +1,3 @@
# Page with alternative .mkdn extension
Hope this loads fine 🤞

View file

@ -17,6 +17,41 @@ describe('Markdown tests', () => {
await fixture.build();
});
it('Can load a markdown page with the `.markdown` extension', async () => {
const html = await fixture.readFile('/dot-markdown-page/index.html');
const $ = cheerio.load(html);
expect($('h1').html()).to.equal('Page with alternative .markdown extension');
expect($('p').html()).to.equal('Hope this loads fine 🤞');
});
it('Can load a markdown page with the `.mdwn` extension', async () => {
const html = await fixture.readFile('/dot-mdwn-page/index.html');
const $ = cheerio.load(html);
expect($('h1').html()).to.equal('Page with alternative .mdwn extension');
expect($('p').html()).to.equal('Hope this loads fine 🤞');
});
it('Can load a markdown page with the `.mkdn` extension', async () => {
const html = await fixture.readFile('/dot-mkdn-page/index.html');
const $ = cheerio.load(html);
expect($('h1').html()).to.equal('Page with alternative .mkdn extension');
expect($('p').html()).to.equal('Hope this loads fine 🤞');
});
it('Can load a markdown page with the `.mdown` extension', async () => {
const html = await fixture.readFile('/dot-mdown-page/index.html');
const $ = cheerio.load(html);
expect($('h1').html()).to.equal('Page with alternative .mdown extension');
expect($('p').html()).to.equal('Hope this loads fine 🤞');
});
it('Can load a markdown page with the `.mkd` extension', async () => {
const html = await fixture.readFile('/dot-mkd-page/index.html');
const $ = cheerio.load(html);
expect($('h1').html()).to.equal('Page with alternative .mkd extension');
expect($('p').html()).to.equal('Hope this loads fine 🤞');
});
it('Can load a simple markdown page with Astro', async () => {
const html = await fixture.readFile('/post/index.html');
const $ = cheerio.load(html);

View file

@ -68,7 +68,7 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
recmaPlugins: mdxOptions.recmaPlugins,
jsx: true,
jsxImportSource: 'astro',
// Note: disable `.md` support
// Note: disable `.md` (and other alternative extensions for markdown files like `.markdown`) support
format: 'mdx',
mdExtensions: [],
};