Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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\n<Card title="Keep spaces">Fix text</Card>';
const ast = parseMdx(input);
Expand Down
8 changes: 6 additions & 2 deletions __tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 3 additions & 3 deletions __tests__/mdx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
parseMd,
parseMdx,
parseMdxWithSourceMap,
SourceMapUnavailableError,
MdxSourceMapUnavailableError,
} from './helpers';

const source = [
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('MDX parsing', () => {
expect(sourceMap.getRaw(jsx)).toBe(
'<Card title="Keep spaces" count={1 + 2}>Fix body</Card>',
);
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', () => {
Expand All @@ -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);
});
});
10 changes: 5 additions & 5 deletions __tests__/source-map/mdx-boundaries.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
parseMdx,
parseMdxWithSourceMap,
SourceMapUnavailableError,
MdxSourceMapUnavailableError,
} from '../helpers';

const nested = [
Expand Down Expand Up @@ -85,14 +85,14 @@ describe('MDX source-map boundaries', () => {
const expression = at<any>(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(' ');
Expand Down
10 changes: 6 additions & 4 deletions __tests__/types/package-exports.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
parseMd,
parseMdx,
parseMdWithSourceMap,
parseMdxWithSourceMap,
revertMdAstNode,
stringifyMdAst,
SourceMapError,
Expand All @@ -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');

Expand Down
14 changes: 14 additions & 0 deletions api-extractor.mdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "./api-extractor.json",
"mainEntryPointFilePath": "<projectFolder>/dist/tmp/mdx.d.ts",
"apiReport": {
"enabled": true,
"reportFileName": "parser-mdx.api.md",
"reportFolder": "<projectFolder>/etc/"
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/lint-md-parser-mdx.d.ts"
}
}
60 changes: 60 additions & 0 deletions docs/mdx-entry-experiment.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading