diff --git a/README.md b/README.md
index f683176..51fdafa 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,11 @@ const ast = parseMd('你的 Markdown 文本');
### MDX input
Use `parseMdx` for MDX files. It keeps ESM, JSX, and JavaScript expressions in separate nodes.
+The MDX entry loads `remark-mdx` when the caller imports `@lint-md/parser/mdx`.
+The default entry supports Markdown without loading the MDX parser.
```ts
-import { parseMdx, parseMdxWithSourceMap } from '@lint-md/parser';
+import { parseMdx, parseMdxWithSourceMap } from '@lint-md/parser/mdx';
const input = 'export const x = 1\n\nFix text';
const ast = parseMdx(input);
diff --git a/__tests__/helpers.ts b/__tests__/helpers.ts
index 600cb25..1f058dd 100644
--- a/__tests__/helpers.ts
+++ b/__tests__/helpers.ts
@@ -1,12 +1,16 @@
const parser = require('../dist/lint-md-parser.cjs');
+const mdxParser = require('../dist/lint-md-parser-mdx.cjs');
export const {
parseMd,
- parseMdx,
revertMdAstNode,
stringifyMdAst,
parseMdWithSourceMap,
- parseMdxWithSourceMap,
SourceMapError,
SourceMapConsistencyError,
SourceMapUnavailableError,
} = parser;
+export const {
+ parseMdx,
+ parseMdxWithSourceMap,
+ SourceMapUnavailableError: MdxSourceMapUnavailableError,
+} = mdxParser;
diff --git a/__tests__/mdx.spec.ts b/__tests__/mdx.spec.ts
index 545f39c..43823d3 100644
--- a/__tests__/mdx.spec.ts
+++ b/__tests__/mdx.spec.ts
@@ -2,7 +2,7 @@ import {
parseMd,
parseMdx,
parseMdxWithSourceMap,
- SourceMapUnavailableError,
+ MdxSourceMapUnavailableError,
} from './helpers';
const source = [
@@ -79,7 +79,7 @@ describe('MDX parsing', () => {
expect(sourceMap.getRaw(jsx)).toBe(
'Fix body',
);
- expect(() => sourceMap.getRaw(jsx.attributes[0])).toThrow(SourceMapUnavailableError);
+ expect(() => sourceMap.getRaw(jsx.attributes[0])).toThrow(MdxSourceMapUnavailableError);
});
test('keeps flow JSX and flow expressions outside Markdown text maps', () => {
@@ -104,6 +104,6 @@ describe('MDX parsing', () => {
visit(ast, node => { if (node.type === 'text') textNodes.push(node); });
expect(textNodes.map(node => node.value)).toEqual(['Fix body']);
expect(sourceMap.getRaw(textNodes[0])).toBe('Fix body');
- expect(() => sourceMap.getValueSourceIndex(expression)).toThrow(SourceMapUnavailableError);
+ expect(() => sourceMap.getValueSourceIndex(expression)).toThrow(MdxSourceMapUnavailableError);
});
});
diff --git a/__tests__/source-map/mdx-boundaries.spec.ts b/__tests__/source-map/mdx-boundaries.spec.ts
index 1d3a328..26a103d 100644
--- a/__tests__/source-map/mdx-boundaries.spec.ts
+++ b/__tests__/source-map/mdx-boundaries.spec.ts
@@ -1,7 +1,7 @@
import {
parseMdx,
parseMdxWithSourceMap,
- SourceMapUnavailableError,
+ MdxSourceMapUnavailableError,
} from '../helpers';
const nested = [
@@ -85,14 +85,14 @@ describe('MDX source-map boundaries', () => {
const expression = at(trailing.children, 'mdxTextExpression');
for (const unsupported of [jsx.attributes[0], jsx.attributes[1], expressionAttribute]) {
- expect(() => sourceMap.getRaw(unsupported)).toThrow(SourceMapUnavailableError);
+ expect(() => sourceMap.getRaw(unsupported)).toThrow(MdxSourceMapUnavailableError);
expect(() => sourceMap.getValueSourceIndex(unsupported))
- .toThrow(SourceMapUnavailableError);
+ .toThrow(MdxSourceMapUnavailableError);
}
expect(() => sourceMap.getValueSourceIndex(expression))
- .toThrow(SourceMapUnavailableError);
+ .toThrow(MdxSourceMapUnavailableError);
expect(() => sourceMap.getSourceRange(expression, 0, 1))
- .toThrow(SourceMapUnavailableError);
+ .toThrow(MdxSourceMapUnavailableError);
const spaces = sourceMap.getSourceRange(text, 2, 4);
expect(mixed.slice(spaces.start.offset, spaces.end.offset)).toBe(' ');
diff --git a/__tests__/types/package-exports.mts b/__tests__/types/package-exports.mts
index e3a852c..b96556b 100644
--- a/__tests__/types/package-exports.mts
+++ b/__tests__/types/package-exports.mts
@@ -1,8 +1,6 @@
import {
parseMd,
- parseMdx,
parseMdWithSourceMap,
- parseMdxWithSourceMap,
revertMdAstNode,
stringifyMdAst,
SourceMapError,
@@ -14,14 +12,18 @@ import {
type IndentedCodeSourceInfo,
type MarkdownCodeNode,
type ParsedMarkdownDocument,
- type ParsedMdxDocument,
- type PositionedMdxRoot,
type MarkdownLinkNode,
type MarkdownTextNode,
type MarkdownValueSourceIndex,
type PositionedMarkdownRoot,
type PositionedMarkdownNode,
} from '@lint-md/parser';
+import {
+ parseMdx,
+ parseMdxWithSourceMap,
+ type ParsedMdxDocument,
+ type PositionedMdxRoot,
+} from '@lint-md/parser/mdx';
const root = parseMd('# ESM');
diff --git a/api-extractor.mdx.json b/api-extractor.mdx.json
new file mode 100644
index 0000000..98ea14f
--- /dev/null
+++ b/api-extractor.mdx.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "extends": "./api-extractor.json",
+ "mainEntryPointFilePath": "/dist/tmp/mdx.d.ts",
+ "apiReport": {
+ "enabled": true,
+ "reportFileName": "parser-mdx.api.md",
+ "reportFolder": "/etc/"
+ },
+ "dtsRollup": {
+ "enabled": true,
+ "untrimmedFilePath": "/dist/lint-md-parser-mdx.d.ts"
+ }
+}
diff --git a/docs/mdx-entry-experiment.md b/docs/mdx-entry-experiment.md
new file mode 100644
index 0000000..bf06541
--- /dev/null
+++ b/docs/mdx-entry-experiment.md
@@ -0,0 +1,60 @@
+# MDX entry experiment
+
+This experiment moves the synchronous MDX functions to `@lint-md/parser/mdx`.
+The default entry keeps the Markdown functions.
+Both entries include their own bundled parser code.
+
+## Measurement
+
+The baseline is commit `22b995f`.
+The measurements use Node 24.15.0 and nine fresh processes per case.
+Each process parses about 16 KiB of input.
+The table shows median values.
+
+| Case | Baseline | Split entry |
+| --- | ---: | ---: |
+| Default CJS bytes | 484,137 | 194,515 |
+| Default CJS gzip bytes | 143,556 | 59,141 |
+| Default CJS load time | 14.44 ms | 7.85 ms |
+| Default CJS load heap | 2.06 MB | 1.00 MB |
+| Default ESM bytes | 483,787 | 194,198 |
+| Default ESM gzip bytes | 143,438 | 59,011 |
+| Default ESM load time | 16.17 ms | 9.05 ms |
+| Default ESM load heap | 2.30 MB | 1.24 MB |
+| Cold MDX CJS load time | 14.61 ms | 14.21 ms |
+| Cold MDX ESM load time | 15.14 ms | 15.81 ms |
+| MDX CJS load after default | 0.08 ms | 12.42 ms |
+| MDX CJS load heap after default | 0.01 MB | 1.93 MB |
+| MDX ESM load after default | 0.09 ms | 12.69 ms |
+| MDX ESM load heap after default | 0.01 MB | 1.88 MB |
+
+The four bundled JavaScript files total 1,356,173 bytes after the split.
+The two baseline files total 967,924 bytes.
+This adds 388,249 bytes to the package's uncompressed JavaScript files.
+
+Use this command to compare the default entries:
+
+```bash
+node scripts/bench-markdown-versions.mjs --baseline /path/to/baseline --current . --runs 9 --bytes 16384
+```
+
+Use this command to compare cold MDX loads:
+
+```bash
+node scripts/bench-markdown-versions.mjs --baseline /path/to/baseline --current . --mode mdx --runs 9 --bytes 16384
+```
+
+Add `--preload-default` to measure MDX loading after the default entry.
+Build both checkouts before running the commands.
+
+## Decision
+
+The split cuts default bundle size and load work by about half.
+It gives no clear cold MDX gain.
+It adds a second entry, a second type bundle, and separate error class instances.
+Callers must change MDX imports to `@lint-md/parser/mdx`.
+The import change needs a major release or a migration plan.
+The split also duplicates bundled code across the two entries.
+
+Keep this PR as an experiment until consumers review the import change.
+Adopt it only if default entry load cost matters more than the added package size and API change.
diff --git a/etc/parser-mdx.api.md b/etc/parser-mdx.api.md
new file mode 100644
index 0000000..3034f17
--- /dev/null
+++ b/etc/parser-mdx.api.md
@@ -0,0 +1,217 @@
+## API Report File for "@lint-md/parser"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import type { BlockContent } from 'mdast';
+import { Code } from 'mdast';
+import type { Content } from 'mdast';
+import { Definition } from 'mdast';
+import type { DefinitionContent } from 'mdast';
+import { InlineCode } from 'mdast';
+import { Link } from 'mdast';
+import { ListItem } from 'mdast';
+import type { Literal } from 'mdast';
+import type { MdxFlowExpression } from 'mdast-util-mdx';
+import type { MdxjsEsm } from 'mdast-util-mdx';
+import type { MdxJsxAttribute } from 'mdast-util-mdx';
+import type { MdxJsxAttributeValueExpression } from 'mdast-util-mdx';
+import type { MdxJsxExpressionAttribute } from 'mdast-util-mdx';
+import type { MdxJsxFlowElement } from 'mdast-util-mdx';
+import type { MdxJsxTextElement } from 'mdast-util-mdx';
+import type { MdxTextExpression } from 'mdast-util-mdx';
+import type { Parent } from 'mdast';
+import type { PhrasingContent } from 'mdast';
+import type { Root } from 'mdast';
+import { Text as Text_2 } from 'mdast';
+
+export { Code }
+
+// @public
+export type CodeSourceInfo = FencedCodeSourceInfo | IndentedCodeSourceInfo;
+
+// @public
+export interface FencedCodeSourceInfo {
+ infoInsertPoint: ParsedPoint;
+ kind: 'fenced';
+ openingFence: ParsedPosition;
+}
+
+// @public
+export interface IndentedCodeSourceInfo {
+ kind: 'indented';
+}
+
+export { Link }
+
+export { ListItem }
+
+// @public (undocumented)
+export type MarkdownCodeNode = Code;
+
+// @public (undocumented)
+export interface MarkdownContainerDirective extends Parent, MarkdownDirectiveFields {
+ // (undocumented)
+ children: Array;
+ // (undocumented)
+ type: 'containerDirective';
+}
+
+// @public (undocumented)
+export type MarkdownDefinitionNode = Definition;
+
+// @public (undocumented)
+export interface MarkdownDirectiveFields {
+ // (undocumented)
+ attributes?: Record | null | undefined;
+ // (undocumented)
+ name: string;
+}
+
+// @public (undocumented)
+export type MarkdownInlineCodeNode = InlineCode;
+
+// @public (undocumented)
+export interface MarkdownInlineMath extends Literal {
+ // (undocumented)
+ type: 'inlineMath';
+}
+
+// @public (undocumented)
+export interface MarkdownLeafDirective extends Parent, MarkdownDirectiveFields {
+ // (undocumented)
+ children: PhrasingContent[];
+ // (undocumented)
+ type: 'leafDirective';
+}
+
+// @public (undocumented)
+export type MarkdownLinkNode = Link;
+
+// @public (undocumented)
+export type MarkdownListItemNode = ListItem;
+
+// @public (undocumented)
+export interface MarkdownMath extends Literal {
+ // (undocumented)
+ meta?: string | null | undefined;
+ // (undocumented)
+ type: 'math';
+}
+
+// @public (undocumented)
+export type MarkdownNode = MarkdownRoot | Content;
+
+// @public (undocumented)
+export type MarkdownRoot = Root;
+
+// @public
+export interface MarkdownSourceMap {
+ getCodeSourceInfo(node: MarkdownCodeNode): CodeSourceInfo;
+ getFieldSourceRange(node: MarkdownLinkNode | MarkdownDefinitionNode, field: 'url', valueStart: number, valueEnd: number): ParsedPosition;
+ getRaw(node: MarkdownNode | MarkdownTextNode | MarkdownInlineCodeNode | MarkdownCodeNode | MarkdownLinkNode | MarkdownDefinitionNode): string;
+ getSourceRange(node: MarkdownTextNode | MarkdownInlineCodeNode | MarkdownCodeNode, valueStart: number, valueEnd: number): ParsedPosition;
+ getValueSourceIndex(node: MarkdownTextNode | MarkdownInlineCodeNode | MarkdownCodeNode): MarkdownValueSourceIndex;
+}
+
+// @public (undocumented)
+export interface MarkdownTextDirective extends Parent, MarkdownDirectiveFields {
+ // (undocumented)
+ children: PhrasingContent[];
+ // (undocumented)
+ type: 'textDirective';
+}
+
+// @public (undocumented)
+export type MarkdownTextNode = Text_2;
+
+// @public
+export interface MarkdownValueSourceIndex {
+ sourceOffsetAt(valueIndex: number): number;
+}
+
+export { MdxFlowExpression }
+
+export { MdxjsEsm }
+
+export { MdxJsxAttribute }
+
+export { MdxJsxAttributeValueExpression }
+
+export { MdxJsxExpressionAttribute }
+
+export { MdxJsxFlowElement }
+
+export { MdxJsxTextElement }
+
+export { MdxTextExpression }
+
+// @public
+export interface ParsedMdxDocument {
+ ast: PositionedMdxRoot;
+ sourceMap: MarkdownSourceMap;
+}
+
+// @public
+export interface ParsedPoint {
+ column: number;
+ line: number;
+ offset: number;
+}
+
+// @public
+export interface ParsedPosition {
+ end: ParsedPoint;
+ start: ParsedPoint;
+}
+
+// @public
+export const parseMdx: (mdx: string) => PositionedMdxRoot;
+
+// @public
+export const parseMdxWithSourceMap: (mdx: string) => ParsedMdxDocument;
+
+// @public
+export type Positioned = T extends {
+ children: Array;
+} ? Omit & {
+ position: ParsedPosition;
+ children: Array>;
+} : Omit & {
+ position: ParsedPosition;
+};
+
+// @public
+export type PositionedMarkdownNode = Positioned;
+
+// @public
+export type PositionedMarkdownRoot = Positioned;
+
+// @public
+export type PositionedMdxRoot = Positioned;
+
+// @public
+export class SourceMapConsistencyError extends SourceMapError {
+ constructor(message?: string);
+}
+
+// @public
+export class SourceMapError extends RangeError {
+ constructor(code: SourceMapErrorCode, message: string);
+ readonly code: SourceMapErrorCode;
+}
+
+// @public
+export type SourceMapErrorCode = 'ERR_SOURCE_MAP_CONSISTENCY' | 'ERR_SOURCE_MAP_UNAVAILABLE';
+
+// @public
+export class SourceMapUnavailableError extends SourceMapError {
+ constructor(message?: string);
+}
+
+export { Text_2 as Text }
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/etc/parser.api.md b/etc/parser.api.md
index 2243fee..05f426d 100644
--- a/etc/parser.api.md
+++ b/etc/parser.api.md
@@ -153,12 +153,6 @@ export interface ParsedMarkdownDocument {
sourceMap: MarkdownSourceMap;
}
-// @public
-export interface ParsedMdxDocument {
- ast: PositionedMdxRoot;
- sourceMap: MarkdownSourceMap;
-}
-
// @public
export interface ParsedPoint {
column: number;
@@ -178,12 +172,6 @@ export const parseMd: (md: string) => PositionedMarkdownRoot;
// @public
export const parseMdWithSourceMap: (md: string) => ParsedMarkdownDocument;
-// @public
-export const parseMdx: (mdx: string) => PositionedMdxRoot;
-
-// @public
-export const parseMdxWithSourceMap: (mdx: string) => ParsedMdxDocument;
-
// @public
export type Positioned = T extends {
children: Array;
diff --git a/package.json b/package.json
index a7ee7f3..762c1d0 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,16 @@
"types": "./dist/lint-md-parser.d.cts",
"default": "./dist/lint-md-parser.cjs"
}
+ },
+ "./mdx": {
+ "import": {
+ "types": "./dist/lint-md-parser-mdx.d.mts",
+ "default": "./dist/lint-md-parser-mdx.mjs"
+ },
+ "require": {
+ "types": "./dist/lint-md-parser-mdx.d.cts",
+ "default": "./dist/lint-md-parser-mdx.cjs"
+ }
}
},
"files": [
@@ -36,15 +46,21 @@
"dist/lint-md-parser.d.ts",
"dist/lint-md-parser.d.cts",
"dist/lint-md-parser.d.mts",
+ "dist/lint-md-parser-mdx.cjs",
+ "dist/lint-md-parser-mdx.mjs",
+ "dist/lint-md-parser-mdx.d.ts",
+ "dist/lint-md-parser-mdx.d.cts",
+ "dist/lint-md-parser-mdx.d.mts",
"dist/tsdoc-metadata.json"
],
"scripts": {
"watch": "tsc -w",
"build": "rm -rf dist && pnpm run build:dist && pnpm run build:ts-declaration",
- "build:dist": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/lint-md-parser.cjs --minify && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/lint-md-parser.mjs --minify",
- "build:ts-declaration": "tsc && api-extractor run --local --verbose && cp dist/lint-md-parser.d.ts dist/lint-md-parser.d.cts && cp dist/lint-md-parser.d.ts dist/lint-md-parser.d.mts && for f in dist/lint-md-parser.d.ts dist/lint-md-parser.d.cts dist/lint-md-parser.d.mts; do cat scripts/mdast-augment.d.ts >> \"$f\"; done",
+ "build:dist": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/lint-md-parser.cjs --minify && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/lint-md-parser.mjs --minify && esbuild src/mdx.ts --bundle --platform=node --format=cjs --outfile=dist/lint-md-parser-mdx.cjs --minify && esbuild src/mdx.ts --bundle --platform=node --format=esm --outfile=dist/lint-md-parser-mdx.mjs --minify",
+ "build:ts-declaration": "tsc && api-extractor run --local --verbose && api-extractor run --local --verbose --config api-extractor.mdx.json && cp dist/lint-md-parser.d.ts dist/lint-md-parser.d.cts && cp dist/lint-md-parser.d.ts dist/lint-md-parser.d.mts && cp dist/lint-md-parser-mdx.d.ts dist/lint-md-parser-mdx.d.cts && cp dist/lint-md-parser-mdx.d.ts dist/lint-md-parser-mdx.d.mts && for f in dist/lint-md-parser.d.ts dist/lint-md-parser.d.cts dist/lint-md-parser.d.mts dist/lint-md-parser-mdx.d.ts dist/lint-md-parser-mdx.d.cts dist/lint-md-parser-mdx.d.mts; do cat scripts/mdast-augment.d.ts >> \"$f\"; done",
"lint": "eslint --ext js,jsx,ts,tsx ./src",
- "test": "jest && pnpm run test:types && pnpm run test:package && pnpm run test:mdx3",
+ "test": "jest && pnpm run test:types && pnpm run test:package && pnpm run test:mdx3 && pnpm run test:bench-entry",
+ "test:bench-entry": "node scripts/test-bench-markdown-versions.mjs",
"test:mdx3": "node scripts/test-mdx3.mjs",
"test:types": "tsc -p __tests__/types/tsconfig.json",
"test:package": "publint && node scripts/test-package.mjs",
diff --git a/scripts/bench-markdown-versions.mjs b/scripts/bench-markdown-versions.mjs
index b88e8a3..ea9c587 100644
--- a/scripts/bench-markdown-versions.mjs
+++ b/scripts/bench-markdown-versions.mjs
@@ -1,5 +1,5 @@
#!/usr/bin/env node
-// Compare cold Markdown calls from two built parser packages.
+// Compare cold Markdown or MDX calls from two built parser packages.
// Run bench-parse.mjs in each checkout for warm parse and source-map costs.
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
@@ -7,27 +7,36 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import fs from 'node:fs';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
+import { gzipSync } from 'node:zlib';
const SCRIPT = fileURLToPath(import.meta.url);
const FIXTURE = '# 标题\n\n这是**粗体**和*斜体*混合的段落文字。\n\n- 列表项 [链接](https://example.com)\n\n';
+const MDX_BLOCK = '\n\n中文 **正文** {x}\n\n';
const FORMATS = [
{ name: 'cjs', file: 'lint-md-parser.cjs' },
{ name: 'esm', file: 'lint-md-parser.mjs' },
];
-const FUNCTIONS = ['parseMd', 'parseMdWithSourceMap'];
+const MODES = {
+ markdown: ['parseMd', 'parseMdWithSourceMap'],
+ mdx: ['parseMdx', 'parseMdxWithSourceMap'],
+};
function parseArgs() {
const args = process.argv.slice(2);
- const options = { runs: 7, bytes: 16 * 1024 };
+ const options = { runs: 7, bytes: 16 * 1024, mode: 'markdown', preloadDefault: false };
for (let index = 0; index < args.length; index += 1) {
const flag = args[index];
if (flag === '--help') {
- console.log('Usage: node scripts/bench-markdown-versions.mjs --baseline --current [--runs ] [--bytes ]');
+ console.log('Usage: node scripts/bench-markdown-versions.mjs --baseline --current [--runs ] [--bytes ] [--mode markdown|mdx] [--preload-default] [--baseline-entry default|mdx]');
process.exit(0);
}
- if (!['--baseline', '--current', '--runs', '--bytes'].includes(flag))
+ if (flag === '--preload-default') {
+ options.preloadDefault = true;
+ continue;
+ }
+ if (!['--baseline', '--current', '--runs', '--bytes', '--mode', '--baseline-entry'].includes(flag))
throw new Error(`Unknown option: ${flag}`);
- options[flag.slice(2)] = args[++index];
+ options[flag === '--baseline-entry' ? 'baselineEntry' : flag.slice(2)] = args[++index];
}
options.runs = Number(options.runs);
options.bytes = Number(options.bytes);
@@ -37,6 +46,11 @@ function parseArgs() {
throw new Error('--runs must be a positive integer.');
if (!Number.isSafeInteger(options.bytes) || options.bytes < 1)
throw new Error('--bytes must be a positive integer.');
+ if (!(options.mode in MODES))
+ throw new Error('--mode must be markdown or mdx.');
+ options.baselineEntry ??= 'default';
+ if (!['default', 'mdx'].includes(options.baselineEntry))
+ throw new Error('--baseline-entry must be default or mdx.');
return options;
}
@@ -47,17 +61,25 @@ function median(values) {
}
async function runChild() {
- const { BENCH_FILE, BENCH_FORMAT, BENCH_FUNCTION, BENCH_BYTES } = process.env;
+ const {
+ BENCH_FILE, BENCH_FORMAT, BENCH_FUNCTION, BENCH_BYTES,
+ BENCH_MODE, BENCH_PRELOAD_FILE,
+ } = process.env;
const targetBytes = Number(BENCH_BYTES);
- const fixtureBytes = Buffer.byteLength(FIXTURE);
- const input = FIXTURE.repeat(Math.floor(targetBytes / fixtureBytes))
- + 'a'.repeat(targetBytes % fixtureBytes);
+ const input = BENCH_MODE === 'mdx'
+ ? 'export const x = 42\n\n' + MDX_BLOCK.repeat(
+ Math.max(1, Math.ceil(targetBytes / Buffer.byteLength(MDX_BLOCK))),
+ )
+ : FIXTURE.repeat(Math.floor(targetBytes / Buffer.byteLength(FIXTURE)))
+ + 'a'.repeat(targetBytes % Buffer.byteLength(FIXTURE));
+ const load = file => BENCH_FORMAT === 'cjs'
+ ? createRequire(import.meta.url)(file)
+ : import(pathToFileURL(file).href);
+ if (BENCH_PRELOAD_FILE) await load(BENCH_PRELOAD_FILE);
global.gc?.();
const before = process.memoryUsage();
const start = performance.now();
- const parser = BENCH_FORMAT === 'cjs'
- ? createRequire(import.meta.url)(BENCH_FILE)
- : await import(pathToFileURL(BENCH_FILE).href);
+ const parser = await load(BENCH_FILE);
const loadedAt = performance.now();
const afterLoad = process.memoryUsage();
const result = parser[BENCH_FUNCTION](input);
@@ -77,8 +99,18 @@ async function runChild() {
})}\n`);
}
-function runSample(root, format, functionName, bytes) {
- const file = path.resolve(root, 'dist', format.file);
+function entryFile(root, format, entry) {
+ const name = entry === 'mdx'
+ ? `lint-md-parser-mdx.${format.name === 'esm' ? 'mjs' : 'cjs'}`
+ : format.file;
+ return path.resolve(root, 'dist', name);
+}
+
+function runSample(root, version, format, functionName, options) {
+ const entry = options.mode === 'mdx'
+ ? (version === 'baseline' ? options.baselineEntry : 'mdx')
+ : 'default';
+ const file = entryFile(root, format, entry);
if (!fs.existsSync(file))
throw new Error(`Build the package first: ${file}`);
const child = spawnSync(process.execPath, ['--expose-gc', SCRIPT], {
@@ -86,9 +118,12 @@ function runSample(root, format, functionName, bytes) {
...process.env,
BENCH_MARKDOWN_CHILD: '1',
BENCH_FILE: file,
+ BENCH_PRELOAD_FILE: options.preloadDefault
+ ? entryFile(root, format, 'default') : '',
BENCH_FORMAT: format.name,
BENCH_FUNCTION: functionName,
- BENCH_BYTES: String(bytes),
+ BENCH_BYTES: String(options.bytes),
+ BENCH_MODE: options.mode,
},
encoding: 'utf8',
});
@@ -99,15 +134,16 @@ function runSample(root, format, functionName, bytes) {
function printResults(options) {
const samples = { baseline: {}, current: {} };
+ const functions = MODES[options.mode];
for (const format of FORMATS) {
- for (const functionName of FUNCTIONS) {
+ for (const functionName of functions) {
const key = `${format.name}/${functionName}`;
samples.baseline[key] = [];
samples.current[key] = [];
for (let index = 0; index < options.runs; index += 1) {
const order = index % 2 ? ['current', 'baseline'] : ['baseline', 'current'];
for (const version of order)
- samples[version][key].push(runSample(options[version], format, functionName, options.bytes));
+ samples[version][key].push(runSample(options[version], version, format, functionName, options));
}
}
}
@@ -115,8 +151,12 @@ function printResults(options) {
for (const version of ['baseline', 'current']) {
summary[version] = {};
for (const format of FORMATS) {
- const file = path.resolve(options[version], 'dist', format.file);
+ const entry = options.mode === 'mdx'
+ ? (version === 'baseline' ? options.baselineEntry : 'mdx')
+ : 'default';
+ const file = entryFile(options[version], format, entry);
summary[version][`${format.name}Bytes`] = fs.statSync(file).size;
+ summary[version][`${format.name}GzipBytes`] = gzipSync(fs.readFileSync(file)).length;
}
for (const [key, rows] of Object.entries(samples[version])) {
summary[version][key] = {};
@@ -126,6 +166,8 @@ function printResults(options) {
}
console.log(JSON.stringify({
node: process.version,
+ mode: options.mode,
+ preloadDefault: options.preloadDefault,
bytes: options.bytes,
runs: options.runs,
summary,
diff --git a/scripts/test-bench-markdown-versions.mjs b/scripts/test-bench-markdown-versions.mjs
index 24ffa3f..85ae313 100644
--- a/scripts/test-bench-markdown-versions.mjs
+++ b/scripts/test-bench-markdown-versions.mjs
@@ -30,3 +30,30 @@ for (const version of ['baseline', 'current']) {
}
}
console.log('Markdown comparison smoke test passed.');
+
+const mdxChild = spawnSync(process.execPath, [
+ script,
+ '--baseline', root,
+ '--current', root,
+ '--runs', '1',
+ '--bytes', '1024',
+ '--mode', 'mdx',
+ '--baseline-entry', 'mdx',
+ '--preload-default',
+], { encoding: 'utf8' });
+
+assert.equal(mdxChild.status, 0, mdxChild.stderr);
+const mdxResult = JSON.parse(mdxChild.stdout);
+assert.equal(mdxResult.mode, 'mdx');
+assert.equal(mdxResult.preloadDefault, true);
+for (const version of ['baseline', 'current']) {
+ assert.ok(mdxResult.summary[version].cjsGzipBytes > 0);
+ for (const format of ['cjs', 'esm']) {
+ for (const name of ['parseMdx', 'parseMdxWithSourceMap']) {
+ const sample = mdxResult.summary[version][`${format}/${name}`];
+ assert.ok(sample.coldTotalMs > 0);
+ assert.ok(sample.rssBytes > 0);
+ }
+ }
+}
+console.log('MDX comparison smoke test passed.');
diff --git a/scripts/test-mdx3.mjs b/scripts/test-mdx3.mjs
index 64a8300..84d4712 100644
--- a/scripts/test-mdx3.mjs
+++ b/scripts/test-mdx3.mjs
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { compile } from '@mdx-js/mdx';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
-import { parseMdx, parseMdxWithSourceMap } from '../dist/lint-md-parser.mjs';
+import { parseMdx, parseMdxWithSourceMap } from '../dist/lint-md-parser-mdx.mjs';
const source = [
'export const 用户ID = 42',
diff --git a/scripts/test-package.mjs b/scripts/test-package.mjs
index c7ed12f..a7c0497 100644
--- a/scripts/test-package.mjs
+++ b/scripts/test-package.mjs
@@ -48,6 +48,12 @@ try {
]);
assert.equal(cjsResult, 'root');
+ const defaultMdxResult = run('node', [
+ '-e',
+ "const parser = require('@lint-md/parser'); process.stdout.write(String(parser.parseMdx))",
+ ]);
+ assert.equal(defaultMdxResult, 'undefined');
+
const cjsStringifyResult = run('node', [
'-e',
"const parser = require('@lint-md/parser'); process.stdout.write(String(parser.stringifyMdAst === parser.revertMdAstNode))",
@@ -64,10 +70,16 @@ try {
const mdxResult = run('node', [
'--input-type=module',
'-e',
- "import { parseMdx, parseMdxWithSourceMap } from '@lint-md/parser'; const input = ''; process.stdout.write([parseMdx(input).children[0].type, parseMdxWithSourceMap(input).ast.children[0].type].join(','))",
+ "import { parseMdx, parseMdxWithSourceMap } from '@lint-md/parser/mdx'; const input = ''; process.stdout.write([parseMdx(input).children[0].type, parseMdxWithSourceMap(input).ast.children[0].type].join(','))",
]);
assert.equal(mdxResult, 'mdxJsxFlowElement,mdxJsxFlowElement');
+ const cjsMdxResult = run('node', [
+ '-e',
+ "const parser = require('@lint-md/parser/mdx'); const input = ''; const doc = parser.parseMdxWithSourceMap(input); let code = ''; try { doc.sourceMap.getRaw(doc.ast.children[0].attributes[0]); } catch (error) { code = error.code; } process.stdout.write([parser.parseMdx(input).children[0].type, code].join(','))",
+ ]);
+ assert.equal(cjsMdxResult, 'mdxJsxFlowElement,ERR_SOURCE_MAP_UNAVAILABLE');
+
const esmStringifyResult = run('node', [
'--input-type=module',
'-e',
@@ -107,7 +119,8 @@ try {
' MarkdownRoot,',
' MarkdownTextDirective,',
"} from '@lint-md/parser';",
- "import { parseMd, parseMdx, revertMdAstNode, stringifyMdAst } from '@lint-md/parser';",
+ "import { parseMd, revertMdAstNode, stringifyMdAst } from '@lint-md/parser';",
+ "import { parseMdx } from '@lint-md/parser/mdx';",
'',
'const markdown: string = revertMdAstNode(parseMd("# ESM"));',
'const same: boolean = stringifyMdAst === revertMdAstNode;',
@@ -196,6 +209,7 @@ try {
' MarkdownTextDirective,',
"} from '@lint-md/parser';",
"import parser = require('@lint-md/parser');",
+ "import mdx = require('@lint-md/parser/mdx');",
'',
'const markdown: string = parser.revertMdAstNode(parser.parseMd("# CJS"));',
'const same: boolean = parser.stringifyMdAst === parser.revertMdAstNode;',
@@ -205,6 +219,8 @@ try {
'function assertType(_val: T): void {}',
'',
'const root = parser.parseMd("# test");',
+ 'const mdxRoot = mdx.parseMdx("");',
+ 'void mdxRoot;',
'',
'for (const child of root.children) {',
' if (child.type === "yaml") {',
diff --git a/src/index.ts b/src/index.ts
index e684477..37528cf 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,5 @@
-export { parseMd, parseMdx, revertMdAstNode, revertMdAstNode as stringifyMdAst } from './parse-md';
-export { parseMdWithSourceMap, parseMdxWithSourceMap } from './source-map/build-source-map';
+export { parseMd, revertMdAstNode, revertMdAstNode as stringifyMdAst } from './parse-md';
+export { parseMdWithSourceMap } from './source-map/build-source-map';
export {
SourceMapError,
SourceMapConsistencyError,
@@ -15,5 +15,4 @@ export type {
MarkdownSourceMap,
MarkdownValueSourceIndex,
ParsedMarkdownDocument,
- ParsedMdxDocument,
} from './source-map/types';
diff --git a/src/mdx-config.ts b/src/mdx-config.ts
new file mode 100644
index 0000000..ca0aacd
--- /dev/null
+++ b/src/mdx-config.ts
@@ -0,0 +1,11 @@
+import remarkMdx from 'remark-mdx';
+import { createParserProcessor, getProcessorExtensions } from './remark-config';
+
+/** Add MDX syntax only when a caller loads the MDX entry. */
+export const createMdxProcessor = (): ReturnType => {
+ // The MDX 3 test dependency exposes unified 11 types. remark-mdx 2 uses unified 10 at runtime.
+ return createParserProcessor().use(remarkMdx as any);
+};
+
+export const getMdxParserExtensions = () =>
+ getProcessorExtensions(createMdxProcessor());
diff --git a/src/mdx.ts b/src/mdx.ts
new file mode 100644
index 0000000..4a3962f
--- /dev/null
+++ b/src/mdx.ts
@@ -0,0 +1,17 @@
+export { parseMdx } from './parse-mdx';
+export { parseMdxWithSourceMap } from './source-map/parse-mdx-with-source-map';
+export {
+ SourceMapError,
+ SourceMapConsistencyError,
+ SourceMapUnavailableError,
+ type SourceMapErrorCode,
+} from './source-map/errors';
+export * from './types';
+export type {
+ CodeSourceInfo,
+ FencedCodeSourceInfo,
+ IndentedCodeSourceInfo,
+ MarkdownSourceMap,
+ MarkdownValueSourceIndex,
+ ParsedMdxDocument,
+} from './source-map/types';
diff --git a/src/parse-md.ts b/src/parse-md.ts
index 0c08fc2..5bcdf8f 100644
--- a/src/parse-md.ts
+++ b/src/parse-md.ts
@@ -1,8 +1,7 @@
import { createParserProcessor } from './remark-config';
-import type { MarkdownRoot, PositionedMarkdownRoot, PositionedMdxRoot } from './types';
+import type { MarkdownRoot, PositionedMarkdownRoot } from './types';
const depsLink = createParserProcessor();
-const mdxProcessor = createParserProcessor('mdx');
/**
* 将 Markdown 解析成 ast
@@ -32,15 +31,6 @@ export const parseMd = (md: string): PositionedMarkdownRoot => {
return depsLink.parse(md) as PositionedMarkdownRoot;
};
-/**
- * Parse MDX into positioned Markdown and MDX syntax nodes.
- * Use this function for MDX input so JSX and expressions stay distinct.
- *
- * @public
- */
-export const parseMdx = (mdx: string): PositionedMdxRoot =>
- mdxProcessor.parse(mdx) as PositionedMdxRoot;
-
/**
* 将 ast 解析成 markdown
*
diff --git a/src/parse-mdx.ts b/src/parse-mdx.ts
new file mode 100644
index 0000000..4941756
--- /dev/null
+++ b/src/parse-mdx.ts
@@ -0,0 +1,8 @@
+import { createMdxProcessor } from './mdx-config';
+import type { PositionedMdxRoot } from './types';
+
+const mdxProcessor = createMdxProcessor();
+
+/** Parse MDX into positioned Markdown and MDX syntax nodes. @public */
+export const parseMdx = (mdx: string): PositionedMdxRoot =>
+ mdxProcessor.parse(mdx) as PositionedMdxRoot;
diff --git a/src/remark-config.ts b/src/remark-config.ts
index 3c68b0d..0448ccc 100644
--- a/src/remark-config.ts
+++ b/src/remark-config.ts
@@ -3,7 +3,6 @@ import remarkGfm from 'remark-gfm';
import remarkDirective from 'remark-directive';
import remarkMath from 'remark-math';
import { remark } from 'remark';
-import remarkMdx from 'remark-mdx';
import { gfmAutolinkLiteralFromMarkdown } from 'mdast-util-gfm-autolink-literal';
/**
@@ -87,18 +86,13 @@ const positionSafeGfm = function positionSafeGfm(this: {
* Each call returns an independent processor whose own data holds a fresh
* clone of the autolink extension — no shared mutable state between calls.
*/
-export type ParserSyntax = 'markdown' | 'mdx';
-
-export const createParserProcessor = (syntax: ParserSyntax = 'markdown'): ReturnType => {
- const processor = remark()
+export const createParserProcessor = (): ReturnType =>
+ remark()
.use(frontmatter)
.use(remarkGfm)
.use(positionSafeGfm)
.use(remarkDirective)
.use(remarkMath);
- // The MDX 3 test dependency exposes unified 11 types. remark-mdx 2 uses unified 10 at runtime.
- return syntax === 'mdx' ? processor.use(remarkMdx as any) : processor;
-};
export interface ParserExtensions {
micromarkExtensions: unknown[]
@@ -112,8 +106,9 @@ export interface ParserExtensions {
* kept literal). Reusing this frozen data is what keeps
* `parseMdWithSourceMap`'s AST identical to `parseMd`'s.
*/
-export const getParserExtensions = (syntax: ParserSyntax = 'markdown'): ParserExtensions => {
- const frozen = createParserProcessor(syntax);
+export const getProcessorExtensions = (
+ frozen: ReturnType,
+): ParserExtensions => {
frozen.freeze();
const data = frozen.data();
return {
@@ -121,3 +116,6 @@ export const getParserExtensions = (syntax: ParserSyntax = 'markdown'): ParserEx
fromMarkdownExtensions: (data.fromMarkdownExtensions as unknown[]) || [],
};
};
+
+export const getParserExtensions = (): ParserExtensions =>
+ getProcessorExtensions(createParserProcessor());
diff --git a/src/source-map/build-source-map.ts b/src/source-map/build-source-map.ts
index 559cffc..b1ffa46 100644
--- a/src/source-map/build-source-map.ts
+++ b/src/source-map/build-source-map.ts
@@ -11,7 +11,7 @@ import type {
ParsedPosition,
PositionedMarkdownRoot,
} from '../types';
-import { getParserExtensions } from '../remark-config';
+import { type ParserExtensions, getParserExtensions } from '../remark-config';
import {
SourceMapConsistencyError,
SourceMapUnavailableError,
@@ -24,7 +24,6 @@ import type {
MarkdownSourceMapSegment,
MarkdownValueSourceIndex,
ParsedMarkdownDocument,
- ParsedMdxDocument,
} from './types';
// Use the same parser extensions as `parseMd`.
@@ -33,7 +32,6 @@ import type {
// The replacements mirror the matching `mdast-util-from-markdown` AST behavior.
// All other tokens use the standard handlers.
const markdownExtensions = getParserExtensions();
-const mdxExtensions = getParserExtensions('mdx');
interface RecordingState {
/** Markdown source used for parser-confirmed indentation spans. */
@@ -335,9 +333,9 @@ function resolveSegmentRange(options: SegmentRangeOptions): ParsedPosition {
*
* @public
*/
-const parseWithSourceMap = (
+export const parseWithSourceMap = (
md: string,
- syntax: 'markdown' | 'mdx',
+ { micromarkExtensions, fromMarkdownExtensions }: ParserExtensions,
): ParsedMarkdownDocument => {
const state: RecordingState = {
source: md,
@@ -350,9 +348,6 @@ const parseWithSourceMap = (
emptyUrlOffsets: new WeakMap(),
};
- const { micromarkExtensions, fromMarkdownExtensions } = syntax === 'mdx'
- ? mdxExtensions
- : markdownExtensions;
const tree = fromMarkdown(md, {
extensions: micromarkExtensions as any,
mdastExtensions: [
@@ -705,8 +700,4 @@ const parseWithSourceMap = (
/** Parse Markdown and map supported values to source positions. @public */
export const parseMdWithSourceMap = (md: string): ParsedMarkdownDocument =>
- parseWithSourceMap(md, 'markdown');
-
-/** Parse MDX and map Markdown values to source positions. @public */
-export const parseMdxWithSourceMap = (mdx: string): ParsedMdxDocument =>
- parseWithSourceMap(mdx, 'mdx');
+ parseWithSourceMap(md, markdownExtensions);
diff --git a/src/source-map/parse-mdx-with-source-map.ts b/src/source-map/parse-mdx-with-source-map.ts
new file mode 100644
index 0000000..f79d05d
--- /dev/null
+++ b/src/source-map/parse-mdx-with-source-map.ts
@@ -0,0 +1,9 @@
+import { getMdxParserExtensions } from '../mdx-config';
+import { parseWithSourceMap } from './build-source-map';
+import type { ParsedMdxDocument } from './types';
+
+const mdxExtensions = getMdxParserExtensions();
+
+/** Parse MDX and map Markdown values to source positions. @public */
+export const parseMdxWithSourceMap = (mdx: string): ParsedMdxDocument =>
+ parseWithSourceMap(mdx, mdxExtensions);