CLI: Migrate to oxc for AST manipulation

This commit is contained in:
Fuma Nama
2026-01-05 23:18:47 +08:00
parent 8110c50912
commit e0aa029edb
24 changed files with 1327 additions and 594 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@fumadocs/cli': patch
---
Migrate to oxc for AST manipulation
+6 -6
View File
@@ -14,7 +14,7 @@
},
"dependencies": {
"@ai-sdk/openai-compatible": "^2.0.2",
"@ai-sdk/react": "^3.0.6",
"@ai-sdk/react": "^3.0.7",
"@fumadocs/mdx-remote": "workspace:*",
"@mixedbread/sdk": "^0.46.0",
"@orama/core": "^1.2.16",
@@ -26,8 +26,8 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@shikijs/rehype": "^3.20.0",
"@shikijs/transformers": "^3.20.0",
"@takumi-rs/image-response": "^0.61.0",
"ai": "^6.0.6",
"@takumi-rs/image-response": "^0.62.6",
"ai": "^6.0.7",
"class-variance-authority": "^0.7.1",
"d3-force": "^3.0.0",
"estree-util-build-jsx": "^3.0.1",
@@ -47,7 +47,7 @@
"next": "16.1.1",
"next-themes": "^0.4.6",
"octokit": "^5.0.5",
"oxc-transform": "^0.106.0",
"oxc-transform": "^0.107.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-force-graph-2d": "^1.29.0",
@@ -63,7 +63,7 @@
"tailwind-merge": "^3.4.0",
"twoslash": "^0.3.6",
"unist-util-visit": "^5.0.0",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@fumadocs/cli": "workspace:*",
@@ -84,7 +84,7 @@
"browserslist-config-baseline": "^0.5.0",
"eslint-config-custom": "workspace:*",
"gray-matter": "^4.0.3",
"mcp-handler": "^1.0.4",
"mcp-handler": "^1.0.5",
"next-validate-link": "^1.6.3",
"postcss": "^8.5.6",
"shadcn": "3.6.2",
+1 -1
View File
@@ -16,7 +16,7 @@
"react-dom": "^19.2.3"
},
"devDependencies": {
"@content-collections/core": "^0.13.0",
"@content-collections/core": "^0.13.1",
"@content-collections/mdx": "^0.2.2",
"@content-collections/next": "^0.2.10",
"@types/mdx": "^2.0.13",
+1 -1
View File
@@ -26,7 +26,7 @@
"@types/node": "^24.10.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-rsc": "^0.5.7",
"@vitejs/plugin-rsc": "^0.5.10",
"react-router-devtools": "^6.0.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
+1 -1
View File
@@ -13,7 +13,7 @@
"@orama/tokenizers": "^3.1.18",
"@tanstack/react-router": "1.144.0",
"@tanstack/react-router-devtools": "1.144.0",
"@tanstack/react-start": "1.145.3",
"@tanstack/react-start": "1.145.5",
"fumadocs-core": "workspace:*",
"fumadocs-mdx": "workspace:*",
"fumadocs-ui": "workspace:*",
+2 -2
View File
@@ -13,8 +13,8 @@
"@orama/orama": "^3.1.18",
"@tanstack/react-router": "1.144.0",
"@tanstack/react-router-devtools": "1.144.0",
"@tanstack/react-start": "1.145.3",
"@tanstack/start-static-server-functions": "1.145.3",
"@tanstack/react-start": "1.145.5",
"@tanstack/start-static-server-functions": "1.145.5",
"fumadocs-core": "workspace:*",
"fumadocs-mdx": "workspace:*",
"fumadocs-ui": "workspace:*",
+1 -1
View File
@@ -12,7 +12,7 @@
"dependencies": {
"@tanstack/react-router": "1.144.0",
"@tanstack/react-router-devtools": "1.144.0",
"@tanstack/react-start": "1.145.3",
"@tanstack/react-start": "1.145.5",
"fumadocs-core": "workspace:*",
"fumadocs-mdx": "workspace:*",
"fumadocs-ui": "workspace:*",
+1 -1
View File
@@ -25,7 +25,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-tailwindcss": "^3.18.2",
"oxfmt": "^0.21.0",
"oxfmt": "^0.22.0",
"rimraf": "^6.1.2",
"turbo": "2.7.2",
"typescript": "^5.9.3",
+5 -2
View File
@@ -37,13 +37,16 @@
"dependencies": {
"@clack/prompts": "^0.11.0",
"commander": "^14.0.2",
"magic-string": "^0.30.21",
"oxc-parser": "^0.107.0",
"oxc-resolver": "^11.16.2",
"package-manager-detector": "^1.6.0",
"picocolors": "^1.1.1",
"tinyexec": "^1.0.2",
"ts-morph": "^27.0.2",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@oxc-project/types": "^0.107.0",
"@types/node": "24.10.2",
"eslint-config-custom": "workspace:*",
"shadcn": "3.6.2",
+107 -117
View File
@@ -8,7 +8,10 @@ import type {
registryInfoSchema,
} from '@/registry/schema';
import type { z } from 'zod';
import { Project, SourceFile, StringLiteral, ts } from 'ts-morph';
import { parse } from 'oxc-parser';
import { ResolverFactory } from 'oxc-resolver';
import MagicString from 'magic-string';
import { transformSpecifiers } from '@/utils/ast';
export type OnResolve = (reference: SourceReference) => Reference;
@@ -73,14 +76,10 @@ export interface Registry extends Omit<z.input<typeof registryInfoSchema>, 'inde
export class RegistryCompiler {
readonly raw: Registry;
readonly project: Project;
resolver!: RegistryResolver;
constructor(registry: Registry) {
this.raw = registry;
this.project = new Project({
tsConfigFilePath: path.join(registry.dir, registry.tsconfigPath),
});
}
private async readPackageJson(): Promise<PackageJson | undefined> {
@@ -92,13 +91,6 @@ export class RegistryCompiler {
.catch(() => undefined);
}
async createSourceFile(file: string) {
const content = await fs.readFile(file);
return this.project.createSourceFile(file, content.toString(), {
overwrite: true,
});
}
async compile(): Promise<CompiledRegistry> {
const registry = this.raw;
this.resolver = new RegistryResolver(this, await this.readPackageJson());
@@ -140,6 +132,7 @@ class RegistryResolver {
private readonly deps: Record<string, string | null>;
private readonly devDeps: Record<string, string | null>;
private readonly fileToComponent = new Map<string, [Component, ComponentFile]>();
readonly oxc: ResolverFactory;
constructor(
private readonly compiler: RegistryCompiler,
@@ -166,6 +159,15 @@ class RegistryResolver {
...packageJson?.devDependencies,
...registry.devDependencies,
};
// resolve anything possible
this.oxc = new ResolverFactory({
extensions: ['.js', '.jsx', '.ts', '.tsx', '.node'],
conditionNames: ['node', 'import', 'require', 'default', 'types'],
tsconfig: {
configFile: path.join(registry.dir, registry.tsconfigPath),
},
});
}
getDepFromSpecifier(specifier: string) {
@@ -241,6 +243,10 @@ export type SourceReference =
file: ComponentFile;
registryName: string;
};
}
| {
type: 'unknown-specifier';
specifier: string;
};
export type Reference =
@@ -253,7 +259,7 @@ export type Reference =
export class ComponentCompiler {
private readonly processedFiles = new Set<string>();
private readonly registry: Registry;
private readonly subComponents = new Set<string | z.input<typeof httpSubComponent>>();
private readonly subComponents = new Map<string, string | z.input<typeof httpSubComponent>>();
private readonly devDependencies = new Map<string, string | null>();
private readonly dependencies = new Map<string, string | null>();
@@ -279,22 +285,28 @@ export class ComponentCompiler {
name: this.component.name,
title: this.component.title,
description: this.component.description,
files: (
await Promise.all(this.component.files.map((file) => this.buildFileAndDeps(file)))
).flat(),
subComponents: Array.from(this.subComponents),
files: (await Promise.all(this.component.files.map((file) => this.onBuildFile(file)))).flat(),
subComponents: Array.from(this.subComponents.values()),
dependencies: Object.fromEntries(this.dependencies),
devDependencies: Object.fromEntries(this.devDependencies),
};
}
private async buildFileAndDeps(file: ComponentFile): Promise<CompiledFile[]> {
private async onBuildFile(file: ComponentFile): Promise<CompiledFile[]> {
if (this.processedFiles.has(file.path)) return [];
this.processedFiles.add(file.path);
const resolver = this.compiler.resolver;
const queue: ComponentFile[] = [];
const result = await this.buildFile(file, (reference) => {
if (reference.type === 'unknown-specifier') {
if (!reference.specifier.startsWith('node:')) {
console.warn(`Unknown specifier ${reference.specifier}, skipping for now`);
}
return reference.specifier;
}
if (reference.type === 'custom') return reference.specifier;
if (reference.type === 'file') {
@@ -311,16 +323,17 @@ export class ComponentCompiler {
if (reference.type === 'sub-component') {
const resolved = reference.resolved;
if (resolved.component.name !== this.component.name) {
if (resolved.type === 'remote') {
this.subComponents.add({
type: 'http',
baseUrl: resolved.registryName,
component: resolved.component.name,
});
} else {
this.subComponents.add(resolved.component.name);
}
if (resolved.component.name === this.component.name)
return this.toImportPath(resolved.file);
if (resolved.type === 'remote') {
this.subComponents.set(`${resolved.registryName}:${resolved.component.name}`, {
type: 'http',
baseUrl: resolved.registryName,
component: resolved.component.name,
});
} else {
this.subComponents.set(resolved.component.name, resolved.component.name);
}
return this.toImportPath(resolved.file);
@@ -335,54 +348,7 @@ export class ComponentCompiler {
return reference.specifier;
});
return [
result,
...(await Promise.all(queue.map((file) => this.buildFileAndDeps(file)))).flat(),
];
}
private resolveImport(
sourceFilePath: string,
specifier: string,
specified: SourceFile | undefined,
): SourceReference | undefined {
let filePath: string;
if (specified) {
filePath = specified.getFilePath();
} else if (specifier.startsWith('./') || specifier.startsWith('../')) {
filePath = path.join(path.dirname(sourceFilePath), specifier);
} else {
if (!specifier.startsWith('node:'))
console.warn(`Unknown specifier ${specifier}, skipping for now`);
return;
}
const resolver = this.compiler.resolver;
// outside of registry dir
if (path.relative(this.registry.dir, filePath).startsWith('../')) {
return {
type: 'dependency',
dep: resolver.getDepFromSpecifier(specifier),
specifier,
};
}
const sub = resolver.getSubComponent(filePath);
if (sub) {
return {
type: 'sub-component',
resolved: {
type: 'local',
component: sub.component,
file: sub.file,
},
};
}
return {
type: 'file',
file: filePath,
};
return [result, ...(await Promise.all(queue.map((file) => this.onBuildFile(file)))).flat()];
}
private async buildFile(
@@ -395,55 +361,79 @@ export class ComponentCompiler {
writeReference: (reference: Reference) => string | undefined,
): Promise<CompiledFile> {
const sourceFilePath = path.join(this.registry.dir, file.path);
const astTypes: Record<string, 'js' | 'ts' | undefined> = {
'.ts': 'ts',
'.tsx': 'ts',
'.js': 'js',
'.jsx': 'js',
};
const astType = astTypes[path.extname(file.path)];
const content = (await fs.readFile(sourceFilePath)).toString();
if (!astType)
return {
content,
path: file.path,
type: file.type,
target: file.target,
};
const resolver = this.compiler.resolver;
const ast = await parse(sourceFilePath, content, {
astType,
});
if (ast.errors.length > 0) {
throw new Error(`failed to parse file ${sourceFilePath}: \n${ast.errors.join('\n')}`);
}
const s = new MagicString(content);
/**
* Process import paths
*/
const process = (specifier: StringLiteral, specifiedFile: SourceFile | undefined) => {
transformSpecifiers(ast.program, s, (specifier) => {
let resolved: Reference = {
type: 'unknown-specifier',
specifier: specifier,
};
const onResolve = this.component.onResolve ?? this.registry.onResolve;
let resolved: Reference | undefined = this.resolveImport(
sourceFilePath,
specifier.getLiteralValue(),
specifiedFile,
);
if (!resolved) return;
if (onResolve) resolved = onResolve(resolved);
const out = writeReference(resolved);
if (out) specifier.setLiteralValue(out);
};
const sourceFile = await this.compiler.createSourceFile(sourceFilePath);
for (const item of sourceFile.getImportDeclarations()) {
process(item.getModuleSpecifier(), item.getModuleSpecifierSourceFile());
}
for (const item of sourceFile.getExportDeclarations()) {
const specifier = item.getModuleSpecifier();
if (!specifier) continue;
process(specifier, item.getModuleSpecifierSourceFile());
}
// transform async imports
const calls = sourceFile.getDescendantsOfKind(ts.SyntaxKind.CallExpression);
for (const expression of calls) {
if (
expression.getExpression().isKind(ts.SyntaxKind.ImportKeyword) &&
expression.getArguments().length === 1
) {
const argument = expression.getArguments()[0];
if (!argument.isKind(ts.SyntaxKind.StringLiteral)) continue;
process(argument, argument.getSymbol()?.getDeclarations()[0].getSourceFile());
const resolvedSpecifier = resolver.oxc.resolveFileSync(sourceFilePath, specifier);
if (resolvedSpecifier.error || !resolvedSpecifier.path) {
return writeReference(onResolve ? onResolve(resolved) : resolved);
}
}
resolved = {
type: 'file',
file: resolvedSpecifier.path,
};
// outside of registry dir
if (path.relative(this.registry.dir, resolvedSpecifier.path).startsWith('../')) {
resolved = {
type: 'dependency',
dep: resolver.getDepFromSpecifier(specifier),
specifier,
};
} else {
const sub = resolver.getSubComponent(resolvedSpecifier.path);
if (sub) {
resolved = {
type: 'sub-component',
resolved: {
type: 'local',
component: sub.component,
file: sub.file,
},
};
}
}
return writeReference(onResolve ? onResolve(resolved) : resolved);
});
return {
content: sourceFile.getFullText(),
content: s.toString(),
type: file.type,
path: file.path,
target: file.target,
+32 -25
View File
@@ -1,32 +1,36 @@
import path from 'node:path';
import fs from 'node:fs/promises';
import { confirm, isCancel, log, outro } from '@clack/prompts';
import { createEmptyProject } from '@/utils/typescript';
import { typescriptExtensions } from '@/constants';
import { toImportSpecifier } from '@/utils/ast';
import { toImportSpecifier, transformSpecifiers } from '@/utils/ast';
import type { Component, File } from '@/registry/schema';
import { HttpRegistryClient, type RegistryClient } from '@/registry/client';
import { x } from 'tinyexec';
import { DependencyManager } from '@/registry/installer/dep-manager';
import { AsyncCache } from '@/utils/cache';
import type { SourceFile } from 'ts-morph';
import { createCache } from '@/utils/cache';
import { parse, type ParseResult } from 'oxc-parser';
import MagicString from 'magic-string';
interface DownloadedComponent extends Omit<Component, 'subComponents'> {
variables?: Record<string, unknown>;
}
export interface ComponentInstallerPlugin {
transform?: (context: {
file: SourceFile;
componentFile: File;
/**
* transform file downloaded from components
*/
transformFile?: (context: {
s: MagicString;
parsed: ParseResult;
file: File;
component: DownloadedComponent;
}) => void | Promise<void>;
}
export class ComponentInstaller {
private readonly project = createEmptyProject();
private readonly installedFiles = new Set<string>();
private readonly downloadCache = new AsyncCache<DownloadedComponent[]>();
private readonly downloadCache = createCache<DownloadedComponent[]>();
readonly dependencies: Record<string, string | null> = {};
readonly devDependencies: Record<string, string | null> = {};
@@ -124,11 +128,11 @@ export class ComponentInstaller {
variables[k] ??= v.default;
}
const out = await this.downloadCache.cached(hash, async () => {
const out = await this.downloadCache.cached(hash, async (presolve) => {
const comp = await client.fetchComponent(name);
const result: DownloadedComponent[] = [comp];
// place it before downloading child components to avoid recursive downloads
this.downloadCache.store.set(hash, result);
presolve(result);
const child = await Promise.all(
comp.subComponents.map((sub) => {
@@ -152,7 +156,7 @@ export class ComponentInstaller {
return out.map((file) => ({ ...file, variables }));
}
private readonly pathToFileCache = new AsyncCache<Map<string, File>>();
private readonly pathToFileCache = createCache<Map<string, File>>();
private async transform(
taskId: string,
file: File,
@@ -160,9 +164,8 @@ export class ComponentInstaller {
allComponents: DownloadedComponent[],
): Promise<string> {
const filePath = this.resolveOutputPath(file);
const sourceFile = this.project.createSourceFile(filePath, file.content, {
overwrite: true,
});
const parsed = await parse(filePath, file.content);
const s = new MagicString(file.content);
// transform alias
const prefix = '@/';
@@ -174,32 +177,36 @@ export class ComponentInstaller {
}
return map;
});
for (const specifier of sourceFile.getImportStringLiterals()) {
transformSpecifiers(parsed.program, s, (specifier) => {
for (const [k, v] of variables) {
specifier.setLiteralValue(specifier.getLiteralValue().replaceAll(`<${k}>`, v as string));
specifier = specifier.replaceAll(`<${k}>`, v as string);
}
if (specifier.getLiteralValue().startsWith(prefix)) {
const lookup = specifier.getLiteralValue().substring(prefix.length);
if (specifier.startsWith(prefix)) {
const lookup = specifier.substring(prefix.length);
const target = pathToFile.get(lookup);
if (target) {
specifier.setLiteralValue(toImportSpecifier(filePath, this.resolveOutputPath(target)));
specifier = toImportSpecifier(filePath, this.resolveOutputPath(target));
} else {
console.warn(`cannot find the referenced file of ${specifier}`);
}
}
}
return specifier;
});
for (const plugin of this.plugins) {
await plugin.transform?.({
file: sourceFile,
componentFile: file,
await plugin.transformFile?.({
s,
parsed,
file,
component,
});
}
return sourceFile.getFullText();
return s.toString();
}
private resolveOutputPath(file: File): string {
+49
View File
@@ -1,5 +1,7 @@
import path from 'node:path';
import { typescriptExtensions } from '@/constants';
import { type Program, Visitor } from 'oxc-parser';
import type MagicString from 'magic-string';
/**
* Return the import modifier for `sourceFile` to import `referenceFile`
@@ -27,3 +29,50 @@ export function toImportSpecifier(sourceFile: string, referenceFile: string): st
return importPath.startsWith('../') ? importPath : `./${importPath}`;
}
export function transformSpecifiers(
program: Program,
s: MagicString,
transformSpecifier: (value: string) => string | undefined,
) {
new Visitor({
// static imports
ImportDeclaration(node) {
const source = node.source;
const out = transformSpecifier(source.value);
if (out) {
s.update(source.start + 1, source.end - 1, out);
source.value = out;
}
},
// dynamic imports
ImportExpression(node) {
if (node.source.type === 'Literal') {
const source = node.source;
const out = transformSpecifier(source.value as string);
if (out) {
s.update(source.start + 1, source.end - 1, out);
source.value = out;
}
}
},
// exports
ExportAllDeclaration(node) {
const source = node.source;
const out = transformSpecifier(source.value);
if (out) {
s.update(source.start + 1, source.end - 1, out);
source.value = out;
}
},
ExportNamedDeclaration(node) {
const source = node.source;
if (!source) return;
const out = transformSpecifier(source.value);
if (out) {
s.update(source.start + 1, source.end - 1, out);
source.value = out;
}
},
}).visit(program);
}
+44 -9
View File
@@ -1,12 +1,47 @@
export class AsyncCache<V> {
readonly store = new Map<string, V | Promise<V>>();
export interface AsyncCache<V> {
cached: (
key: string,
fn: (
/**
* set a cache value before the compute function completes.
*
* useful to handle recursive access.
*/
presolve: (v: V) => void,
) => V | Promise<V>,
) => V | Promise<V>;
$value: <T>() => AsyncCache<T>;
invalidate: (key: string) => void;
}
cached<V1 extends V = V>(key: string, fn: () => V1 | Promise<V1>): V1 | Promise<V1> {
let cached = this.store.get(key);
if (cached !== undefined) return cached as V1;
/**
* cache for async resources, finished promises will be resolved into original value, otherwise wrapped with a promise.
*/
export function createCache<V>(store = new Map<string, V | Promise<V>>()): AsyncCache<V> {
return {
cached(key, fn) {
let cached = store.get(key);
if (cached) return cached;
cached = fn();
this.store.set(key, cached);
return cached as V1;
}
cached = fn((v) => store.set(key, v));
if (cached instanceof Promise) {
cached = cached.then((out) => {
// replace with resolved if still exists
if (store.has(key)) {
store.set(key, out);
}
return out;
});
}
store.set(key, cached);
return cached;
},
invalidate(key) {
store.delete(key);
},
$value<T>() {
return this as unknown as AsyncCache<T>;
},
};
}
-7
View File
@@ -1,7 +0,0 @@
import { Project } from 'ts-morph';
export function createEmptyProject(): Project {
return new Project({
compilerOptions: {},
});
}
+2 -2
View File
@@ -38,10 +38,10 @@
"types:check": "tsc --noEmit"
},
"dependencies": {
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@content-collections/core": "^0.13.0",
"@content-collections/core": "^0.13.1",
"@content-collections/mdx": "^0.2.2",
"@types/node": "24.10.2",
"eslint-config-custom": "workspace:*",
+1 -1
View File
@@ -168,7 +168,7 @@
"unified": "^11.0.5",
"vfile": "^6.0.3",
"waku": "1.0.0-alpha.0",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"peerDependencies": {
"@mixedbread/sdk": "^0.46.0",
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"description": "Used to track dependency versions in create-fumadocs-app",
"dependencies": {
"@biomejs/biome": "^2.3.10",
"@biomejs/biome": "^2.3.11",
"@fumadocs/ui": "workspace:*",
"@orama/core": "^1.2.16",
"eslint": "^9.39.2",
+2 -2
View File
@@ -42,9 +42,9 @@
"estree-util-to-js": "^2.0.0",
"estree-util-value-to-estree": "^3.5.0",
"npm-to-yarn": "^3.0.1",
"oxc-transform": "^0.106.0",
"oxc-transform": "^0.107.0",
"unist-util-visit": "^5.0.0",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@mdx-js/mdx": "^3.1.1",
+1 -1
View File
@@ -41,7 +41,7 @@
"dependencies": {
"@mdx-js/mdx": "^3.1.1",
"gray-matter": "^4.0.3",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
+2 -2
View File
@@ -83,7 +83,7 @@
"unist-util-remove-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.3",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@fumadocs/mdx-remote": "workspace:*",
@@ -103,7 +103,7 @@
"remark": "^15.0.1",
"remark-directive": "^4.0.0",
"remark-stringify": "^11.0.0",
"rollup": "^4.54.0",
"rollup": "^4.55.1",
"tsconfig": "workspace:*",
"tsdown": "^0.18.4",
"vite": "^7.3.0",
+1 -1
View File
@@ -55,7 +55,7 @@
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.3",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@types/estree": "^1.0.8",
+1 -1
View File
@@ -74,7 +74,7 @@
"lucide-react": "^0.562.0",
"next-themes": "^0.4.6",
"openapi-sampler": "^1.6.2",
"react-hook-form": "^7.69.0",
"react-hook-form": "^7.70.0",
"remark": "^15.0.1",
"remark-rehype": "^11.1.2",
"tailwind-merge": "^3.4.0",
+2 -2
View File
@@ -43,7 +43,7 @@
},
"dependencies": {
"@vitejs/plugin-react": "^5.1.2",
"@vitejs/plugin-rsc": "^0.5.7",
"@vitejs/plugin-rsc": "^0.5.10",
"commander": "^14.0.2",
"fumadocs-core": "workspace:*",
"fumadocs-mdx": "workspace:*",
@@ -52,7 +52,7 @@
"react-router": "^7.11.0",
"tinyglobby": "^0.2.15",
"vite": "^7.3.0",
"zod": "^4.3.4"
"zod": "^4.3.5"
},
"devDependencies": {
"@mdx-js/mdx": "^3.1.1",
+1059 -408
View File
File diff suppressed because it is too large Load Diff