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: 0 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,6 @@ Nesting the markdown exposes a few pipeline limits:
- A `types.ts` outside an indexed docs section must pass `{ excludeFromIndex: true }`, or the
types loader creates a parent index page (`docs/app/page.mdx` shadowed the landing route).
It must also import the component by relative path; the loader resolves `@/` as a package.
- In fenced code, a trailing `// @highlight` on the **last** line drops that line, and
`createDemo(import.meta.url, X, {` followed by emphasis comments disables them for the
block. Wrap the statement in `@highlight-start` / `@highlight-end`, or put each argument
on its own line.
- The landing demos use their own `createDemo` (`docs/app/(shared)/demos/createDemo.ts`) with
`fallbackUsesExtraFiles`, so the page backs its claim that every file of a demo is in the
initial HTML as plain text. A `ContentLoading` must take `extraSource` from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const e = 5; // @highlight`,
</span><span class="frame" data-lined=""><span class="line" data-ln="2"><span class="pl-k">const</span> <span class="pl-c1">b</span> <span class="pl-k fgd-pu">=</span> <span class="pl-c1 fgd-num">2</span>;</span>
</span><span class="frame" data-lined="" data-frame-type="highlighted-unfocused"><span class="line" data-ln="3"><span class="pl-k">const</span> <span class="pl-c1">c</span> <span class="pl-k fgd-pu">=</span> <span class="pl-c1 fgd-num">3</span>;</span>
</span><span class="frame" data-lined=""><span class="line" data-ln="4"><span class="pl-k">const</span> <span class="pl-c1">d</span> <span class="pl-k fgd-pu">=</span> <span class="pl-c1 fgd-num">4</span>;</span>
</span>"
</span><span class="frame" data-lined="" data-frame-type="highlighted-unfocused"><span class="line" data-ln="5"><span class="pl-k">const</span> <span class="pl-c1">e</span> <span class="pl-k fgd-pu">=</span> <span class="pl-c1 fgd-num">5</span>;</span></span>"
`,
);
});
Expand Down Expand Up @@ -488,7 +488,7 @@ const e = 5; // @highlight`,
);

expect(result).toMatchInlineSnapshot(
`"<span class="frame" data-lined="" data-frame-type="focus"><span class="line" data-ln="1">&#x3C;<span class="pl-c1 fgd-jsx">Input</span> <mark class="pl-e fgd-ak">value</mark><span class="pl-k fgd-pu fgd-ae">=</span><span class="pl-pse">{</span><span class="pl-smi fgd-jv">a</span><span class="pl-pse">}</span> /> &#x3C;<span class="pl-c1 fgd-jsx">Input</span> <mark class="pl-e fgd-ak">value</mark><span class="pl-k fgd-pu fgd-ae">=</span><span class="pl-pse">{</span><span class="pl-smi fgd-jv">b</span><span class="pl-pse">}</span> /> <span class="pl-c">// @highlight-text "<mark>value</mark>"</span></span></span>"`,
`"<span class="frame" data-lined="" data-frame-type="focus"><span class="line" data-ln="1">&#x3C;<span class="pl-c1 fgd-jsx">Input</span> <mark class="pl-e fgd-ak">value</mark><span class="pl-k fgd-pu fgd-ae">=</span><span class="pl-pse">{</span><span class="pl-smi fgd-jv">a</span><span class="pl-pse">}</span> /> &#x3C;<span class="pl-c1 fgd-jsx">Input</span> <mark class="pl-e fgd-ak">value</mark><span class="pl-k fgd-pu fgd-ae">=</span><span class="pl-pse">{</span><span class="pl-smi fgd-jv">b</span><span class="pl-pse">}</span> /></span></span>"`,
);
});

Expand Down Expand Up @@ -530,7 +530,8 @@ const e = 5; // @highlight`,
createEnhanceCodeEmphasis({ strictHighlightText: true }),
);

expect(result).toContain('<mark>');
// The element itself becomes the mark, keeping its classes.
expect(result).toContain('<mark class="pl-e fgd-ak">value</mark>');
expect(result).not.toContain('data-hl-part');
});

