Skip to content
Open
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
15 changes: 15 additions & 0 deletions packages/mdxe/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
extends: 'next/core-web-vitals',
ignorePatterns: [
'.next/**/*',
'node_modules/**/*',
'dist/**/*',
'src/.next/**/*'
],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@next/next/no-html-link-for-pages': 'off'
}
}
246 changes: 246 additions & 0 deletions packages/mdxe/bin/mdx-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
#!/usr/bin/env node

import { Command } from 'commander'
import { spawn } from 'child_process'
import { join, dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const packageJson = JSON.parse(fs.readFileSync(join(__dirname, '../package.json'), 'utf8'))
const version = packageJson.version

const program = new Command()
program
.name('mdx')
.description('Simple MDX file server with Next.js')
.version(version)

let activeProcess = null
process.on('SIGINT', () => {
if (activeProcess) {
activeProcess.kill('SIGINT')
}
process.exit(0)
})

function ensureCustom404Page(userCwd) {
const nextConfigPath = join(userCwd, 'next.config.js')

if (!fs.existsSync(nextConfigPath)) {
const nextConfigContent = `
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
disableOptimizedLoading: true,
optimizeCss: false
},
typescript: {
ignoreBuildErrors: true
},
eslint: {
ignoreDuringBuilds: true
}
}

module.exports = nextConfig
`
fs.writeFileSync(nextConfigPath, nextConfigContent.trim())
console.log(`Created Next.js config at ${nextConfigPath}`)
} else {
let nextConfig = fs.readFileSync(nextConfigPath, 'utf-8')

if (!nextConfig.includes('disableOptimizedLoading')) {
nextConfig = nextConfig.replace(
'const nextConfig = {',
`const nextConfig = {
experimental: {
disableOptimizedLoading: true,
optimizeCss: false
},`
)

fs.writeFileSync(nextConfigPath, nextConfig)
console.log(`Updated Next.js config at ${nextConfigPath}`)
}
}

const pagesDir = join(userCwd, 'pages')
if (fs.existsSync(pagesDir)) {
const custom404Path = join(pagesDir, '404.js')
if (fs.existsSync(custom404Path)) {
fs.unlinkSync(custom404Path)
console.log(`Removed custom 404 page at ${custom404Path}`)
}
}
}

function runNextCommand(command, options = {}) {
const userCwd = process.cwd()
const mdxRoot = join(__dirname, '..')
const embeddedAppPath = join(mdxRoot, 'src')

if (command === 'build') {
const nextConfigPath = join(userCwd, 'next.config.js')
const nextConfigContent = `
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: {
unoptimized: true,
},
typescript: {
ignoreBuildErrors: true
},
eslint: {
ignoreDuringBuilds: true
}
}

module.exports = nextConfig
`
fs.writeFileSync(nextConfigPath, nextConfigContent.trim())
console.log(`Created Next.js config at ${nextConfigPath}`)

const pagesDir = join(userCwd, 'pages')
if (!fs.existsSync(pagesDir)) {
fs.mkdirSync(pagesDir, { recursive: true })
}

const custom404Path = join(pagesDir, '404.js')
if (!fs.existsSync(custom404Path)) {
const simple404Content = `
export default function Custom404() {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<a href="/">Return to Home</a>
</div>
)
}
`
fs.writeFileSync(custom404Path, simple404Content.trim())
console.log(`Created custom 404 page at ${custom404Path}`)
}

const documentPath = join(pagesDir, '_document.js')
if (!fs.existsSync(documentPath)) {
const documentContent = `
import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}

export default MyDocument
`
fs.writeFileSync(documentPath, documentContent.trim())
console.log(`Created custom _document page at ${documentPath}`)
}
}

const localNextBin = join(userCwd, 'node_modules', '.bin', 'next')
const mdxeNextBin = join(mdxRoot, 'node_modules', '.bin', 'next')

let nextCommand
if (fs.existsSync(localNextBin)) {
nextCommand = localNextBin
} else if (fs.existsSync(mdxeNextBin)) {
nextCommand = mdxeNextBin
} else {
nextCommand = 'npx next'
}

const args = [command]

if (options.port) {
args.push('-p', options.port)
}

if (options.hostname) {
args.push('-H', options.hostname)
}

console.log(`Running Next.js command: ${nextCommand} ${args.join(' ')}`)
console.log(`User current directory: ${userCwd}`)
console.log(`App directory: ${embeddedAppPath}`)

const readmePath = join(userCwd, 'README.md')
const hasReadme = fs.existsSync(readmePath)

const env = {
...process.env,
NEXT_PUBLIC_USER_CWD: userCwd,
USER_CWD: userCwd,
APP_ROOT_PATH: embeddedAppPath,
README_PATH: hasReadme ? readmePath : '',
NODE_ENV: command === 'build' ? 'production' : 'development',
NEXT_SKIP_404: 'true'
}

activeProcess = spawn(nextCommand, args, {
stdio: 'inherit',
shell: true,
cwd: embeddedAppPath,
env
})

return new Promise((resolve, reject) => {
activeProcess.on('close', (code) => {
if (code === 0) {
resolve()
} else {
reject(new Error(`Next.js process exited with code ${code}`))
}
})
})
}

program
.command('dev')
.description('Start development server')
.option('-p, --port <port>', 'Port to run on', '3000')
.option('-H, --hostname <hostname>', 'Hostname to run on', 'localhost')
.action(async (options) => {
await runNextCommand('dev', options)
})

program
.command('build')
.description('Build for production')
.action(async () => {
await runNextCommand('build')
})

program
.command('start')
.description('Start production server')
.option('-p, --port <port>', 'Port to run on', '3000')
.option('-H, --hostname <hostname>', 'Hostname to run on', 'localhost')
.action(async (options) => {
await runNextCommand('start', options)
})

if (!process.argv.slice(2).some(arg => ['dev', 'build', 'start'].includes(arg))) {
program
.argument('[path]', 'Path to a markdown file or directory', '.')
.action(async (path) => {
console.log(`Serving markdown content from: ${path || '.'}`)
await runNextCommand('dev')
})
}

program.parse(process.argv)
6 changes: 3 additions & 3 deletions packages/mdxe/bin/mdxe.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { join, resolve } from 'path'
import { spawn } from 'child_process'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
import { isDirectory, isMarkdownFile, findIndexFile, resolvePath, getAllMarkdownFiles, filePathToRoutePath } from '../src/utils/file-resolution.js'
import { isDirectory, isMarkdownFile, findIndexFile, resolveMdxPath, getAllMarkdownFiles, filePathToRoutePath } from '../src/utils/file-utils.js'
import { createTempNextConfig } from '../src/utils/temp-config.js'

const __filename = fileURLToPath(import.meta.url)
Expand Down Expand Up @@ -140,13 +140,13 @@ program

if (!process.argv.slice(2).some(arg => ['dev', 'build', 'start', 'lint'].includes(arg))) {
program.argument('[path]', 'Path to a markdown file or directory', '.').action(async (path) => {
const resolvedPath = resolvePath(path)
const resolvedPath = await resolveMdxPath(path)

if (!resolvedPath && path !== '.') {
console.error(`Error: Could not resolve path ${path} to a markdown file or directory with index file`)
console.error('Make sure the path exists and is either:')
console.error(' - A .md or .mdx file')
console.error(' - A directory containing index.md, index.mdx, page.md, page.mdx, or README.md')
console.error(' - A directory containing index.md, index.mdx, or README.md')
process.exit(1)
}

Expand Down
15 changes: 15 additions & 0 deletions packages/mdxe/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
// output: 'export',
images: {
unoptimized: true,
},
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
serverExternalPackages: ['next-mdx-remote'],
experimental: {
disableOptimizedLoading: true,
optimizeCss: false
}
};

export default nextConfig;
17 changes: 10 additions & 7 deletions packages/mdxe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,30 @@
"types": "dist/index.d.ts",
"bin": {
"mdxe": "./bin/mdxe.js"
}, "scripts": {
},
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"commander": "^12.0.0",
"next": "15.3.2",
"next-mdx-remote": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.2"
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4",
"typescript": "^5"
},
"keywords": [
"mdx",
Expand Down
Loading
Loading