mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-24 20:09:02 +00:00
Local MD: implement dev server
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
"packages/mdx-remote/test/fixtures/**/*.js",
|
||||
"packages/mdx-remote/test/fixtures/**/*.json",
|
||||
"packages/local-md/test/fixtures/**/*.json",
|
||||
"packages/core/test/fixtures/page-trees/**/*.json",
|
||||
|
||||
"routeTree.gen.ts",
|
||||
|
||||
@@ -15,3 +15,9 @@ console.log('Hello World');
|
||||
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
|
||||
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
|
||||
</Cards>
|
||||
|
||||
safdafsd
|
||||
fsdafsdaafsdsf
|
||||
sffsadafsd
|
||||
fsad
|
||||
fsad
|
||||
|
||||
@@ -7,6 +7,10 @@ export const docs = localMd({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
void docs.devServer();
|
||||
}
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
baseUrl: docsRoute,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dev": "local-md dev -- pnpm next dev",
|
||||
"start": "next start",
|
||||
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit"
|
||||
},
|
||||
|
||||
@@ -10,13 +10,17 @@
|
||||
"license": "MIT",
|
||||
"author": "Fuma Nama",
|
||||
"repository": "github:fuma-nama/fumadocs",
|
||||
"bin": {
|
||||
"local-md": "./dist/bin.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./dev": "./dist/dev/index.js",
|
||||
"./dev/client": "./dist/dev/client.js",
|
||||
"./dev/server": "./dist/dev/server.js",
|
||||
"./js/executor": "./dist/js/executor.js",
|
||||
"./js/executor-native": "./dist/js/executor-native.js",
|
||||
"./js/executor-virtual": "./dist/js/executor-virtual.js",
|
||||
@@ -36,15 +40,19 @@
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"commander": "^14.0.3",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"estree-util-to-js": "^2.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||
"remark": "^15.0.1",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"tinyglobby": "^0.2.16",
|
||||
"unified": "^11.0.5",
|
||||
"vfile": "^6.0.3"
|
||||
"vfile": "^6.0.3",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/estree-jsx": "^1.0.5",
|
||||
"@types/hast": "^3.0.4",
|
||||
@@ -52,6 +60,7 @@
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"ignore": "^7.0.5",
|
||||
"react": "^19.2.5",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from 'commander';
|
||||
import spawn from 'cross-spawn';
|
||||
import packageJson from '../package.json';
|
||||
import { startDevServer } from './dev/server';
|
||||
import { LOCAL_MD_DEV_PORT_ENV } from './dev/shared';
|
||||
|
||||
const program = new Command()
|
||||
.name('local-md')
|
||||
.description('CLI for local Markdown development utilities')
|
||||
.version(packageJson.version)
|
||||
.enablePositionalOptions();
|
||||
|
||||
program
|
||||
.command('dev')
|
||||
.description('start the local-md dev server and run a command')
|
||||
.argument('<command...>', 'command to run')
|
||||
.option('-p, --port <number>', 'port for the dev server', parsePort, 8000)
|
||||
.allowUnknownOption()
|
||||
.passThroughOptions()
|
||||
.action(async (command: string[], options: { port: number }) => {
|
||||
const handle = await startDevServer({
|
||||
port: options.port,
|
||||
});
|
||||
|
||||
const child = spawn(command[0], command.slice(1), {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
[LOCAL_MD_DEV_PORT_ENV]: String(handle.port),
|
||||
},
|
||||
});
|
||||
|
||||
const forwardSignal = (signal: NodeJS.Signals) => {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill(signal);
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', forwardSignal);
|
||||
process.on('SIGTERM', forwardSignal);
|
||||
|
||||
let exitCode = 1;
|
||||
try {
|
||||
exitCode = await new Promise<number>((resolve, reject) => {
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
resolve(signalToExitCode(signal));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(code ?? 0);
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
process.off('SIGINT', forwardSignal);
|
||||
process.off('SIGTERM', forwardSignal);
|
||||
await handle.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
process.exit(exitCode);
|
||||
});
|
||||
|
||||
program.parseAsync(process.argv).catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function parsePort(value: string): number {
|
||||
const port = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(port) || port < 0) {
|
||||
throw new Error(`Invalid port: ${value}`);
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
function signalToExitCode(signal: NodeJS.Signals): number {
|
||||
switch (signal) {
|
||||
case 'SIGINT':
|
||||
return 130;
|
||||
case 'SIGTERM':
|
||||
return 143;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
decodeDevEvent,
|
||||
encodeDevClientEvent,
|
||||
getDevServerPort,
|
||||
getDevServerUrl,
|
||||
type DevServerEvent,
|
||||
} from './shared';
|
||||
import { WebSocket, type RawData } from 'ws';
|
||||
|
||||
export interface DevServerConnection {
|
||||
readonly port: number;
|
||||
readonly url: string;
|
||||
readonly connected: boolean;
|
||||
readonly lastEvent?: DevServerEvent;
|
||||
subscribe: (listener: (event: DevServerEvent) => void) => () => void;
|
||||
watchDir: (dir: string) => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const STORE_KEY = Symbol.for('__fumadocs_local_md_dev_connections__');
|
||||
|
||||
export function connectDevServer(port = getDevServerPort()): DevServerConnection {
|
||||
if (!port) {
|
||||
throw new Error('Missing local-md dev server port');
|
||||
}
|
||||
|
||||
const url = getDevServerUrl(port);
|
||||
const store = getStore();
|
||||
let connection = store.get(url);
|
||||
|
||||
if (!connection) {
|
||||
connection = new SharedDevServerConnection(port, url);
|
||||
store.set(url, connection);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
class SharedDevServerConnection implements DevServerConnection {
|
||||
readonly listeners = new Set<(event: DevServerEvent) => void>();
|
||||
readonly watchedDirs = new Set<string>();
|
||||
readonly port: number;
|
||||
readonly url: string;
|
||||
connected = false;
|
||||
lastEvent?: DevServerEvent;
|
||||
|
||||
private socket?: WebSocket;
|
||||
private retryTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(port: number, url: string) {
|
||||
this.port = port;
|
||||
this.url = url;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
subscribe(listener: (event: DevServerEvent) => void) {
|
||||
this.listeners.add(listener);
|
||||
|
||||
if (this.lastEvent) {
|
||||
listener(this.lastEvent);
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
this.connected = false;
|
||||
clearTimeout(this.retryTimer);
|
||||
this.socket?.close();
|
||||
this.listeners.clear();
|
||||
getStore().delete(this.url);
|
||||
}
|
||||
|
||||
watchDir(dir: string) {
|
||||
this.watchedDirs.add(dir);
|
||||
this.send({
|
||||
type: 'watch',
|
||||
absolutePath: dir,
|
||||
});
|
||||
}
|
||||
|
||||
private connect() {
|
||||
if (this.socket) return;
|
||||
const socket = new WebSocket(this.url);
|
||||
this.socket = socket;
|
||||
|
||||
socket.on('open', () => {
|
||||
this.connected = true;
|
||||
console.log(`[@fumadocs/local-md] connected to dev server at ${this.url}`);
|
||||
for (const dir of this.watchedDirs) {
|
||||
this.send({
|
||||
type: 'watch',
|
||||
absolutePath: dir,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('message', (data) => {
|
||||
const decoded = decodeDevEvent(rawDataToString(data));
|
||||
if (decoded) this.emit(decoded);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
this.connected = false;
|
||||
this.socket = undefined;
|
||||
console.log(`[@fumadocs/local-md] disconnected from dev server at ${this.url}`);
|
||||
});
|
||||
|
||||
socket.on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
private emit(event: DevServerEvent) {
|
||||
this.lastEvent = event;
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
private send(message: { type: 'watch'; absolutePath: string }) {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
this.socket.send(encodeDevClientEvent(message));
|
||||
}
|
||||
}
|
||||
|
||||
function rawDataToString(data: RawData): string {
|
||||
if (typeof data === 'string') return data;
|
||||
if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8');
|
||||
if (Array.isArray(data)) return Buffer.concat(data).toString('utf8');
|
||||
|
||||
return data.toString('utf8');
|
||||
}
|
||||
|
||||
function getStore() {
|
||||
const globalStore = globalThis as typeof globalThis & {
|
||||
[STORE_KEY]?: Map<string, SharedDevServerConnection>;
|
||||
};
|
||||
|
||||
globalStore[STORE_KEY] ??= new Map<string, SharedDevServerConnection>();
|
||||
return globalStore[STORE_KEY];
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './watcher';
|
||||
@@ -0,0 +1,224 @@
|
||||
import { type ChokidarOptions, type FSWatcher, watch } from 'chokidar';
|
||||
import ignore, { type Ignore } from 'ignore';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
decodeDevClientEvent,
|
||||
encodeDevEvent,
|
||||
getDevServerUrl,
|
||||
LOCAL_MD_DEV_PATH,
|
||||
type DevServerEvent,
|
||||
type DevWatchEvent,
|
||||
} from './shared';
|
||||
import { WebSocket, WebSocketServer, type RawData } from 'ws';
|
||||
|
||||
const WATCH_EVENTS = new Set<DevWatchEvent>(['add', 'addDir', 'change', 'unlink', 'unlinkDir']);
|
||||
type IgnoredFn = (value: string) => boolean;
|
||||
|
||||
export interface DevServerOptions {
|
||||
port: number;
|
||||
host?: string;
|
||||
/**
|
||||
* customise chokidar, by default, file watcher will watch all files under the `dir` directory.
|
||||
*/
|
||||
watchOptions?: (options: ChokidarOptions) => ChokidarOptions;
|
||||
}
|
||||
|
||||
export interface DevServerHandle {
|
||||
port: number;
|
||||
host: string;
|
||||
watcher: FSWatcher;
|
||||
watchDir: (dir: string) => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export async function startDevServer(options: DevServerOptions): Promise<DevServerHandle> {
|
||||
const host = options.host ?? '127.0.0.1';
|
||||
const port = options.port;
|
||||
const rootIgnored = await fromGitIgnore(process.cwd(), 'node_modules\ndist\nbuild');
|
||||
|
||||
let watchOptions: ChokidarOptions = {
|
||||
ignoreInitial: true,
|
||||
followSymlinks: false,
|
||||
ignored: (value) => {
|
||||
if (rootIgnored?.(value)) return true;
|
||||
return false;
|
||||
},
|
||||
};
|
||||
if (options.watchOptions) {
|
||||
watchOptions = options.watchOptions(watchOptions);
|
||||
}
|
||||
|
||||
const watcher = watch([], watchOptions);
|
||||
const clients = new Map<WebSocket, Set<string>>();
|
||||
const watchedDirs = new Map<string, number>();
|
||||
const wsServer = new WebSocketServer({
|
||||
path: LOCAL_MD_DEV_PATH,
|
||||
port,
|
||||
host,
|
||||
});
|
||||
|
||||
function broadcast(event: DevServerEvent) {
|
||||
const encoded = encodeDevEvent(event);
|
||||
|
||||
for (const client of clients.keys()) {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
clients.delete(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
client.send(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
wsServer.on('connection', (client) => {
|
||||
clients.set(client, new Set());
|
||||
client.send(
|
||||
encodeDevEvent({
|
||||
type: 'connected',
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
client.on('message', async (data) => {
|
||||
const decoded = decodeDevClientEvent(rawDataToString(data));
|
||||
if (!decoded) return;
|
||||
|
||||
if (decoded.type === 'watch') {
|
||||
try {
|
||||
await addClientDir(client, decoded.absolutePath);
|
||||
} catch (error) {
|
||||
client.send(
|
||||
encodeDevEvent({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
void removeClientDirs(client);
|
||||
clients.delete(client);
|
||||
});
|
||||
});
|
||||
|
||||
watcher.on('all', (event, filePath) => {
|
||||
if (!WATCH_EVENTS.has(event as DevWatchEvent)) return;
|
||||
|
||||
broadcast({
|
||||
type: 'change',
|
||||
event: event as DevWatchEvent,
|
||||
absolutePath: filePath,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
watcher.on('error', (error) => {
|
||||
broadcast({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
wsServer.once('error', reject);
|
||||
wsServer.on('listening', () => {
|
||||
wsServer.off('error', reject);
|
||||
console.log(`[@fumadocs/local-md] dev server is ready at ${getDevServerUrl(port, host)}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
port,
|
||||
host,
|
||||
watcher,
|
||||
watchDir: addWatchDir,
|
||||
async close() {
|
||||
for (const client of clients.keys()) {
|
||||
client.close();
|
||||
}
|
||||
clients.clear();
|
||||
wsServer.close();
|
||||
|
||||
await watcher.close();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
wsServer.close((error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
async function addClientDir(client: WebSocket, dir: string) {
|
||||
const absolutePath = path.resolve(dir);
|
||||
const dirs = clients.get(client);
|
||||
if (!dirs || dirs.has(absolutePath)) return;
|
||||
|
||||
dirs.add(absolutePath);
|
||||
await addWatchDir(absolutePath);
|
||||
}
|
||||
|
||||
async function addWatchDir(dir: string) {
|
||||
const absolutePath = path.resolve(dir);
|
||||
const count = watchedDirs.get(absolutePath) ?? 0;
|
||||
if (count === 0) watcher.add(absolutePath);
|
||||
|
||||
watchedDirs.set(absolutePath, count + 1);
|
||||
}
|
||||
|
||||
async function removeClientDirs(client: WebSocket) {
|
||||
const dirs = clients.get(client);
|
||||
if (!dirs) return;
|
||||
|
||||
for (const dir of dirs) removeWatchDir(dir);
|
||||
dirs.clear();
|
||||
}
|
||||
|
||||
function removeWatchDir(dir: string) {
|
||||
const absolutePath = path.resolve(dir);
|
||||
const count = watchedDirs.get(absolutePath);
|
||||
if (!count) return;
|
||||
|
||||
if (count === 1) {
|
||||
watchedDirs.delete(absolutePath);
|
||||
watcher.unwatch(absolutePath);
|
||||
return;
|
||||
}
|
||||
|
||||
watchedDirs.set(absolutePath, count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function fromGitIgnore(dir: string, defaultValue?: string): Promise<IgnoredFn | undefined> {
|
||||
const gitignore = await fs
|
||||
.readFile(path.join(dir, '.gitignore'), 'utf-8')
|
||||
.catch(() => defaultValue);
|
||||
|
||||
if (gitignore) {
|
||||
const ig = ignore();
|
||||
ig.add(gitignore);
|
||||
return toMatcher(dir, ig);
|
||||
}
|
||||
}
|
||||
|
||||
function toMatcher(dir: string, ig: Ignore): IgnoredFn {
|
||||
return (v) => {
|
||||
const relativePath = path.relative(dir, v);
|
||||
// for invalid path, don't ignore
|
||||
return ignore.isPathValid(relativePath) && ig.checkIgnore(relativePath).ignored;
|
||||
};
|
||||
}
|
||||
|
||||
function rawDataToString(data: RawData): string {
|
||||
if (typeof data === 'string') return data;
|
||||
if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8');
|
||||
if (Array.isArray(data)) return Buffer.concat(data).toString('utf8');
|
||||
|
||||
return data.toString('utf8');
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export const LOCAL_MD_DEV_PATH = '/_fumadocs_local_md';
|
||||
export const LOCAL_MD_DEV_PORT_ENV = 'FUMADOCS_LOCAL_MD_DEV_SERVER_PORT';
|
||||
|
||||
export type DevWatchEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
|
||||
|
||||
export interface DevClientEvent {
|
||||
type: 'watch';
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export type DevServerEvent =
|
||||
| {
|
||||
type: 'connected';
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
type: 'change';
|
||||
event: DevWatchEvent;
|
||||
absolutePath: string;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
type: 'error';
|
||||
message: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export function encodeDevClientEvent(event: DevClientEvent): string {
|
||||
return JSON.stringify(event);
|
||||
}
|
||||
|
||||
export function decodeDevClientEvent(value: string): DevClientEvent | undefined {
|
||||
try {
|
||||
return JSON.parse(value) as DevClientEvent;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeDevEvent(event: DevServerEvent): string {
|
||||
return JSON.stringify(event);
|
||||
}
|
||||
|
||||
export function decodeDevEvent(value: string): DevServerEvent | undefined {
|
||||
try {
|
||||
return JSON.parse(value) as DevServerEvent;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDevServerPort(port = process.env[LOCAL_MD_DEV_PORT_ENV]): number | undefined {
|
||||
if (!port) return undefined;
|
||||
|
||||
const value = Number.parseInt(port, 10);
|
||||
if (!Number.isFinite(value) || value <= 0) return undefined;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getDevServerUrl(port: number, host = '127.0.0.1'): string {
|
||||
return `ws://${host}:${port}${LOCAL_MD_DEV_PATH}`;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { type ChokidarOptions, type Matcher, watch } from 'chokidar';
|
||||
import ignore, { type Ignore } from 'ignore';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { LocalMarkdown } from '..';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
|
||||
export async function startWatcher(
|
||||
instance: LocalMarkdown<StandardSchemaV1, StandardSchemaV1>,
|
||||
/**
|
||||
* customise chokidar, by default, file watcher will watch all files under the `dir` directory.
|
||||
*/
|
||||
watchOptions?: (options: ChokidarOptions) => ChokidarOptions,
|
||||
) {
|
||||
const dir = instance.config.dir;
|
||||
// init .gitignore
|
||||
const ignored = await Promise.all([
|
||||
fromGitIgnore(process.cwd(), 'node_modules\ndist\nbuild'),
|
||||
fromGitIgnore(dir),
|
||||
]);
|
||||
|
||||
let options: ChokidarOptions = {
|
||||
ignoreInitial: true,
|
||||
followSymlinks: false,
|
||||
ignored: ignored.filter((v) => v !== undefined),
|
||||
};
|
||||
|
||||
if (watchOptions) options = watchOptions(options);
|
||||
|
||||
return watch(dir, options);
|
||||
}
|
||||
|
||||
async function fromGitIgnore(dir: string, defaultValue?: string) {
|
||||
const gitignore = await fs
|
||||
.readFile(path.join(dir, '.gitignore'), 'utf-8')
|
||||
.catch(() => defaultValue);
|
||||
|
||||
if (gitignore) {
|
||||
const ig = ignore();
|
||||
ig.add(gitignore);
|
||||
return toMatcher(dir, ig);
|
||||
}
|
||||
}
|
||||
|
||||
function toMatcher(dir: string, ig: Ignore): Matcher {
|
||||
return (v) => {
|
||||
const relativePath = path.relative(dir, v);
|
||||
// for invalid path, don't ignore
|
||||
return ignore.isPathValid(relativePath) && ig.checkIgnore(relativePath).ignored;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { MetaData, Source, VirtualFile } from 'fumadocs-core/source';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import path from 'node:path';
|
||||
import { createStorage } from './storage';
|
||||
import type * as defaultSchemas from 'fumadocs-core/source/schema';
|
||||
import { createMarkdownRenderer, MarkdownRendererOptions, PageRenderer } from './md/renderer';
|
||||
import { createMarkdownCompiler, MarkdownCompilerOptions } from './md/compiler';
|
||||
import { getDevServerPort } from './dev/shared';
|
||||
|
||||
export interface LocalMarkdownConfig<
|
||||
FrontmatterSchema extends StandardSchemaV1,
|
||||
@@ -29,6 +31,8 @@ export interface LocalMarkdown<
|
||||
MetaSchema extends StandardSchemaV1,
|
||||
> {
|
||||
config: LocalMarkdownConfig<FrontmatterSchema, MetaSchema>;
|
||||
/** connect to dev server, required for hot reload */
|
||||
devServer: (port?: number) => Promise<void>;
|
||||
toSource: () => Promise<
|
||||
Source<{
|
||||
pageData: LocalMarkdownPage<StandardSchemaV1.InferOutput<FrontmatterSchema>>;
|
||||
@@ -58,6 +62,20 @@ export function localMd<
|
||||
|
||||
return {
|
||||
config,
|
||||
async devServer(port) {
|
||||
const { connectDevServer } = await import('@/dev/client');
|
||||
const resolvedPort = port ?? getDevServerPort();
|
||||
if (!resolvedPort) return;
|
||||
|
||||
const conn = connectDevServer(resolvedPort);
|
||||
conn.watchDir(path.resolve(config.dir));
|
||||
|
||||
conn.subscribe((event) => {
|
||||
if (event.type === 'change') {
|
||||
storage.invalidateCache(event.absolutePath);
|
||||
}
|
||||
});
|
||||
},
|
||||
async toSource() {
|
||||
const { metas, pages } = await storage.getPages();
|
||||
const files: VirtualFile<{
|
||||
|
||||
@@ -110,6 +110,9 @@ export function createStorage<
|
||||
}
|
||||
|
||||
return {
|
||||
invalidateCache(absolutePath: string) {
|
||||
filesCache.delete(absolutePath);
|
||||
},
|
||||
async getPages() {
|
||||
const files = await glob(include, {
|
||||
cwd: dir,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# JSX expressions
|
||||
|
||||
export const ignored = <span>Ignored export</span>
|
||||
export const ignored = <span>Ignored export</span>;
|
||||
|
||||
{<div id="native-jsx">Native JSX child</div>}
|
||||
|
||||
|
||||
@@ -4,9 +4,14 @@ export default defineConfig({
|
||||
dts: true,
|
||||
fixedExtension: false,
|
||||
target: 'es2023',
|
||||
entry: ['./src/index.ts', './src/js/*', './src/dev/index.ts'],
|
||||
entry: ['./src/index.ts', './src/bin.ts', './src/js/*', './src/dev/{server,client}.ts'],
|
||||
format: 'esm',
|
||||
exports: true,
|
||||
exports: {
|
||||
bin: {
|
||||
'local-md': './src/bin.ts',
|
||||
},
|
||||
exclude: ['bin'],
|
||||
},
|
||||
deps: {
|
||||
onlyBundle: ['ignore', 'stable-hash'],
|
||||
neverBundle: ['mdx/types'],
|
||||
|
||||
Generated
+66
-29
@@ -1798,7 +1798,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
@@ -1832,7 +1832,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/content:
|
||||
dependencies:
|
||||
@@ -1857,7 +1857,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/content-collections:
|
||||
dependencies:
|
||||
@@ -1885,7 +1885,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
@@ -2033,7 +2033,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -2085,7 +2085,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -2176,7 +2176,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -2222,7 +2222,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -2241,6 +2241,12 @@ importers:
|
||||
chokidar:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
commander:
|
||||
specifier: ^14.0.3
|
||||
version: 14.0.3
|
||||
cross-spawn:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6
|
||||
estree-util-to-js:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -2262,7 +2268,13 @@ importers:
|
||||
vfile:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
devDependencies:
|
||||
'@types/cross-spawn':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6
|
||||
'@types/estree':
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
@@ -2284,6 +2296,9 @@ importers:
|
||||
'@types/unist':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
fumadocs-core:
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
@@ -2304,7 +2319,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/mdx:
|
||||
dependencies:
|
||||
@@ -2410,7 +2425,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
vite:
|
||||
specifier: ^8.0.8
|
||||
version: 8.0.8(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
@@ -2459,7 +2474,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/obsidian:
|
||||
dependencies:
|
||||
@@ -2535,7 +2550,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -2647,7 +2662,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/press:
|
||||
dependencies:
|
||||
@@ -2786,7 +2801,7 @@ importers:
|
||||
version: 4.2.2
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -2838,7 +2853,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/radix-ui:
|
||||
dependencies:
|
||||
@@ -2959,7 +2974,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
@@ -3003,7 +3018,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/story:
|
||||
dependencies:
|
||||
@@ -3061,7 +3076,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/tailwind:
|
||||
devDependencies:
|
||||
@@ -3085,7 +3100,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
|
||||
packages/tsconfig: {}
|
||||
|
||||
@@ -3145,7 +3160,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -3221,7 +3236,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
tsdown:
|
||||
specifier: 0.21.8
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
version: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
@@ -7333,6 +7348,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@typescript/[email protected]':
|
||||
resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==}
|
||||
peerDependencies:
|
||||
@@ -7832,6 +7850,7 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KLwDUqs5WIny/94F4xZ4QfaAE6YWyjR+s79pt/ItQhk2CG+PJQ5xL6VuOWhiyN2eP3fryZK95vog9CTLCaYV2Q==}
|
||||
cpu: [arm64, x64]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@@ -12419,6 +12438,18 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -16196,7 +16227,7 @@ snapshots:
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5([email protected])
|
||||
optionalDependencies:
|
||||
'@vitejs/plugin-rsc': 0.5.24([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected])([email protected]([email protected])))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
'@vitejs/plugin-rsc': 0.5.24([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
transitivePeerDependencies:
|
||||
- '@rsbuild/core'
|
||||
- crossws
|
||||
@@ -16232,7 +16263,7 @@ snapshots:
|
||||
react-dom: 19.2.5([email protected])
|
||||
vite: 8.0.8(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
optionalDependencies:
|
||||
'@vitejs/plugin-rsc': 0.5.24([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected])([email protected]([email protected])))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
'@vitejs/plugin-rsc': 0.5.24([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
transitivePeerDependencies:
|
||||
- '@rsbuild/core'
|
||||
- crossws
|
||||
@@ -16403,7 +16434,7 @@ snapshots:
|
||||
lightningcss: 1.32.0
|
||||
postcss-load-config: 6.0.1([email protected])([email protected])([email protected])([email protected])
|
||||
rolldown: 1.0.0-rc.15
|
||||
tsdown: 0.21.8(@tsdown/[email protected])([email protected])([email protected])
|
||||
tsdown: 0.21.8(@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])
|
||||
optionalDependencies:
|
||||
postcss: 8.5.9
|
||||
transitivePeerDependencies:
|
||||
@@ -16703,6 +16734,10 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@typescript/[email protected]([email protected])':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -18069,7 +18104,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
[email protected]([email protected](@emnapi/[email protected])(@emnapi/[email protected])):
|
||||
optionalDependencies:
|
||||
oxc-resolver: 11.19.1(@emnapi/[email protected])(@emnapi/[email protected])
|
||||
|
||||
@@ -21447,7 +21482,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected])([email protected])([email protected]):
|
||||
[email protected]([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@babel/generator': 8.0.0-rc.3
|
||||
'@babel/helper-validator-identifier': 8.0.0-rc.3
|
||||
@@ -21455,7 +21490,7 @@ snapshots:
|
||||
'@babel/types': 8.0.0-rc.3
|
||||
ast-kit: 3.0.0-beta.1
|
||||
birpc: 4.0.0
|
||||
dts-resolver: 2.1.3([email protected])
|
||||
dts-resolver: 2.1.3([email protected](@emnapi/[email protected])(@emnapi/[email protected]))
|
||||
get-tsconfig: 4.14.0
|
||||
obug: 2.1.1
|
||||
picomatch: 4.0.4
|
||||
@@ -22094,7 +22129,7 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-bom: 3.0.0
|
||||
|
||||
[email protected](@tsdown/[email protected])([email protected])([email protected]):
|
||||
[email protected](@tsdown/[email protected])([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected]):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
cac: 7.0.0
|
||||
@@ -22105,7 +22140,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
picomatch: 4.0.4
|
||||
rolldown: 1.0.0-rc.15
|
||||
rolldown-plugin-dts: 0.23.2([email protected])([email protected])([email protected])
|
||||
rolldown-plugin-dts: 0.23.2([email protected](@emnapi/[email protected])(@emnapi/[email protected]))([email protected])([email protected])
|
||||
semver: 7.7.4
|
||||
tinyexec: 1.1.1
|
||||
tinyglobby: 0.2.16
|
||||
@@ -22670,6 +22705,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-wsl: 3.1.1
|
||||
|
||||
Reference in New Issue
Block a user