diff --git a/data/onPostBuild/__fixtures__/input.mdx b/data/onPostBuild/__fixtures__/input.mdx index eda50c70de..c3b1d3f8a1 100644 --- a/data/onPostBuild/__fixtures__/input.mdx +++ b/data/onPostBuild/__fixtures__/input.mdx @@ -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 */} diff --git a/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap b/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap index 970d2a45a4..e1fa016b40 100644 --- a/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap +++ b/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap @@ -8,8 +8,6 @@ This is a test introduction - - ## Basic heading ## Heading with anchor diff --git a/data/onPostBuild/transpileMdxToMarkdown.test.ts b/data/onPostBuild/transpileMdxToMarkdown.test.ts index 7c6a77d59c..054ee21550 100644 --- a/data/onPostBuild/transpileMdxToMarkdown.test.ts +++ b/data/onPostBuild/transpileMdxToMarkdown.test.ts @@ -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); @@ -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', () => { diff --git a/data/onPostBuild/transpileMdxToMarkdown.ts b/data/onPostBuild/transpileMdxToMarkdown.ts index b456518d41..66dcfc5ebd 100644 --- a/data/onPostBuild/transpileMdxToMarkdown.ts +++ b/data/onPostBuild/transpileMdxToMarkdown.ts @@ -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'); } /**