Skip to content

Commit

Permalink
feat: add support for other markdown file extensions (#5164)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MoustaphaDev committed Oct 26, 2022
1 parent d151d9f commit 4a8a346
Show file tree
Hide file tree
Showing 22 changed files with 228 additions and 32 deletions.
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`
);
}
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

0 comments on commit 4a8a346

Please sign in to comment.