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

feat: add support for other markdown file extensions #5164

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/many-rockets-admire.md
@@ -0,0 +1,11 @@
---
'astro': minor
'@astrojs/rss': patch
---

Add support for markdown files with the following extensions:
- `.markdown`
- `.mdown`
- `.mkdn`
- `.mkd`
- `.mdwn`
4 changes: 2 additions & 2 deletions packages/astro-rss/src/index.ts
Expand Up @@ -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 */
Expand Down Expand Up @@ -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`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would cause a patch for the rss package, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that's file.

);
}
if (!Boolean(frontmatter.title) || !Boolean(frontmatter.pubDate)) {
Expand Down
6 changes: 5 additions & 1 deletion packages/astro-rss/test/rss.test.js
Expand Up @@ -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'
);
}
});
});
Expand Down
109 changes: 98 additions & 11 deletions packages/astro/client-base.d.ts
@@ -1,19 +1,106 @@
/// <reference path="./import-meta.d.ts" />

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()` */
getHeaders: () => void;
Content: MD['Content'];
rawContent: MD['rawContent'];
compiledContent: MD['compiledContent'];
load: MD['default'];
}

declare module '*.md' {
type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}

export const frontmatter: MD['frontmatter'];
export const file: MD['file'];
export const url: MD['url'];
export const 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'];
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;
}

const load: MD['default'];
declare module '*.mdown' {
const { load }: ExportedMarkdownModuleEntities;
export const {
frontmatter,
file,
url,
getHeadings,
getHeaders,
Content,
rawContent,
compiledContent,
}: ExportedMarkdownModuleEntities;
export default load;
}

Expand Down
10 changes: 8 additions & 2 deletions packages/astro/src/@types/astro.ts
@@ -1,3 +1,4 @@
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
import type {
MarkdownHeading,
MarkdownMetadata,
Expand Down Expand Up @@ -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.
Expand All @@ -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[]>;
/**
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/graph.ts
Expand Up @@ -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 }
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/config/settings.ts
@@ -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';
Expand All @@ -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] : [],
Expand Down
10 changes: 10 additions & 0 deletions packages/astro/src/core/constants.ts
@@ -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;
3 changes: 2 additions & 1 deletion packages/astro/src/core/render/dev/vite.ts
@@ -1,13 +1,14 @@
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';

/**
* 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 = /\?.*$/;

Expand Down
7 changes: 6 additions & 1 deletion packages/astro/src/core/routing/manifest/create.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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[]) {
Expand Down
18 changes: 18 additions & 0 deletions packages/astro/src/core/util.ts
Expand Up @@ -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> {
Expand All @@ -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[] {
Expand Down
5 changes: 3 additions & 2 deletions packages/astro/src/vite-plugin-astro-postprocess/index.ts
Expand Up @@ -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*\(/;
Expand All @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/vite-plugin-jsx/index.ts
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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',
Expand Down
11 changes: 6 additions & 5 deletions packages/astro/src/vite-plugin-markdown-legacy/index.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -79,18 +80,18 @@ 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, '');
}
// If the markdown file is imported from another file via ESM, resolve a JS representation
// 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;
Expand All @@ -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');
Expand Down Expand Up @@ -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;
Expand Down