Expand Down Expand Up @@ -623,7 +624,7 @@ const another = 99; // @highlight`,
<span class="line" data-ln="4"> <span class="pl-k">const</span> <span class="pl-c1">y</span> <span class="pl-k fgd-pu">=</span> <span class="pl-c1 fgd-num">2</span>;</span>
</span><span class="frame" data-lined=""><span class="line" data-ln="5"> <span class="pl-k">return</span> <span class="pl-smi">x</span> <span class="pl-k fgd-pu">+</span> <span class="pl-smi">y</span>;</span>
<span class="line" data-ln="6">}</span>
</span>"
</span><span class="frame" data-lined="" data-frame-type="highlighted-unfocused"><span class="line" data-ln="7"><span class="pl-k">const</span> <span class="pl-c1">another</span> <span class="pl-k fgd-pu">=</span> <span class="pl-c1 fgd-num">99</span>;</span></span>"
`);
});

Expand Down
100 changes: 100 additions & 0 deletions packages/docs/src/pipeline/loaderUtils/optimization.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { resolveModulePath } from './resolveModulePath';
import type { DirectoryEntry } from './resolveModulePath';
import { parseImportsAndComments } from './parseImportsAndComments';

describe('Filesystem Optimization Tests', () => {
it('should make only one directory read when includeTypeDefs is true', async () => {
Expand Down Expand Up @@ -129,3 +130,102 @@ describe('Filesystem Optimization Tests', () => {
});
});
});

describe('parseImportsAndComments scales linearly', () => {
// Each input once made the parser rescan earlier text for every position or
// comment, so its parse time grew with the square of its size. In one pass,
// parsing an input 16 times as large takes about 16 times as long; a rescan
// takes about 256 times as long. The limit sits between the two, so the guard
// doesn't depend on how fast the machine is, and taking the fastest of several
// runs at each size keeps one-off pauses (JIT compilation, GC) out of it.
const growth = 16;
const maxTimeGrowth = 64;
const options = {
removeCommentsWithPrefix: ['@highlight'],
notableCommentsPrefix: ['@highlight'],
};

/**
* Parses `build(count)` and `build(count * growth)` several times, returning how
* many times as long the larger input took, and its result.
*/
function measureTimeGrowth(build: (count: number) => string, count: number, fileName: string) {
const inputs = [build(count), build(count * growth)];
const fastestMs = [Infinity, Infinity];
// The larger input is parsed last in each round, so this ends as its result
let result: ReturnType<typeof parseImportsAndComments> | undefined;
for (let round = 0; round < 7; round += 1) {
for (let index = 0; index < inputs.length; index += 1) {
const start = performance.now();
result = parseImportsAndComments(inputs[index], fileName, options);
fastestMs[index] = Math.min(fastestMs[index], performance.now() - start);
}
}
return { timeGrowth: fastestMs[1] / fastestMs[0], result: result! };
}

it('reads an MDX import after a very long line', () => {
const { timeGrowth, result } = measureTimeGrowth(
(count) => `${' '.repeat(count)}x\nimport { Button } from './Button';\n`,
5_000,
'/src/demo.mdx',
);

expect(Object.keys(result.relative)).toEqual(['./Button']);
expect(timeGrowth).toBeLessThan(maxTimeGrowth);
});

it('strips and keeps many comments on one long line', () => {
const { timeGrowth, result } = measureTimeGrowth(
(count) => 'const count = 1; /* @highlight */ /* note */ '.repeat(count),
300,
'/src/demo.js',
);

expect(result.code).toBe('const count = 1; /* note */ '.repeat(300 * growth));
expect(timeGrowth).toBeLessThan(maxTimeGrowth);
});

it('maps the positions of many imports in stripped code', () => {
const count = 250;
const { timeGrowth, result } = measureTimeGrowth(
(importCount) =>
Array.from(
{ length: importCount },
(_, index) => `import { Item${index} } from './item${index}'; // @highlight`,
).join('\n'),
count,
'/src/demo.ts',
);

const lastIndex = count * growth - 1;
const lastPath = `'./item${lastIndex}'`;
const lastStart = result.code!.indexOf(lastPath);
expect(result.relative[`./item${lastIndex}`].positions).toEqual([
{ start: lastStart, end: lastStart + lastPath.length },
]);
expect(timeGrowth).toBeLessThan(maxTimeGrowth);
});

it('ends unfinished MDX ESM blocks without searching the rest of the file for each', () => {
const { timeGrowth, result } = measureTimeGrowth(
(count) => "export const loaders = {\n load: () => import('./chart'),\n\n".repeat(count),
250,
'/src/demo.mdx',
);

expect(Object.keys(result.relative)).toEqual(['./chart']);
expect(timeGrowth).toBeLessThan(maxTimeGrowth);
});

it('gives up on an unclosed export list without searching the rest of the file', () => {
const { timeGrowth, result } = measureTimeGrowth(
(count) => 'export { first, second\nconst count = 1;\n'.repeat(count),
250,
'/src/demo.ts',
);

expect(result.relative).toEqual({});
expect(timeGrowth).toBeLessThan(maxTimeGrowth);
});
});
Loading
Loading