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
5 changes: 5 additions & 0 deletions data/onPostBuild/__fixtures__/input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ import {
MultiLine,
Import
} from 'module'
import 'side-effect-polyfill'

export const foo = 'bar';
export const ArrowFunc = () => {
const x = 1;
return x;
};
export default SomeComponent;

{/* This is a JSX comment */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ This is a test introduction





## Basic heading

## Heading with anchor
Expand Down
119 changes: 119 additions & 0 deletions data/onPostBuild/transpileMdxToMarkdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ Content without intro`;
expect(output).toContain('Content here');
});

it('should remove side-effect imports without semicolons', () => {
const input = `import 'side-effect-module'

## First Heading

Content here`;
const output = removeImportExportStatements(input);
expect(output).not.toContain('import');
expect(output).not.toContain('side-effect');
expect(output).toContain('## First Heading');
expect(output).toContain('Content here');
});

it('should remove side-effect imports with semicolons', () => {
const input = `import "another-module";

Content here`;
const output = removeImportExportStatements(input);
expect(output).not.toContain('import');
expect(output).not.toContain('another-module');
expect(output).toContain('Content here');
});

it('should remove export default statements', () => {
const input = `export default SomeComponent;\n\nContent here`;
const output = removeImportExportStatements(input);
Expand Down Expand Up @@ -95,6 +118,102 @@ Content without intro`;
expect(output).not.toContain('class Foo');
expect(output).toContain('Content here');
});

it('should preserve import/export statements in code blocks', () => {
const input = `import Component from './component'

## Code Example

\`\`\`javascript
import { realtime } from '@ably/realtime';
export const config = { ... };
\`\`\`
`;
const output = removeImportExportStatements(input);
expect(output).toContain('import { realtime }');
expect(output).toContain('export const config');
expect(output).not.toContain('import Component');
});

it('should handle multi-line imports followed by content', () => {
const input = `import {
Foo,
Bar
} from 'module';

## First Heading

Content here`;
const output = removeImportExportStatements(input);
expect(output).toContain('## First Heading');
expect(output).toContain('Content here');
expect(output).not.toContain('import');
expect(output).not.toContain('Foo');
expect(output).not.toContain('Bar');
});

it('should stop removing at first non-import/export line', () => {
const input = `import Foo from 'bar';

{/* JSX comment */}

import { something } from 'somewhere';`;
const output = removeImportExportStatements(input);
expect(output).toContain('{/* JSX comment */}');
expect(output).toContain("import { something } from 'somewhere'");
expect(output).not.toContain('import Foo');
});

it('should handle blank lines between imports', () => {
const input = `import Foo from 'bar';

import Baz from 'qux';

## Content`;
const output = removeImportExportStatements(input);
expect(output).toContain('## Content');
expect(output).not.toContain('import Foo');
expect(output).not.toContain('import Baz');
});

it('should handle export function on one line', () => {
const input = `export function foo() { return 'bar'; }

## Content`;
const output = removeImportExportStatements(input);
expect(output).toContain('## Content');
expect(output).not.toContain('export');
expect(output).not.toContain('function foo');
});

it('should remove multi-line arrow function exports', () => {
const input = `export const MyComponent = () => {
const x = 1;
return x;
};

## Content`;
const output = removeImportExportStatements(input);
expect(output).toContain('## Content');
expect(output).not.toContain('export');
expect(output).not.toContain('MyComponent');
expect(output).not.toContain('const x = 1');
});

it('should remove object exports with nested braces', () => {
const input = `export const config = {
nested: {
value: 'test';
}
};

## Content`;
const output = removeImportExportStatements(input);
expect(output).toContain('## Content');
expect(output).not.toContain('export');
expect(output).not.toContain('config');
expect(output).not.toContain('nested');
});
});

describe('removeScriptTags', () => {
Expand Down
119 changes: 101 additions & 18 deletions data/onPostBuild/transpileMdxToMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,114 @@ interface FrontMatterAttributes {

/**
* Remove import and export statements from content
* Handles both single-line and multi-line statements
* Uses a line-by-line parser that only removes import/export from the top of the file,
* preserving import/export statements in code blocks later in the file
*/
function removeImportExportStatements(content: string): string {
let result = content;
const lines = content.split('\n');
const result: string[] = [];
let isInTopImportExportSection = true;
let inMultiLineStatement: 'none' | 'import' | 'export' | 'export-function' = 'none';
let braceDepth = 0;

for (const line of lines) {
if (!isInTopImportExportSection) {
// Once we're past the import/export section, keep everything
result.push(line);
continue;
}

const trimmed = line.trim();

// Handle blank lines - skip them while in import/export section
if (trimmed === '') {
continue;
}

// Remove import statements (single and multi-line)
result = result
.replace(/^import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '')
.replace(/^import\s+['"][^'"]+['"];?\s*$/gm, '');
// Check if we're continuing a multi-line statement
if (inMultiLineStatement !== 'none') {
if (inMultiLineStatement === 'export-function' || inMultiLineStatement === 'export') {
// For any export with braces (functions, classes, arrow functions, etc.), track brace depth
if (braceDepth > 0) {
// Count opening and closing braces
const openBraces = (line.match(/\{/g) || []).length;
const closeBraces = (line.match(/\}/g) || []).length;
braceDepth += openBraces - closeBraces;

// If we've closed all braces, we're done with this statement
if (braceDepth === 0) {
inMultiLineStatement = 'none';
}
} else {
// No braces being tracked, look for semicolon or closing brace to end
if (line.includes(';') || (line.includes('}') && !line.includes('{'))) {
inMultiLineStatement = 'none';
}
}
} else {
// For regular import statements, look for semicolon or closing brace
if (line.includes(';') || (line.includes('}') && !line.includes('{'))) {
inMultiLineStatement = 'none';
}
}
continue;
}

// Remove export statements
// Handle: export { foo, bar }; (single and multi-line)
result = result
.replace(/^export\s+\{[\s\S]*?\}\s*;?\s*$/gm, '')
.replace(/^export\s+\{[\s\S]*?\}\s+from\s+['"][^'"]+['"];?\s*$/gm, '');
// Check if line starts an import statement
if (trimmed.startsWith('import ')) {
// Detect if it's a complete single-line import or incomplete multi-line
const hasFrom = trimmed.includes(' from ');
const endsWithQuote = trimmed.match(/['"][;]?\s*$/);
const hasSemicolon = trimmed.includes(';');
const isSideEffectImport = trimmed.match(/^import\s+['"]/);

// Complete cases:
// 1. Has semicolon
// 2. Has 'from' and ends with quote (with or without semicolon)
// 3. Is a side-effect import: import 'foo' or import "foo" (with or without semicolon)
if (!hasSemicolon && hasFrom && !endsWithQuote) {
// Incomplete: multi-line import like "import {" without closing
inMultiLineStatement = 'import';
} else if (!hasSemicolon && !hasFrom && !isSideEffectImport) {
// Incomplete: just "import" or "import {" at start of multi-line
// (but not side-effect imports which are complete)
inMultiLineStatement = 'import';
}
// Otherwise it's complete
continue;
}

// Handle: export default Component; or export const foo = 'bar';
result = result.replace(/^export\s+(default|const|let|var)\s+.*$/gm, '');
// Check if line starts an export statement
if (trimmed.startsWith('export ')) {
// Detect export function/class (multi-line with braces)
if (trimmed.match(/^export\s+(function|class)\s+/)) {
inMultiLineStatement = 'export-function';
// Count braces on this line
const openBraces = (line.match(/\{/g) || []).length;
const closeBraces = (line.match(/\}/g) || []).length;
braceDepth = openBraces - closeBraces;
// Check if it's all on one line (rare but possible)
if (braceDepth === 0 && line.includes('}')) {
inMultiLineStatement = 'none';
}
} else if (!line.includes(';') && line.includes('{') && !line.includes('}')) {
// Multi-line export with braces (arrow functions, objects, etc.)
inMultiLineStatement = 'export';
// Initialize brace depth tracking
const openBraces = (line.match(/\{/g) || []).length;
const closeBraces = (line.match(/\}/g) || []).length;
braceDepth = openBraces - closeBraces;
}
// Otherwise it's complete (has semicolon, or no braces)
continue;
}

// Handle: export function/class declarations (multi-line)
// Match from 'export function/class' until the closing brace
result = result.replace(/^export\s+(function|class)\s+\w+[\s\S]*?\n\}/gm, '');
// First non-import/export line - we're done with the section
isInTopImportExportSection = false;
result.push(line);
}

// Clean up extra blank lines left behind
return result.replace(/\n\n\n+/g, '\n\n');
return result.join('\n');
}

/**
Expand Down
Loading